Skip to content

Domain Specification: Customer Onboarding

Validated against PRD v1.0 — All FR-ON-* requirements implemented here. See Traceability Matrix.


1. Context Overview

Bounded Context: onboarding
Responsibility: Manage the customer onboarding lifecycle — from application intake to final acceptance decision.
Owns: Customer, Individual, LegalEntity, OwnershipRelationship, Document, ApplicationIntake, OnboardingDecision aggregates.
Depends On: Configuration Engine (workflow templates, document rules), Workflow Orchestration (state machine driver), Screening (invoked at screening step), Risk Rating (invoked at risk step), Network Analysis (invoked for ownership mapping).
Events Published: CustomerCreated, DocumentUploaded, IdentityVerified, UBOIdentified, OnboardingDecisionMade.


2. State Machine

stateDiagram-v2
    [*] --> NEW
    NEW --> INTAKE : application submitted
    INTAKE --> DATA_COLLECTION : classification complete
    DATA_COLLECTION --> DOCUMENT_COLLECTION : data captured
    DOCUMENT_COLLECTION --> VALIDATION_PENDING : mandatory docs received
    VALIDATION_PENDING --> ON_HOLD : missing info

    state VALIDATION_PENDING {
        [*] --> identity_validated
        identity_validated --> SCREENING_PENDING
        identity_validated --> RISK_ASSESSMENT_PENDING
        identity_validated --> NETWORK_ANALYSIS_PENDING
    }

    SCREENING_PENDING --> ANALYST_REVIEW : screening result
    RISK_ASSESSMENT_PENDING --> ANALYST_REVIEW : risk assessment
    NETWORK_ANALYSIS_PENDING --> ANALYST_REVIEW : network analysis

    ANALYST_REVIEW --> WAITING_EXTERNAL : additional docs requested
    ANALYST_REVIEW --> APPROVED : LOW risk fast-track
    ANALYST_REVIEW --> APPROVED : MEDIUM standard review
    ANALYST_REVIEW --> REJECTED : MEDIUM standard review
    ANALYST_REVIEW --> EDD_REVIEW : HIGH risk

    EDD_REVIEW --> COMPLIANCE_APPROVAL : EDD report complete
    COMPLIANCE_APPROVAL --> APPROVED : FCC approves
    COMPLIANCE_APPROVAL --> REJECTED : FCC rejects
    COMPLIANCE_APPROVAL --> POLICY_EXCEPTION : policy override

    APPROVED --> CLOSED
    REJECTED --> CLOSED
    WITHDRAWN --> CLOSED

State Transition Rules

From To Trigger Actor
NEW INTAKE Application submitted Relationship Manager
INTAKE DATA_COLLECTION Classification complete Onboarding Specialist
DATA_COLLECTION DOCUMENT_COLLECTION All required data fields captured System
DOCUMENT_COLLECTION VALIDATION_PENDING Mandatory documents received System (minimum docs check)
VALIDATION_PENDING SCREENING_PENDING Identity validated System
VALIDATION_PENDING RISK_ASSESSMENT_PENDING Identity validated System
VALIDATION_PENDING NETWORK_ANALYSIS_PENDING Identity validated (conditional: corporate only) System
SCREENING_PENDING ANALYST_REVIEW Screening result received System
RISK_ASSESSMENT_PENDING ANALYST_REVIEW Risk assessment received System
NETWORK_ANALYSIS_PENDING ANALYST_REVIEW Network analysis received System
ANALYST_REVIEW APPROVED Analyst approves low/medium risk KYC Analyst
ANALYST_REVIEW EDD_REVIEW High risk → escalate to EDD System (auto-branch)
ANALYST_REVIEW WAITING_EXTERNAL Additional documents requested KYC Analyst
EDD_REVIEW COMPLIANCE_APPROVAL EDD report complete EDD Analyst
COMPLIANCE_APPROVAL APPROVED FCC approves FCC Reviewer
COMPLIANCE_APPROVAL REJECTED FCC rejects FCC Reviewer
COMPLIANCE_APPROVAL POLICY_EXCEPTION Policy override required FCC Reviewer
Any active ON_HOLD Manual hold Supervisor
Any active PROHIBITED Sanctions confirmed hit System (auto-block)
WAITING_EXTERNAL ANALYST_REVIEW Documents received System

