Declaration Lifecycle¶
Overview¶
Declarations are the central entity in Regnify, representing a representative's submission to the Monetary Authority of Singapore (MAS). Every declaration follows a status state machine that governs its progression from creation through internal approval to MAS submission and final disposition.
The declarations table stores all declarations, with the formType column distinguishing between declaration types. The DeclarationStatus enum in Prisma defines 12 statuses. Valid transitions between statuses are enforced by VALID_TRANSITIONS in declaration-status.ts.
Declaration Types¶
| formType | Description | Workflow Behaviour |
|---|---|---|
DECLARATION_FORM_3A |
Appointed Representative application (Form 3A) | Auto-creates multi-step workflow instance on submit (from org's template) |
DECLARATION_ATTESTATION_QUARTERLY |
Quarterly compliance attestation | Does NOT auto-create workflow instance. Single-step approval via approveAttestation. |
DECLARATION_INCIDENT_DISCLOSURE |
Incident disclosure filing | Auto-creates single-step workflow instance (Review by Compliance) |
The default formType is "DECLARATION_FORM_3A" if not specified at creation.
Canonical Status Flow¶
Draft -> Pending Review -> Approved -> Good -> MAS_PENDING_REVIEW -> MAS_APPROVED
| | |
Rejected Rejected MAS_REJECTED
| | |
(re-editable) (re-editable) (re-editable)
PENDING_REVIEW can also transition to:
- REVISION_REQUESTED (via sendBackDeclaration)
- which can return to PENDING_REVIEW (via resubmitDeclaration)
The full DeclarationStatus enum (12 values):
| Enum Value | Display Label | Meaning |
|---|---|---|
DRAFT |
Draft | Declaration being edited, not yet submitted |
PENDING_REVIEW |
Pending Review | Submitted, awaiting approval chain |
APPROVED |
Approved | All internal approvals complete |
REJECTED |
Rejected | Rejected by an approver; editable by rep |
REVISION_REQUESTED |
Revision Requested | Sent back for revision; prior approvals RETAINED |
GOOD |
Good | COMP confirmed all checks complete, ready for MAS |
FORM_COMPLETED |
Form Completed | Legacy intermediate status |
CLEARED_FOR_MAS |
Cleared for MAS Submission | Legacy intermediate status |
SUBMITTED_TO_MAS |
Submitted to MAS | Legacy intermediate status |
MAS_PENDING_REVIEW |
MAS Pending Review | Submitted to MAS via CoRe, awaiting MAS response |
MAS_APPROVED |
MAS Approved | MAS has approved the representative |
MAS_REJECTED |
MAS Rejected | MAS has rejected; editable by rep |
Note: FORM_COMPLETED, CLEARED_FOR_MAS, and SUBMITTED_TO_MAS are legacy intermediate statuses from the NYP reference backend. The canonical production flow skips these and goes directly Good -> MAS_PENDING_REVIEW via sendToMas().
Status Transitions and Mutations¶
Draft -> Pending Review: submitDeclaration¶
Roles: FI_USER
Pre-conditions:
- Declaration status must be Draft or Revision Requested (both in SUBMITTABLE_STATUSES)
- At least 1 supporting document attached (Form 3A only; enforced in service, not resolver)
- Form data passes validation (validateForm3aForSubmission for Form 3A, validateIncidentFormData for incidents)
- Attestation declarations use submitAttestationResponse instead (see below)
Post-conditions:
- Status set to Pending Review, statusLabel to PENDING_REVIEW
- submittedAt set to current timestamp
- Workflow instance auto-created from org's template for DECLARATION_FORM_3A and DECLARATION_INCIDENT_DISCLOSURE
- Notifications sent to approvers on pending steps
- PubSub event declarationStatusChanged emitted
Pending Review -> Approved: approveDeclaration¶
Roles: HR_ADMIN, COMP, ORG_ADMIN, SYS_ADMIN
Pre-conditions: Status must be PENDING_REVIEW
Post-conditions:
- Status set to Approved, statusLabel to APPROVED
- approvedAt set to current timestamp
- Notification sent to rep
Note on workflow-step approval: In the multi-step workflow chain, approveDeclaration is the declaration-level mutation. When the LAST workflow step is approved via approveWorkflowStep, the declaration is auto-transitioned to Approved by WorkflowService.approveStep(). Individual step approvals use approveWorkflowStep, not approveDeclaration.
Pending Review -> Rejected: rejectDeclaration¶
Roles: HR_ADMIN, COMP, ORG_ADMIN, SYS_ADMIN
Pre-conditions: Status must be PENDING_REVIEW
Post-conditions:
- Status set to Rejected, statusLabel to REJECTED
- All prior approvals are LOST -- rep must restart from Draft
- Notification sent to rep
Pending Review -> Revision Requested: sendBackDeclaration¶
Roles: HR_ADMIN, COMP, ORG_ADMIN, SYS_ADMIN
Pre-conditions: Status must be PENDING_REVIEW
Post-conditions:
- Status set to Revision Requested, statusLabel to REVISION_REQUESTED
- Prior workflow-step approvals are RETAINED (unlike reject)
- Rep revises and calls resubmitDeclaration which resets workflow steps to pending and restarts from step 0
- Notification sent to rep with comment
Key distinction from reject: sendBack retains the approval chain context (approver identities, step history). reject is a full restart where all approval work is discarded.
Revision Requested -> Pending Review: resubmitDeclaration¶
Roles: FI_USER
Pre-conditions: Status must be Draft or Revision Requested (both in SUBMITTABLE_STATUSES). The service checks: "Declaration can only be resubmitted when status is Revision Requested or Draft".
Post-conditions:
- Form data re-validated
- Document minimum re-checked (Form 3A only)
- All workflow steps reset to pending via resetForResubmission() -- clears comment, actedAt, sets all step statuses back to pending
- Instance currentStepIndex reset to 0, instance status set to pending
- Declaration version incremented by 1
- Status set to Pending Review
- submittedAt set to current timestamp
Approved -> Good: markAsGood¶
Roles: COMP, SYS_ADMIN
Pre-conditions: Status must be APPROVED
Post-conditions: Status set to Good, statusLabel to GOOD
This is the COMP officer's final internal sign-off confirming all internal checks are complete, and the declaration is ready to be sent to MAS.
Good -> MAS_PENDING_REVIEW: sendToMas¶
Roles: COMP, SYS_ADMIN
Pre-conditions: Status must be GOOD
Post-conditions:
- Status set to MAS_PENDING_REVIEW, statusLabel to MAS_PENDING_REVIEW
- submittedAt set to current timestamp (marks MAS submission time)
MAS_PENDING_REVIEW -> MAS_APPROVED: approveMas¶
Roles: SYS_ADMIN
Pre-conditions: Status must be MAS_PENDING_REVIEW
Post-conditions:
- Status set to MAS_APPROVED, statusLabel to MAS_APPROVED
- approvedAt set to current timestamp
MAS_PENDING_REVIEW -> MAS_REJECTED: rejectMas¶
Roles: SYS_ADMIN
Pre-conditions: Status must be MAS_PENDING_REVIEW
Post-conditions: Status set to MAS_REJECTED, statusLabel to MAS_REJECTED
After MAS rejection: The declaration can be re-edited (MAS_REJECTED -> Draft transition is valid per VALID_TRANSITIONS).
Rejected -> Draft¶
Automatic transition path when the rep re-opens a rejected declaration for editing. Both REJECTED and MAS_REJECTED can transition to DRAFT per VALID_TRANSITIONS.
Attestation-Specific Transitions¶
Attestation declarations (DECLARATION_ATTESTATION_QUARTERLY) use a different set of mutations, all in AttestationService:
| Mutation | From | To | Roles |
|---|---|---|---|
submitAttestationResponse |
Draft or Revision Requested | Pending Review | FI_USER, REP_USER, COMP, HR_ADMIN, ORG_ADMIN, SYS_ADMIN |
approveAttestation |
Pending Review | Approved | COMP, HR_ADMIN, ORG_ADMIN, SYS_ADMIN |
submitAttestationResponse merges the rep's attestation answers (cpdConfirmed, cmfasConfirmed, noAdverseChange, etc.) into formData.responses. It does NOT auto-create a workflow instance. The approveAttestation mutation is a thin wrapper that validates the declaration is an attestation, then delegates to declarationService.approveDeclaration().
Soft Delete¶
Draft declarations can be soft-deleted (deletedAt set to current time):
Roles: FI_USER, SYS_ADMIN
Constraints: - Only Draft-status declarations can be deleted - FI_USER can only delete their own drafts; SYS_ADMIN can delete any draft - Non-Draft declarations cannot be deleted (only rejected/sent back)
Blocking Rule¶
A representative cannot submit a new Form 3A if another non-Draft declaration (status != "Draft") already exists for them with formType = "DECLARATION_FORM_3A". This prevents duplicate applications in the approval pipeline. Attestation and incident declarations do not participate in this blocking check -- the rule is specific to Form 3A.
Implementation note: This blocking rule is enforced on the frontend side (the Form 3A single-page form). The backend submitDeclaration mutation does not currently validate for existing non-Draft declarations server-side.
Auto-Save¶
The Form 3A single-page form auto-saves form data as a Draft every 30 seconds. This is a frontend-side feature implemented in Form3AContainer.tsx, not a backend mutation. The auto-save calls updateDeclaration with the current form state.
Document Minimum¶
At least 1 supporting document is required before a Form 3A declaration can be submitted. This is enforced in DeclarationService.submitDeclaration() and DeclarationService.resubmitDeclaration():
if (
(!decl.formType || decl.formType === "DECLARATION_FORM_3A") &&
!decl.documents?.length
) {
throw new BadRequestException("At least 1 supporting document is required");
}
This check applies only to Form 3A (and the default null formType, treated as Form 3A). Attestation and incident declarations are exempt.
Supporting Document Categories¶
The DocumentCategory enum defines 11 categories:
| Enum Value | Typical Content |
|---|---|
RESUME |
CV / resume |
CPF |
CPF contribution statements |
EDUCATION |
Degree certificates, transcripts |
LICENSE |
Professional licenses |
IDENTIFICATION |
NRIC, passport copies |
PROFESSIONAL_QUALIFICATIONS |
CMFAS certificates, CPD course certificates |
EMPLOYMENT_HISTORY |
Employment letters, contracts |
FIT_AND_PROPER_CHECK |
Fit & Proper assessment records |
CMFAS |
CMFAS exam result slips |
CREDIT_BUREAU |
Credit bureau reports |
CPF_CONTRIBUTION |
CPF contribution history |
Overdue Declaration Rule¶
A declaration is considered "overdue" when it remains in PENDING_REVIEW at any single approval step for more than 5 business days. This is a compliance monitoring metric; the system does not auto-reject or auto-escalate overdue declarations, but the overdue status is surfaced in dashboards and the chatbot.
MAS Processing Timeline¶
- Standard MAS processing: 14 business days
- MAS queries (requests for additional information): typically responded to within 7 business days
- These are regulatory expectations, not hardcoded system timeouts
Roles Summary¶
| Role | Can Create | Can Submit (Form 3A) | Can Submit (Attestation) | Can Approve | Can Reject | Can sendBack | Can markAsGood | Can sendToMas | Can approveMas/rejectMas |
|---|---|---|---|---|---|---|---|---|---|
| FI_USER | Yes | Yes (own) | Yes (own) | No | No | No | No | No | No |
| REP_USER | No | No | Yes (own) | No | No | No | No | No | No |
| HR_ADMIN | Yes | No | Yes | Yes | Yes | Yes | No | No | No |
| COMP | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | No |
| ORG_ADMIN | Yes | No | Yes | Yes | Yes | Yes | No | No | No |
| SYS_ADMIN | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes (approveMas, rejectMas) |
Note: submitDeclaration (Form 3A) is restricted to @Roles("FI_USER") only. submitAttestationResponse (attestation cycles) allows FI_USER, REP_USER, COMP, HR_ADMIN, ORG_ADMIN, and SYS_ADMIN. These are separate mutations with different role guards.
Key Fields on the Declarations Table¶
| Column | Type | Description |
|---|---|---|
id |
Int (auto-increment) | Primary key |
repId |
String | Representative identifier |
userId |
String | Keycloak sub claim of the owning user (String per Loop 84 migration) |
repName |
String? | Cached representative name |
formData |
Json? | Full form data payload (varies by formType) |
formType |
String (default: "DECLARATION_FORM_3A") |
Declaration type discriminator |
repProfileId |
Int? | FK to rep_profiles (null for users without a rep profile) |
status |
String (default: "Draft") |
Current status display string |
statusLabel |
String? | Enum key (e.g., "PENDING_REVIEW") |
version |
Int (default: 1) | Incremented on each resubmission |
submittedAt |
DateTime? | When the declaration was submitted to Pending Review or MAS |
approvedAt |
DateTime? | When the declaration was approved (internal or MAS) |
deletedAt |
DateTime? | Soft-delete timestamp (null = active) |