Guard: Invalid transitions blocked and logged. Transitions only allowed from states listed in From column.


3. Data Model

3.1 Customer (Aggregate Root)

data class Customer(
    val customerId: UUID,
    val customerType: CustomerType,       // INDIVIDUAL, LEGAL_ENTITY
    val status: CustomerStatus,           // ACTIVE, SUSPENDED, CLOSED, PROHIBITED
    val riskBand: RiskBand?,              // LOW, MEDIUM, HIGH (updated by risk rating)
    val createdAt: Instant,
    val updatedAt: Instant,
    // Related entities loaded on demand (not part of aggregate root):
    // individual: Individual?
    // legalEntity: LegalEntity?
    // addresses: List<Address>
    // ownershipRelationships: List<OwnershipRelationship>
    // documents: List<Document>
)

enum class CustomerType { INDIVIDUAL, LEGAL_ENTITY }
enum class CustomerStatus { ACTIVE, SUSPENDED, CLOSED, PROHIBITED, WITHDRAWN }

3.2 Individual

data class Individual(
    val individualId: UUID,              // = customerId (1:1)
    val firstName: String,
    val lastName: String,
    val aliases: List<String>,
    val dateOfBirth: LocalDate,
    val nationality: Country,
    val residenceCountry: Country,
    val taxResidency: Country?,
    val occupation: String?,
    val pepFlag: Boolean,
    val pepLevel: PepLevel?,            // NATIONAL, INTERNATIONAL, CLOSE_ASSOCIATE
    val sourceOfWealth: String?,        // encrypted at rest
    val sourceOfFunds: String?          // encrypted at rest
)

3.3 LegalEntity

data class LegalEntity(
    val entityId: UUID,                  // = customerId (1:1)
    val legalName: String,
    val tradeName: String?,
    val registrationNumber: String,
    val incorporationCountry: Country,
    val legalForm: LegalForm,            // LLC, PLC, LTD, PARTNERSHIP, etc.
    val industryCode: String?,
    val incorporationDate: LocalDate
)

3.4 OwnershipRelationship

data class OwnershipRelationship(
    val relationshipId: UUID,
    val parentEntityId: UUID,            // owner
    val childEntityId: UUID,             // owned
    val ownershipPercentage: BigDecimal, // 0 < x <= 100
    val controlType: ControlType,        // DIRECT, INDIRECT, BENEFICIAL
    val effectiveFrom: LocalDate,
    val effectiveTo: LocalDate?,         // null = current
    val confidenceScore: Double          // 0.0 - 1.0
)

3.5 Document

data class Document(
    val documentId: UUID,
    val customerId: UUID,
    val documentType: DocumentType,
    val fileName: String,
    val content: ByteArray,              // stored in DB BYTEA, encrypted at rest
    val contentHash: String,             // SHA-256
    val issueDate: LocalDate?,
    val expiryDate: LocalDate?,
    val validationStatus: ValidationStatus, // PENDING, VERIFIED, EXPIRED, REJECTED
    val validatedBy: UUID?,
    val validatedAt: Instant?,
    val uploadedAt: Instant
)

enum class DocumentType {
    PASSPORT, DRIVERS_LICENSE, NATIONAL_ID,
    INCORPORATION_CERTIFICATE, CHAMBER_REGISTRATION,
    SHAREHOLDER_REGISTER, DIRECTOR_IDENTIFICATION,
    UBO_DECLARATION, PROOF_OF_ADDRESS,
    SOURCE_OF_WEALTH, SOURCE_OF_FUNDS,
    BUSINESS_LICENSE, TAX_REGISTRATION
}

enum class ValidationStatus { PENDING, VERIFIED, EXPIRED, REJECTED }

3.6 ApplicationIntake

data class ApplicationIntake(
    val intakeId: UUID,
    val customerId: UUID,
    val submittedBy: UUID,               // Relationship Manager
    val customerType: CustomerType,
    val jurisdiction: Country,
    val businessLine: String,
    val productInterest: String,
    val expectedMonthlyVolume: String?,  // LOW, MEDIUM, HIGH
    val expectedAnnualTurnover: String?,
    val notes: String?,
    val classification: CustomerArchetype?,
    val workflowTemplateId: String?,     // resolved after classification
    val submittedAt: Instant
)

enum class CustomerArchetype {
    RETAIL_INDIVIDUAL, SME, CORPORATE, CORRESPONDENT_BANKING,
    PRIVATE_BANKING, LEASING, SPECIALIZED
}

3.7 OnboardingDecision

data class OnboardingDecision(
    val decisionId: UUID,
    val customerId: UUID,
    val decisionType: DecisionType,
    val actorType: ActorType,            // USER, SYSTEM
    val actorId: UUID?,
    val rationale: String,               // mandatory, cannot be empty
    val restrictions: String?,           // only when APPROVED_WITH_RESTRICTIONS
    val evidenceRefs: List<UUID>,        // document IDs supporting this decision
    val configVersion: String,           // config version active when decision made
    val madeAt: Instant
)

enum class DecisionType {
    APPROVED, APPROVED_WITH_RESTRICTIONS, APPROVED_PENDING_EDD,
    REJECTED, WITHDRAWN, PROHIBITED
}

4. API Contracts

4.1 Submit Application

POST /api/v1/onboarding/applications
Authorization: Bearer <jwt>
Idempotency-Key: <uuid>

Request:
{
  "customerType": "LEGAL_ENTITY",
  "legalName": "ACME Holdings B.V.",
  "registrationNumber": "12345678",
  "incorporationCountry": "NLD",
  "jurisdiction": "NLD",
  "businessLine": "COMMERCIAL_LENDING",
  "productInterest": "TERM_LOAN",
  "expectedMonthlyVolume": "MEDIUM",
  "notes": "Existing relationship with subsidiary ACME Trading"
}

Response 201:
{
  "applicationId": "uuid",
  "customerId": "uuid",
  "status": "INTAKE",
  "classification": null,
  "nextAction": "Awaiting intake review by Onboarding Specialist"
}

4.2 Classify Customer

POST /api/v1/onboarding/applications/{applicationId}/classify
Authorization: Bearer <jwt>

Request body optional — system auto-classifies based on intake data. Specialist can override.

Response 200:
{
  "applicationId": "uuid",
  "classification": "CORPORATE",
  "workflowTemplateId": "Corporate_NLD_Lending_Onboarding_v1",
  "requiredDocuments": [
    { "type": "INCORPORATION_CERTIFICATE", "mandatory": true },
    { "type": "CHAMBER_REGISTRATION", "mandatory": true },
    { "type": "SHAREHOLDER_REGISTER", "mandatory": true },
    { "type": "DIRECTOR_IDENTIFICATION", "mandatory": true },
    { "type": "UBO_DECLARATION", "mandatory": true }
  ],
  "state": "DATA_COLLECTION"
}

4.3 Upload Document

POST /api/v1/onboarding/customers/{customerId}/documents
Content-Type: multipart/form-data

Form fields: documentType, issueDate (optional), expiryDate (optional)
File: <binary>

Response 201:
{
  "documentId": "uuid",
  "documentType": "INCORPORATION_CERTIFICATE",
  "validationStatus": "PENDING",
  "uploadedAt": "2025-06-01T10:30:00Z"
}

4.4 Validate Document

POST /api/v1/onboarding/documents/{documentId}/validate
Authorization: Bearer <jwt>

Request:
{
  "status": "VERIFIED",
  "notes": "Incorporation certificate matches Chamber of Commerce registry"
}

Response 200:
{
  "documentId": "uuid",
  "validationStatus": "VERIFIED",
  "validatedBy": "user-uuid",
  "validatedAt": "2025-06-01T10:35:00Z"
}

4.5 Add Ownership

POST /api/v1/onboarding/customers/{customerId}/ownership
Authorization: Bearer <jwt>

Request:
{
  "parentEntityId": "ubo-customer-uuid",
  "ownershipPercentage": 45.0,
  "controlType": "DIRECT",
  "effectiveFrom": "2020-01-01",
  "confidenceScore": 0.95
}

Response 201: { "relationshipId": "uuid" }

4.6 Get UBOs

GET /api/v1/onboarding/customers/{customerId}/ubos?threshold=25
Authorization: Bearer <jwt>

Response 200:
{
  "customerId": "uuid",
  "uboThreshold": 25.0,
  "ubos": [
    {
      "entityId": "uuid",
      "name": "John Smith",
      "ownershipPercentage": 45.0,
      "chain": ["ACME Holdings B.V.", "ACME International Ltd.", "John Smith"],
      "depth": 2
    }
  ],
  "totalDeclared": 100.0,
  "unidentifiedGap": 0.0
}

4.7 Make Decision

POST /api/v1/onboarding/customers/{customerId}/decisions
Authorization: Bearer <jwt>
Idempotency-Key: <uuid>

Request:
{
  "decisionType": "APPROVED_WITH_RESTRICTIONS",
  "rationale": "Customer represents moderate risk due to cross-border ownership. Approved subject to enhanced transaction monitoring for first 6 months.",
  "restrictions": "Enhanced transaction monitoring for 6 months. Monthly review of transaction patterns.",
  "evidenceRefs": ["doc-uuid-1", "doc-uuid-2"]
}

Response 201:
{
  "decisionId": "uuid",
  "customerId": "uuid",
  "decisionType": "APPROVED_WITH_RESTRICTIONS",
  "state": "CLOSED",
  "madeAt": "2025-06-01T14:00:00Z"
}

5. Workflow Templates

Corporate Netherlands Lending — Template Definition (Config Engine Format)

{
  "templateId": "Corporate_NLD_Lending_Onboarding_v1",
  "customerArchetype": "CORPORATE",
  "jurisdiction": "NLD",
  "businessLine": "COMMERCIAL_LENDING",
  "states": [
    { "name": "NEW", "initial": true },
    { "name": "INTAKE" },
    { "name": "DATA_COLLECTION" },
    { "name": "DOCUMENT_COLLECTION" },
    { "name": "VALIDATION_PENDING" },
    { "name": "SCREENING_PENDING" },
    { "name": "RISK_ASSESSMENT_PENDING" },
    { "name": "NETWORK_ANALYSIS_PENDING" },
    { "name": "ANALYST_REVIEW" },
    { "name": "EDD_REVIEW" },
    { "name": "COMPLIANCE_APPROVAL" },
    { "name": "APPROVED" },
    { "name": "REJECTED" },
    { "name": "CLOSED" },
    { "name": "ON_HOLD" },
    { "name": "WAITING_EXTERNAL" }
  ],
  "transitions": [ /* ... see state machine above ... */ ],
  "parallelTasks": {
    "after": "VALIDATION_PENDING",
    "tasks": [
      { "type": "SCREENING", "module": "name_screening" },
      { "type": "RISK_RATING", "module": "customer_risk_rating" },
      { "type": "NETWORK_ANALYSIS", "module": "network_analysis" }
    ],
    "waitFor": "ALL"
  },
  "humanTasks": [
    {
      "state": "ANALYST_REVIEW",
      "taskType": "ONBOARDING_REVIEW",
      "assignedRole": "KYC_ANALYST",
      "slaHours": 24,
      "form": {
        "actions": ["APPROVE", "REJECT", "REQUEST_INFO", "ESCALATE"],
        "requiredFields": ["rationale"]
      }
    },
    {
      "state": "EDD_REVIEW",
      "taskType": "EDD_DEEP_DIVE",
      "assignedRole": "EDD_ANALYST",
      "slaHours": 48,
      "form": {
        "actions": ["SUBMIT_REPORT", "REQUEST_INFO"],
        "requiredFields": ["eddReport", "recommendation"]
      }
    },
    {
      "state": "COMPLIANCE_APPROVAL",
      "taskType": "COMPLIANCE_DECISION",
      "assignedRole": "FCC_REVIEWER",
      "slaHours": 24,
      "form": {
        "actions": ["APPROVE", "REJECT", "OVERRIDE"],
        "requiredFields": ["rationale"]
      }
    }
  ],
  "conditionalBranches": [
    {
      "state": "ANALYST_REVIEW",
      "condition": "riskBand == 'HIGH'",
      "targetState": "EDD_REVIEW"
    },
    {
      "state": "ANALYST_REVIEW",
      "condition": "riskBand == 'MEDIUM' && screeningStatus == 'NO_MATCH'",
      "targetState": "APPROVED"
    }
  ],
  "slaTimers": {
    "INTAKE": { "hours": 4 },
    "DATA_COLLECTION": { "hours": 8 },
    "DOCUMENT_COLLECTION": { "hours": 24, "pauseOnState": "WAITING_EXTERNAL" },
    "VALIDATION_PENDING": { "hours": 24 },
    "SCREENING_PENDING": { "hours": 1 },
    "RISK_ASSESSMENT_PENDING": { "hours": 1 },
    "ANALYST_REVIEW": { "hours": 24 },
    "EDD_REVIEW": { "hours": 48 },
    "COMPLIANCE_APPROVAL": { "hours": 24 }
  }
}

6. Document Requirements (Configurable)

Default Rules by Archetype

Archetype Mandatory Documents Optional
Retail Individual Passport/ID, Proof of Address
SME Chamber Registration, Director ID, UBO Declaration Shareholder Register
Corporate Incorporation Cert, Chamber Registration, Shareholder Register, Director IDs (all), UBO Declaration Business License
Correspondent Banking All Corporate docs + Source of Wealth, Regulatory License
Private Banking Source of Wealth, Source of Funds, Tax Residency Proof

Validation Rules

  • Passport/ID: Must not be expired. Issue date ≤ today. Expiry date ≥ today + 30 days (grace period configurable).
  • Incorporation Certificate: Registration number must match corporate registry lookup. Incorporation date ≤ today.
  • Proof of Address: Must be ≤ 3 months old (configurable). Address must match declared residence/registered address.
  • UBO Declaration: Must list all individuals with ≥ 25% ownership (threshold configurable). Total declared ownership must = 100% ± 5%.

7. Escalation Rules

Trigger Level Route To Auto Action
Document missing > SLA L2 Supervisor Reminder escalated
Identity mismatch L2 Senior KYC Analyst Task re-assigned
Ownership complexity > 3 levels L2 Senior KYC Analyst EDD recommendation
Risk score = HIGH L3 EDD Analyst Branch to EDD_REVIEW
PEP flag + adverse indicators L3 EDD Analyst Deep due diligence
Sanctions POTENTIAL_MATCH L3 Sanctions Analyst Concurrent sanctions investigation
Sanctions CONFIRMED_MATCH L5 FCC + Legal Auto-freeze (PROHIBITED)
Policy exception required L4 FCC Reviewer Manual override
SLA breach (any task) L2 Supervisor Case escalated

8. Error Handling & Edge Cases

Scenario Behavior
Duplicate application Detected by matching registration number + jurisdiction. Returns 409 Conflict with existing application reference.
External registry unavailable Identity validation returns UNAVAILABLE. Task created for manual verification. Workflow continues on other parallel tracks.
Document exceeds size limit (10MB) 413 Payload Too Large. Error message suggests compression or alternative submission.
Invalid state transition 422 Unprocessable Entity. Message: "Cannot transition from X to Y. Allowed transitions: A, B, C."
Approve without rationale 400 Bad Request. "Rationale is required for all onboarding decisions."
Approve own case (SoD violation) 403 Forbidden. "Segregation of duties: case creator cannot approve the same case."
Customer already prohibited Cannot submit new application. 409 Conflict. "Customer is prohibited. Reason: Sanctions block dated 2024-03-15."
Ownership < 100% System flags gap. Analyst must resolve: either identify missing owners or document why ownership is incomplete.
UBO is a minor PEP-like flags raised. Requires additional guardian/representative documentation.
Document expired during onboarding Validation status changes to EXPIRED. New document requested. SLA timer pauses (WAITING_EXTERNAL).

9. Integration Points

Inbound (who calls Onboarding)

Caller Endpoint Purpose
Workflow Engine Internal (Temporal activity) Advance state, invoke document validation
Relationship Manager POST /applications Submit new application
Onboarding Specialist POST /{id}/classify Classify customer

Outbound (who Onboarding calls)

Target When Purpose
Name Screening After VALIDATION_PENDING Screen customer and all UBOs
Risk Rating After VALIDATION_PENDING Calculate risk score
Network Analysis After VALIDATION_PENDING (corporate only) Map ownership graph
Audit Service Every state transition, decision, document upload Immutable logging
Notification Engine SLA warnings, task assignments, status changes Analyst alerts
Configuration Engine At workflow start and module invocation Get active config version

Spec validated against PRD v1.0 requirements FR-ON-01 through FR-ON-06. Re-evaluate if state machine or document rules change.