第 08 課 · Lesson 08

投保流程 | Application & Proposal Flow

投保表資料模型;e-application 流程;濕簽 vs 數位簽名;狀態機

Plays in the sticky player at the bottom of the page

課堂筆記

學習目標 · Learning Objectives

  1. Describe the application form data model that a Hong Kong POS must hold: the four-party structure (proposer 投保人 / life assured 受保人 / beneficiary 受益人 / trustee or parent guardian for a minor), the funding and ownership fields, the money-laundering fields, and the fields that carry a versioned product definition rather than a free-text answer.
  2. Explain e-application (電子投保) end to end: what the client does on their own device versus what the agent does in front of the client, which steps legally require the client's own hand, and why a Hong Kong agent almost never lets the client complete the medical disclosure alone.
  3. Operate the proposal → application → policy-issue state machine and get the signature evidence right: name every state, every transition and the guard on each; distinguish a wet signature (紙本簽署 / wet-ink) from a digital signature for insurance purposes and state when each is required by carrier policy; know what evidence the POS must store for each; and know which transitions are reversible, which require four-eyes approval, and which are terminal.

Application & Proposal Flow (投保流程)

Lesson 08 · Insurance POS 101 · Hong Kong market · agent-facing

Learning Objectives

  • Describe the application form data model that a Hong Kong POS must hold: the four-party structure (proposer 投保人 / life assured 受保人 / beneficiary 受益人 / trustee or parent guardian for a minor), the funding and ownership fields, the money-laundering fields, and the fields that carry a versioned product definition rather than a free-text answer.
  • Explain e-application (電子投保) end to end: what the client does on their own device versus what the agent does in front of the client, which steps legally require the client's own hand, and why a Hong Kong agent almost never lets the client complete the medical disclosure alone.
  • Operate the proposal → application → policy-issue state machine and get the signature evidence right: name every state, every transition and the guard on each; distinguish a wet signature (紙本簽署 / wet-ink) from a digital signature for insurance purposes and state when each is required by carrier policy; know what evidence the POS must store for each; and know which transitions are reversible, which require four-eyes approval, and which are terminal.

The Application Form Data Model

In practice: the agent does not type a name into a box. The agent opens the application and the POS pre-fills the proposer from the client record, the coverage from the accepted quotation, and leaves the medical disclosure blank — and the discipline the POS enforces is that anything pre-filled from a quote must be re-attested by the client, because a quote is a hypothesis and an application is a statement.

The four-party problem

Most Hong Kong protection cases are sold in one of five party patterns, and getting the parties wrong is the single most common cause of a rejected application at the carrier.

PatternProposer (投保人)Life assured (受保人)Typical use
Self= life assuredsame personThe majority of protection cases
SpousalpolicyholderspouseIncome protection bought by the working spouse
ParentparentchildEducation/medical cover for a minor — raises the trustee question
Third-party (employer/association/company)companyemployeeGroup or affinity business, often without an agent at all
Trust / companytrustee or directorindividualEstate planning, buy-sell, key-person

Two consequences for the data model:

  • A policyholder who is not the life assured means "owner" and "insured" are separate axes, and every downstream feature — who receives the no-claim record, who controls who receives the benefit, who may alter the plan — depends on that distinction.
  • A minor as life assured requires a trustee or a specified parent/guardian to receive the death benefit and to exercise the rights. This is on the form as a mandatory field and it is the field agents most often leave blank, because the client does not know it is needed.

The schema

// Prisma schema. Illustrative but shaped the way a real POS shapes it:
// party tables are normalised, product-specific answers live in a versioned
// JSONB column validated against the product version, and every state
// transition is an explicit row rather than a boolean flag.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum ApplicationStatus {
  DRAFT
  PROPOSAL_PREPARED      // 建議書完成
  PROPOSAL_PRESENTED     // client has seen it, has NOT signed
  AWAITING_CLIENT_INPUT  // client is filling the e-application on their device
  SUBMITTED_TO_CARRIER
  UNDER_REVIEW
  REFERRED_TO_UNDERWRITER
  QUOTED_WITH_LOADING_OR_EXCLUSION
  ACCEPTED
  DECLINED
  AWAITING_PAYMENT
  POLICY_ISSUED
  DELIVERY_PENDING
  DELIVERED
  FREE_LOOK_ACTIVE       // Lesson 10: within the free-look window
  WITHDRAWN_IN_FREE_LOOK // cancelled by client inside free-look
  LAPSED_AT_CARRIER
  CANCELLED_BY_CARRIER
}

enum PartyRole {
  PROPOSER
  LIFE_ASSURED
  BENEFICIARY
  TRUSTEE
  PARENT_GUARDIAN
  SETTLER              // 受保人 / 保單持有人 in carrier vocabulary
}

enum IdType {
  HKID
  PASSPORT
  HKID_PROVISIONAL
  MAINLAND_TRAVEL_PERMIT
  BIRTH_CERTIFICATE_HK     // for minors without a travel document
  OTHER_ACCEPTED
}

enum SignatureMethod {
  WET_INK            // 紙本簽名 scanned at 300dpi or photographed
  E_SIGNATURE_ADOPTED_HAND  // signature drawn in client's own hand on a tablet
  E_SIGNATURE_TYPED_WITH_CONSENT // typed name + explicit consent record
  ONE_LEGAL_SIGNATURE_MULTIPLE     // corporate proposer, counter-signed
}

model Party {
  partyId          String     @id @default(cuid())
  role             PartyRole
  partyNumber      Int        // 1..n within an application
  titleZh          String?
  nameZh           String
  nameEn           String
  dateOfBirth      DateTime
  sex              String     // M | F | X  (never assume from name; never infer)
  maritalStatus    String?
  nationality      String?
  occupation       String?
  occupationClass  Int?       // carrier-specific; null when not rated by occupation
  employerName     String?
  employerAddress  String?
  residenceAddress String
  residenceYears   Int?
  hkidNumber       String?    // stored separately + masked (see KYC lesson)
  idType           IdType?
  idNumberMasked   String?
  idExpiry         DateTime?
  email            String?
  mobileMasked     String?
  isMinor          Boolean    @default(false)
  guardianOfLifeAssured  String?  // free text must be avoided — see TrusteeRecord
  createdAt        DateTime   @default(now())
  updatedAt        DateTime   @updatedAt

  applicationId    String
  application      Application @relation(fields: [applicationId], references: [applicationId], onDelete: Cascade)

  @@index([applicationId, role])
  @@index([idNumberMasked])
}

// Trustee / guardian is a first-class record, not a string on the party row.
model TrusteeRecord {
  id                String  @id @default(cuid())
  applicationId     String
  lifeAssuredPartyId String
  trusteeName       String
  trusteeRelation   String  // 父母 / 監護人 / 信託受託人 …
  trusteeIdType     IdType
  trusteeIdNumber   String
  trusteeAddress    String
  acceptAppointments Boolean @default(false)
  documentEvidenceRef String?  // scanned ID held against the KYC record (Lesson 09)
}

model Application {
  applicationId   String   @id @default(cuid())
  reference       String   @unique   // carrier-facing application number once assigned
  status          ApplicationStatus @default(DRAFT)
  channel         String   @default("AGENT_LED")  // AGENT_LED | CLIENT_PORTAL | BRANCH | API

  clientId        String
  agentId         String
  carrierId       String
  quotationNumber String          // the accepted quote from Lesson 07
  productCode     String
  productVersion  String          // MUST match the version the quote was rated on
  bidSheetId      String?

  // Money
  currency           String  @default("HKD")
  annualPremium      Decimal @db.Decimal(12, 2)
  premiumPayingYears Int
  paymentMethod      String?  // FPS | BANK_TRANSFER | CHEQUE | CREDIT_CARD | SALARY_DEDUCTION
  policyCurrencyFxNote String?

  // AML / KYC — Lesson 09 owns the semantics; the POS owns the storage
  sourceOfFunds     String?
  purposeOfTransaction String?
  expectedAnnualPremiumBand String?
  taxResidency     String?      // 稅務居民地
  taxIdNumberMasked String?
  isPepConfirmedNone Boolean  @default(false)
  screeningCaseId   String?

  // Signature + evidence
  signatureMethod   SignatureMethod?
  signatureEvidence Json?      // see SignedEvidence shape in TypeScript below
  clientAttestationId String?

  // Product-specific answers, validated against productVersion at write time
  productAnswers    Json        @default("{}")

  // Money flows
  totalPaid         Decimal @default(0) @db.Decimal(12, 2)
  premiumDueDate    DateTime?

  submittedAt       DateTime?
  carrierAcceptedAt DateTime?
  policyIssuedAt    DateTime?
  policyNumber      String?  @unique
  freeLookEndsAt    DateTime?  // = policyIssuedAt + 21 days (see Lesson 10)

  // Audit
  version           Int      @default(1)   // optimistic concurrency; doubles as audit counter
  updatedBy         String

  parties           Party[]
  transitions       StateTransition[]
  documents         ApplicationDocument[]

  @@index([status, carrierId])
  @@index([clientId, createdAt])
  @@index([policyNumber])
}

model ApplicationDocument {
  id             String  @id @default(cuid())
  applicationId  String
  docType        String  // PROPOSAL | APPLICATION_PDF | ID_FRONT | ID_BACK |
                          // SIGNED_APPLICATION | MEDICAL_REPORT | BANK_PROOF |
                          // OCCUPATION_CERT | PREMIUM_RECEIPT
  storageKey     String  // HK-resident object store; never a public URL
  sha256         String
  mimeType       String
  uploadedBy     String
  uploadedAt     DateTime @default(now())
  carrierAcked   Boolean @default(false)
  application    Application @relation(fields: [applicationId], references: [applicationId], onDelete: Cascade)

  @@index([applicationId, docType])
}

// Every state change is a row. Never a boolean. This table is the answer to
// "what happened to this application and who did it".
model StateTransition {
  id             String   @id @default(cuid())
  applicationId  String
  fromStatus     ApplicationStatus
  toStatus       ApplicationStatus
  trigger        String   // AGENT_ACTION | CLIENT_ACTION | CARRIER_CALLBACK | SYSTEM_JOB | COMPLIANCE
  actorId        String
  actorRole      String   // AGENT | CLIENT | CARRIER_UNDERWRITER | CARRIER_ADMIN | SYSTEM | COMPLIANCE_OFFICER
  reason         String?
  guardResult    Json?    // what the guard checked and what it returned
  occurredAt     DateTime @default(now())
  application    Application @relation(fields: [applicationId], references: [applicationId], onDelete: Cascade)

  @@index([applicationId, occurredAt])
}

The productAnswers problem

productAnswers Json looks like a shortcut and it is the correct design only if a JSON Schema validator is coupled to it. A criticalIllnessProductVersion asks different questions from a savingsProductVersion, and asking a savings client "Have you been diagnosed with any of the following critical illnesses?" is not a minor annoyance — it is an inaccurate disclosure record that the carrier will rely on at claim time.

The coupling rule the POS enforces:

export interface ProductAnswerSchema {
  productCode: string;
  productVersion: string;
  effectiveFrom: string;
  schema: JSONSchema7;            // validates productAnswers
  /** questions whose answer changes underwriting, and therefore need evidence */
  underwritingRelevantFields: string[];
  /** fields the agent may pre-fill from an accepted quotation */
  prefilledFromQuoteFields: string[];
  /** fields that must be re-attested by the client even when prefilled */
  clientAttestationRequired: string[];
}

export function validateProductAnswers(
  schema: ProductAnswerSchema,
  answers: unknown,
  ctx: { productVersion: string },
): { ok: true } | { ok: false; errors: ValidationError[] } {
  // 1. Schema is version-pinned. An answer set validated against v5.0 is
  //    meaningless on v6.0 even if it happens to validate.
  if (schema.productVersion !== ctx.productVersion) {
    return { ok: false, errors: [{
      path: "/",
      message: `schema ${schema.productVersion} does not match policy product version ${ctx.productVersion}; re-render the application`,
    }] };
  }
  // 2. Structural validation.
  const structural = ajvValidate(schema.schema, answers);
  if (!structural.valid) return { ok: false, errors: structural.errors };
  // 3. Semantic validation — the part AJV cannot do.
  const semantic: ValidationError[] = [];
  if (answers.smoker === true && answers.smokingQuitDate === null && schema.requiresQuitDate) {
    semantic.push({ path: "/smokingQuitDate", message: "smoker declared without a quit date" });
  }
  if (answers.heightCm != null && (answers.heightCm < 100 || answers.heightCm > 230)) {
    semantic.push({ path: "/heightCm", message: "height out of plausible range" });
  }
  // 4. Consistency with the rating used in the quote.
  if (answers.smoker !== answers.quotedAsSmoker) {
    semantic.push({
      path: "/smoker",
      message: `disclosure says smoker=${answers.smoker} but the quote was rated as smoker=${answers.quotedAsSmoker}; re-rate required`,
    });
  }
  return semantic.length ? { ok: false, errors: semantic } : { ok: true };
}

Rule 4 is the one that saves agencies. A client is quoted as a non-smoker, then discloses on the application that they smoke 20 a day. The application is now inconsistent with the premium, and the POS must re-rate before submission rather than let the discrepancy travel to the carrier, where it will surface as a post-underwriting premium revision or a cancellation.

Money, ownership and the fields agents skip

FieldWhy the carrier asksWhat happens if it is wrong
Proposer ≠ life assuredDetermines who controls the policy, who receives the benefit on assignmentDeath benefit paid to the wrong person, or held pending a dispute
Minor life assured with no trusteeDeath benefit for a child has no adult to hold or apply for itClaim requires court/ODB involvement; weeks of delay at a time of grief
Source of fundsAML requirement (Lesson 09)Application held for enhanced due diligence; payment can be frozen
Tax residence + TINCRS reporting; a HK policy is reportable to the client's tax jurisdictionClient's accountant cannot complete their return; reputational damage
Payment account in the proposer's own namePrevents third-party payment, which is a classic money-m laundering vectorRefund and clawback; carrier may report under the AMLO
Occupation of the life assured, not the proposerRating and eligibilityWrong premium; decline at claim stage if the occupation was outside the allowed class
Existing-policy declarationMultiple-policy surplus protectionSickness benefits reduced or declined; also an anti-selection issue
Beneficiary relationship and consentWhether the nomination is validNomination ignored; family dispute

E-Application

In practice: in a Hong Kong agency e-application, roughly 80% of the typing is done by the agent on a tablet while the client sits next to them and reads every screen aloud; the client owns the medical disclosure questions because those are theirs to answer truthfully; and the signature is captured on the same device before the packet is submitted.

Who does what

StepWho actsDeviceWhy
Create the application from the accepted quoteAgentAgent tabletThe quote is agent work; it establishes product + version + premium
Confirm party details, contact, occupationAgent, client reading aloudAgent tabletTyping speed; but the client must hear the facts back
Money laundering answers (source of funds, purpose, expected activity)Client, alone if requestedClient device or a private step on the tabletThese are declarations the client makes personally; the agent must not answer them
Health and lifestyle disclosure (健康申報 / 病歷問卷)Client, with the agent available for questions but not answeringClient device, or a step-away step on the tabletNon-solicitation and accuracy: the disclosure is the client's own statement about their body and habits
Occupation and financial justificationAgent, then client confirmsAgent tabletOffice-based occupations are the agent's job; unusual ones need evidence (Lesson 06)
Review and signClientClient device or tabletMust be the client's own act, witnessed by the agent as presenter
Submit to the carrierSystem—Transmission is machine time; the agent's job is to be present

Why the agent does not let the client do it alone

The obvious reason is completion rate: Hong Kong e-application completion rates measured informally in agency practice sit in the 30%–55% range when the client is left alone, against 80%–90% when the agent walks through it. The deeper reasons are compliance:

  1. The agent must demonstrate that the client understood the disclosure. If the client completes a 60-question health questionnaire alone and mis-keys an answer, the agent has no defence when the carrier declines the claim. The agent's presence, the read-back, and the note-taking are the evidence of understanding.
  2. Non-solicitation. If a field agent nudged the answers, the disclosure is tainted and the carrier may decline on the ground of mis-statement.
  3. Agent duties under the Code of Conduct include a duty to take reasonable steps to ensure the client understands the contract. "I sent him a link" is not a reasonable step.

The client-side e-application experience

export type ApplicationStep =
  | "PARTY_DETAILS"
  | "AML_SELF_DECLARATION"
  | "HEALTH_DISCLOSURE"
  | "OCCUPATION_FINANCIALS"
  | "NOMINATION"
  | "REVIEW"
  | "SIGN"
  | "SUBMIT";

export interface StepDefinition {
  step: ApplicationStep;
  titleZh: string;
  whoAnswers: "AGENT_TYPING" | "CLIENT_ALONE" | "CLIENT_WITH_AGENT" | "SYSTEM";
  mandatory: boolean;
  /** A step may not be skipped; it may be *waived* by a state transition with a reason. */
  skippable: false;
  timeoutHours: number;             // client-portal applications expire
  resumeTokenRequired: boolean;
  audit: {
    readBackRequired: boolean;      // agent must read the entered value back to the client
    clientOnlyVisibility: boolean;  // AML + health steps are not visible to the agent UI in client mode
    evidenceCapture?: "SIGNATURE" | "SCREEN_CAPTURE" | "NONE";
  };
}

export const STEP_TABLE: Record<ApplicationStep, StepDefinition> = {
  PARTY_DETAILS: {
    step: "PARTY_DETAILS", titleZh: "當事人資料",
    whoAnswers: "CLIENT_WITH_AGENT", mandatory: true, skippable: false, timeoutHours: 72,
    resumeTokenRequired: true,
    audit: { readBackRequired: true, clientOnlyVisibility: false },
  },
  AML_SELF_DECLARATION: {
    step: "AML_SELF_DECLARATION", titleZh: "打擊洗錢及恐怖分子資金籌集聲明",
    whoAnswers: "CLIENT_ALONE", mandatory: true, skippable: false, timeoutHours: 72,
    resumeTokenRequired: true,
    // The agent never sees these answers typed; the client sees and signs them,
    // and the answers become a record, not a conversation.
    audit: { readBackRequired: false, clientOnlyVisibility: true },
  },
  HEALTH_DISCLOSURE: {
    step: "HEALTH_DISCLOSURE", titleZh: "健康狀況/病歷披露",
    whoAnswers: "CLIENT_ALONE", mandatory: true, skippable: false, timeoutHours: 72,
    resumeTokenRequired: true,
    audit: { readBackRequired: true, clientOnlyVisibility: true },
  },
  OCCUPATION_FINANCIALS: {
    step: "OCCUPATION_FINANCIALS", titleZh: "職業及財務證明",
    whoAnswers: "CLIENT_WITH_AGENT", mandatory: true, skippable: false, timeoutHours: 72,
    resumeTokenRequired: true,
    audit: { readBackRequired: true, clientOnlyVisibility: false },
  },
  NOMINATION: {
    step: "NOMINATION", titleZh: "受益人指定",
    whoAnswers: "CLIENT_WITH_AGENT", mandatory: true, skippable: false, timeoutHours: 72,
    resumeTokenRequired: true,
    audit: { readBackRequired: true, clientOnlyVisibility: false },
  },
  REVIEW: {
    step: "REVIEW", titleZh: "覆核及核對",
    whoAnswers: "CLIENT_WITH_AGENT", mandatory: true, skippable: false, timeoutHours: 24,
    resumeTokenRequired: false,
    audit: { readBackRequired: true, clientOnlyVisibility: false },
  },
  SIGN: {
    step: "SIGN", titleZh: "簽署",
    whoAnswers: "CLIENT_ALONE", mandatory: true, skippable: false, timeoutHours: 24,
    resumeTokenRequired: false,
    audit: { readBackRequired: false, clientOnlyVisibility: false, evidenceCapture: "SIGNATURE" },
  },
  SUBMIT: {
    step: "SUBMIT", titleZh: "遞交申請",
    whoAnswers: "SYSTEM", mandatory: true, skippable: false, timeoutHours: 0,
    resumeTokenRequired: false,
    audit: { readBackRequired: false, clientOnlyVisibility: false },
  },
};

The clientOnlyVisibility flag on the AML and health steps is the design detail that matters. In client-portal mode the agent's console shows "Step completed at 15:41" and nothing else. That is not obfuscation — it is the control that makes the client's declaration the client's, and it is the answer to an investigator who asks how the agent avoided non-solicitation.

Resumption, expiry and the abandoned application

Applications die. In agency practice a meaningful share of e-applications are never completed — the client said "send me the link", the link sits in a chat, the price changes, the client's mood changes, a competing agent calls. The POS treats this as a first-class state, not a failure:

export function handleResume(input: {
  applicationId: string;
  action: "OPEN" | "RESUME" | "EXPIRE" | "ABANDON";
}): ResumeOutcome {
  const app = loadApplication(input.applicationId);
  const elapsedHours = hoursSince(app.updatedAt);

  if (input.action === "OPEN" && app.status === "DRAFT") {
    return { step: nextMandatoryStep(app), resumeToken: mintResumeToken(app.applicationId) };
  }

  if (input.action === "RESUME") {
    // 1. The quote is a promise with an expiry date. Reopening past expiry
    //    does not silently re-rate: it forces an explicit decision.
    if (isQuoteExpired(app.quotationNumber)) {
      return {
        blocked: true,
        reason: "QUOTATION_EXPIRED",
        action: "RE_QUOTE_OR_SEEK_CLIENT_DECISION",
        // The client's own quotes cannot be extended by an agent unilaterally.
        permittedActions: ["RE_QUOTE", "PROPOSE_ALTERNATIVE", "CLOSE_CLIENT_DECLINED"],
      };
    }
    // 2. If the client profile changed, re-rate before continuing.
    const drift = detectProfileDrift(app);
    if (drift.length > 0) {
      return { blocked: true, reason: "PROFILE_DRIFT", drift };
    }
    return { step: nextMandatoryStep(app), resumeToken: mintResumeToken(app.applicationId) };
  }

  if (input.action === "EXPIRE") {
    if (elapsedHours < 72) throw new TooEarlyToExpireError(elapsedHours);
    closeAsAbandoned(app, reason: "NO_CLIENT_ACTIVITY_72H");
    // Abandonment is NOT rejection. It becomes a CRM follow-up task.
    scheduleFollowUp(app, { days: 7, channel: "WHATSAPP", template: "ABANDONED_APPLICATION" });
    scheduleFollowUp(app, { days: 30, channel: "EMAIL", template: "APPLICATION_STILL_AVAILABLE_IF_YOU_WISH" });
    return { status: "ABANDONED", followUpsArmed: 2 };
  }

  // Client explicitly abandoned
  closeAsAbandoned(app, reason: "CLIENT_ABANDONED");
  armNoContactTimer(app.clientId, 180);   // three months; shorter than a decline, longer than a defer
  return { status: "ABANDONED" };
}

Wet Signature vs Digital Signature

In practice: before the client signs, the POS shows one banner with two routes — sign digitally now or print, sign wet-ink, and have the agent scan it — and the carrier's product configuration decides which route is offered, because some carriers will not accept an e-signature for a particular product or a particular premium size.

The distinction that is actually about evidence

In Hong Kong practice, the legal framework for electronic transactions is the Electronic Transactions Ordinance (電子交易條例, Cap. 553), which gives legal recognition to electronic records and electronic signatures, subject to consent and to the reliability and evidentiary requirement in sections 5–7: a signature is treated as a legally valid signature if it can be shown that the signatory intended to sign and that the signature is reliable and as capable as a wet-ink signature of representing who signed. The Personal Data (Privacy) Ordinance (Cap. 486) governs the record. And the Insurance Authority's expectations come through the Best Practice Guide on ... series and the carriers' own operational rules.

The practical differences an agent cares about:

AspectWet signature (紙本簽署)Digital signature (電子簽署)
Physical artifactSigned paper, scanned at 300 dpi minimum, stored as PDF/A with hashNo paper; signature vector data + certificate chain
Evidence trailScan, custody chain from the physical original, courier recordsCryptographic timestamp, signer identity verification method, IP/device, immutable log
SpeedHours to days (print, sign, scan, upload, courier if required)Seconds
WitnessingUsually requires the agent present and countersignedAgent present and recorded as presenter in the session log
Legibility for disputesHigh; an original can be producedDepends entirely on the evidence stored; a poorly implemented e-sign is worse than wet
Common acceptanceUniversally acceptedAccepted by essentially all HK carriers for agency business, but product-level exceptions exist
Failure modesSignature on the wrong page, missing pages, illegible scan, wrong version of the formSignature captured but not bound to the document hash; identity not verified; session replay

Binding the signature to the document

The single most important engineering property is that the signature must be bound to a hash of the exact document the client read. A signature captured on a document that later changes — a corrected occupation, an altered sum insured, an added rider — is worthless and, worse, dangerous, because it looks valid.

export interface SignedEvidence {
  applicationId: string;
  method: "WET_INK" | "E_SIGNATURE_ADOPTED_HAND" | "E_SIGNATURE_TYPED_WITH_CONSENT" | "ONE_LEGAL_SIGNATURE_MULTIPLE";
  documentSha256: string;               // hash of the EXACT PDF the client saw
  documentVersion: number;               // increments on every content change
  signedAt: string;                      // ISO-8601 with offset, HKT
  signatory: {
    partyId: string;
    nameAsSigned: string;
    identityVerification: {
      method: "HKID_OTP" | "DOC_MATCH_TO_KYC" | "AGENT_WITNESSED_IN_PERSON" | "HKID_CARD_READER";
      detail: string;
      confidence?: number;               // OCR/authenticity score where a vendor is used
    };
  };
  presenter?: {
    agentId: string;
    role: "PRESENTER" | "COUNTERSIGNER";
    presentAtSigning: true;              // must be true; the POS enforces it
    attestedAt: string;
  };
  wetInk?: {
    scannedAt: string;
    scanDpi: number;                     // minimum 300
    originalCustody: "AGENT_RETAINED" | "CLIENT_RETAINED" | "COURIER_TO_CARRIER" | "SCANNED_AND_DESTROYED_PER_CLIENT_INSTRUCTION";
    courierRef?: string;
    pagesExpected: number;
    pagesReceived: number;               // these two MUST match
  };
  eSign?: {
    signatureVectorSha256: string;
    certificateSubject: string;
    certificateIssuer: string;
    trustedTimestamp: string;
    sessionId: string;
    ipOrDevice: string;
    documentHashAtSigning: string;       // MUST equal documentSha256 above
    hashMatchVerified: true;
  };
  tamperSeal: string;                    // seals (documentSha256 + all fields above)
}

export function verifySignature(e: SignedEvidence, currentDocHash: string): VerificationResult {
  const problems: string[] = [];

  if (e.documentSha256 !== currentDocHash) {
    problems.push(
      "DOCUMENT_CHANGED_AFTER_SIGNING — the signed document hash does not match the current " +
      "application document. A signature is only evidence of consent to the exact text signed.",
    );
  }
  if (e.method === "WET_INK" && e.wetInk) {
    if (e.wetInk.pagesExpected !== e.wetInk.pagesReceived) {
      problems.push(`PAGE_COUNT_MISMATCH: expected ${e.wetInk.pagesExpected}, got ${e.wetInk.pagesReceived}`);
    }
    if (e.wetInk.scanDpi < 300) {
      problems.push(`SCAN_DPI_TOO_LOW: ${e.wetInk.scanDpi} < 300`);
    }
  }
  if (e.method === "E_SIGNATURE_ADOPTED_HAND" && e.eSign) {
    if (e.eSign.documentHashAtSigning !== e.documentSha256) {
      problems.push("SIGNATURE_NOT_BOUND_TO_DOCUMENT_HASH");
    }
    if (e.eSign.hashMatchVerified !== true) {
      problems.push("HASH_MATCH_NOT_VERIFIED_AT_CAPTURE");
    }
  }
  if (e.presenter?.presentAtSigning !== true) {
    problems.push("AGENT_NOT_RECORDED_AS_PRESENT — many carrier configurations require a witnessed signature");
  }

  return { valid: problems.length === 0, problems, tamperDetected: problems.some((p) => p.startsWith("DOCUMENT_CHANGED")) };
}

The practical rules for an agent

SituationRoute
Client present, tablet available, product allows e-signDigital, agent-witnessed, instant
Client present, no tablet or the client will not use oneWet ink, agent scans at 300 dpi on the spot, keeps the original
Client in a mainland city or abroad, wants to apply remotelyDigital via the client portal, with the agent present by video call — and note that some carriers require a wet-ink original to be couriered regardless
Corporate proposer (受保法團)ONE_LEGAL_SIGNATURE_MULTIPLE — director signs for the company; the company's chop plus the director's signature; both evidenced
Premium above a carrier's thresholdCheck the carrier rule; several carriers require a wet-ink original or a compliance call above a threshold
Any case where the client seems to not understand the documentStop. Do not collect the signature. This is the moment the Code of Conduct matters.

The last row is the one to remember. A signature taken from a client who did not understand what they were signing converts a good-faith sales conversation into a mis-sale, and the signature becomes the carrier's primary defence.

The Proposal → Application → Policy State Machine

In practice: the agent watches the application in a single pipeline view; every card shows its current state, the age in that state, who is blocking it, and the one button that moves it. Nothing moves by itself except by a recorded trigger.

stateDiagram-v2
  direction TB
  [*] --> DRAFT : agent creates from accepted quote (Lesson 07)

  DRAFT --> PROPOSAL_PREPARED : agent completes proposal incl. benefit schedule
  PROPOSAL_PREPARED --> PROPOSAL_PRESENTED : agent shows proposal to client, client views
  PROPOSAL_PRESENTED --> AWAITING_CLIENT_INPUT : client opens e-application link
  PROPOSAL_PRESENTED --> WITHDRAWN_IN_FREE_LOOK : client declines after seeing the proposal

  AWAITING_CLIENT_INPUT --> SUBMITTED_TO_CARRIER : all steps signed incl. signature bound to doc hash
  AWAITING_CLIENT_INPUT --> AWAITING_CLIENT_INPUT : resume (rate still valid)
  AWAITING_CLIENT_INPUT --> [*] : 72h no activity ⇒ ABANDONED (CRM follow-up)

  SUBMITTED_TO_CARRIER --> UNDER_REVIEW : carrier acknowledges receipt
  SUBMITTED_TO_CARRIER --> WITHDRAWN_IN_FREE_LOOK : client withdraws before carrier review

  UNDER_REVIEW --> REFERRED_TO_UNDERWRITER : disclosure requires evidence
  UNDER_REVIEW --> QUOTED_WITH_LOADING_OR_EXCLUSION : automated decision w/ PES
  UNDER_REVIEW --> ACCEPTED : standard decision
  UNDER_REVIEW --> DECLINED : adverse decision
  UNDER_REVIEW --> AWAITING_PAYMENT : accepted subject to first premium

  REFERRED_TO_UNDERWRITER --> QUOTED_WITH_LOADING_OR_EXCLUSION : loading, PES, or exclusion added
  REFERRED_TO_UNDERWRITER --> ACCEPTED : accepted after review
  REFERRED_TO_UNDERWRITER --> DECLINED : declined after review

  QUOTED_WITH_LOADING_OR_EXCLUSION --> AWAITING_PAYMENT : client accepts revised terms (fresh signature required)
  QUOTED_WITH_LOADING_OR_EXCLUSION --> DECLINED : client declines revised terms
  AWAITING_PAYMENT --> AWAITING_PAYMENT : payment failed (max 3 retries over 14 days)
  AWAITING_PAYMENT --> POLICY_ISSUED : first premium received & cleared

  POLICY_ISSUED --> DELIVERY_PENDING : policy documents generated
  DELIVERY_PENDING --> DELIVERED : client received + policy in force
  DELIVERED --> FREE_LOOK_ACTIVE : free-look clock starts at DELIVERED
  FREE_LOOK_ACTIVE --> [*] : 21 days elapse ⇒ in force
  FREE_LOOK_ACTIVE --> WITHDRAWN_IN_FREE_LOOK : client cancels within free look
  FREE_LOOK_ACTIVE --> POLICY_ISSUED : carrier reverts to in-force issue (document re-delivery)

  ACCEPTED --> [*] : terminal without issue (client abandons after acceptance)
  DECLINED --> [*]
  CANCELLED_BY_CARRIER --> [*] : e.g. material mis-statement, AML failure, unpaid premium

Transition table

Every transition below has a guard. The guard is the row in StateTransition.guardResult and it is what compliance reads first.

#FromToTriggerGuard (must be true)ActorReversible?Approval
T1DRAFTPROPOSAL_PREPAREDagentProposal complete; benefit schedule generated from the pinned product version; non-guaranteed illustrations present with assumed return statedagentyes (edit)agent
T2PROPOSAL_PREPAREDPROPOSAL_PRESENTEDagentClient present; view event logged; client acknowledged basis statementagentyesagent
T3PROPOSAL_PRESENTEDAWAITING_CLIENT_INPUTclientResume token issued; KYC step (Lesson 09) not yet failedclientn/a—
T4AWAITING_CLIENT_INPUTSUBMITTED_TO_CARRIERclient + systemAll mandatory steps complete; signature hash matches document hash; no open blocking validation errors; productAnswers validated against the exact product version; quote still liveclient, systemnoagent counter-signs
T5SUBMITTED_TO_CARRIERUNDER_REVIEWcarrierCarrier acknowledgement received with reference numbersystemn/a—
T6UNDER_REVIEWREFERRED_TO_UNDERWRITERcarrierA disclosure answer crossed the carrier's evidence threshold (Lesson 06)systemnounderwriter
T7UNDER_REVIEWQUOTED_WITH_LOADING_OR_EXCLUSIONcarrierLoading % or PES wording non-null; revised premium computedsystemnounderwriter
T8UNDER_REVIEWACCEPTEDcarrierAll disclosures answered; no exclusion; premium matches the quote within tolerance (0%)systemnounderwriter
T9UNDER_REVIEWDECLINEDcarrierAdverse decision reason recorded; decline letter generatedsystemnounderwriter
T10QUOTED_WITH_LOADING_OR_EXCLUSIONAWAITING_PAYMENTclientFresh signature on the revised terms. An old signature does not survive a changed premium or a new exclusionclientnoagent
T11AWAITING_PAYMENTPOLICY_ISSUEDsystemFirst premium received into the correct account and cleared (FPS T+0, bank T+1–2); amount equals the accepted premium to the centsystemno—
T12POLICY_ISSUEDDELIVERY_PENDINGsystemPolicy documents rendered; policy number assigned; in-force date setsystemno—
T13DELIVERY_PENDINGDELIVEREDclientDelivery acknowledged (email receipt, or agent-confirmed hand delivery)client/agentno—
T14DELIVEREDFREE_LOOK_ACTIVEsystemFree-look end date = delivery + 21 days, stored on the applicationsystemn/a—
T15FREE_LOOK_ACTIVEWITHDRAWN_IN_FREE_LOOKclientWithin 21 days of delivery; surrender value = full premium paid, less any stated non-refundable amountclientnoagent
T16anyCANCELLED_BY_CARRIERcarrier/complianceMaterial mis-statement, AML failure, or premium not paid after 3 retries over 14 dayssystemnocompliance officer

The transitions that catch people out

T4's quote-liveness guard. Between the bid sheet and submission, hours or days pass. If the quotation expired, the submitted premium is a premium nobody agreed to. The POS blocks T4 and forces either a re-quote (Lesson 07, R-QTE-01) or a documented reason for proceeding.

T10's re-signature requirement. This is the most valuable guard in the whole machine. When a case comes back with a 50% loading or a pre-existing condition exclusion, the client's signed application is evidence of consent to different terms. Requiring a fresh signature on the revised wording is what prevents the "I never knew about the exclusion" complaint — and the corresponding claim decline is what makes it expensive.

T11's amount check. The first premium must equal the accepted premium exactly. If a carrier's revised premium differs by even HK$0.01 from what the client signed, T11 fails, because a policy issued for a different amount than the signed application is not the policy the client bought.

T15's asymmetry. Inside free-look the client can cancel and receive the premiums paid (subject to the stated deductions). Outside free-look the same client faces surrender value, which is typically far less. That asymmetry is why free-look is a Lesson 10 topic and why the agent must tell the client the free-look date and its meaning at the moment of delivery, not three years later.

The full pipeline, as an agent sees it

MY PIPELINE — agent AG-2210 (Wong Ka Ho)         2026-03-11 16:20 HKT

BLOCKED ON CLIENT (3)
  APP-2026-04471  陳小明 / 陳美玲   SUBMITTED_TO_CARRIER   1d 4h   carrier ack pending
  APP-2026-04502  李嘉欣            AWAITING_CLIENT_INPUT  6d 2h   ⚠ 72h expiry in 11h
  APP-2026-04488  何國榮            AWAITING_PAYMENT       2d 0h   payment link sent, 1 retry left

BLOCKED ON CARRIER (2)
  APP-2026-04403  張家豪            REFERRED_TO_UNDERWRITER 4d 1h  ⚠ SLA 5d
  APP-2026-04419  郭詠恩            UNDER_REVIEW           1d 2h

ACTION REQUIRED FROM ME (2)
  APP-2026-04455  黃美玲            QUOTED_WITH_LOADING_OR_EXCLUSION 2h
       ⚠ client must re-sign revised terms (50% loading + diabetes PES)
       [ Send revised terms ]   [ Call client ]
  APP-2026-04461  劉德明            FREE_LOOK_ACTIVE       ends 2026-03-28
       ⚠ remind client of free-look date (Lesson 10)

ISSUED THIS MONTH: 7   ·  ACCEPTANCE RATE 62%   ·  MEDIAN SUBMIT→ISSUE 9 days

An SLA clock on REFERRED_TO_UNDERWRITER matters more than it looks. Underwriting referrals are where cases die: a client who was told "two weeks" and hears nothing for five weeks has usually already bought somewhere else, and the lost case is usually blamed on the competitor. The pipeline view makes the ageing visible so the agent can chase with a specific question rather than a passive follow-up.

Validation and Error Handling

In practice: the POS shows three kinds of error, distinguished by who can fix them — your error (the agent, now), their error (the client or the carrier), and waiting. Everything that is not immediately fixable becomes a task with an owner, never a red banner that sits on a screen.

The validation ladder

export interface ValidationIssue {
  layer: "FIELD" | "CROSS_FIELD" | "PRODUCT_VERSION" | "CONSISTENCY" | "COMPLIANCE" | "CARRIER_RULE";
  code: string;
  path: string;                       // JSON-pointer-ish, e.g. "/parties/1/hkidNumber"
  message: string;                    // agent-facing
  messageZh: string;                  // client-facing when the client can fix it
  owner: "AGENT" | "CLIENT" | "CARRIER" | "SYSTEM";
  blocking: boolean;
  hint?: string;
}

export type ValidationOutcome =
  | { ok: true }
  | { ok: false; issues: ValidationIssue[]; blocking: ValidationIssue[] };

/**
 * Runs the whole ladder. Order matters: cheap structural checks first,
 * then expensive ones, and COMPLIANCE last because it may need external calls.
 */
export async function validateApplication(
  app: Application,
  deps: { kyc: KycVerifier; carrier: CarrierRuleSet; product: ProductAnswerSchema },
): Promise<ValidationOutcome> {
  const issues: ValidationIssue[] = [];

  // FIELD — required, format, length
  issues.push(...validateFieldLayer(app));

  // CROSS_FIELD — things that are individually fine but jointly impossible
  const cross = validateCrossFieldLayer(app);
  issues.push(...cross);
  // e.g. a life assured aged under 18 with no TrusteeRecord;
  // e.g. a proposer address in Macau with a Hong Kong ID card;
  // e.g. occupation "retired" with employment start date next year;
  // e.g. a beneficiary who is also the life assured on a term plan (usually invalid).

  // PRODUCT_VERSION — answers must validate against the pinned version
  const pv = validateProductAnswers(deps.product, app.productAnswers, {
    productVersion: app.productVersion,
  });
  if (!pv.ok) {
    for (const e of pv.errors) {
      issues.push({
        layer: "PRODUCT_VERSION", code: "PRODUCT_SCHEMA_MISMATCH", path: e.path,
        message: `Answer set does not match product ${deps.product.productCode} ${deps.product.productVersion}`,
        messageZh: "你填寫的答案同目前產品版本的欄位不一致,請重新開啟申請表填寫。",
        owner: "SYSTEM", blocking: true,
        hint: "The product version changed since the application was created. Migrate or restart.",
      });
    }
  }

  // CONSISTENCY — the rating used to quote vs what was disclosed
  const consistency = detectProfileDrift(app);
  issues.push(...consistency.map((d) => ({
    layer: "CONSISTENCY" as const, code: `DRIFT_${d.field.toUpperCase()}`, path: d.path,
    message: `Disclosed ${d.field}=${d.disclosed} but the quotation used ${d.field}=${d.quoted}. Re-rating required.`,
    messageZh: `你申報的${d.label}同報價時使用的資料不同,需要重新計算保費。`,
    owner: "AGENT" as const, blocking: true,
    hint: "Re-run the quote, then have the client re-sign if the premium changes.",
  })));

  // COMPLIANCE — Lesson 09 decides these; the POS only stores and enforces
  const kyc = await deps.kyc.verifyForApplication(app);
  if (kyc.outcome === "FAIL") {
    issues.push({
      layer: "COMPLIANCE", code: `KYC_${kyc.reasonCode}`, path: "/kyc",
      message: kyc.message,
      messageZh: "身份核實未通過,請提供其他身分證明文件或聯絡我們協助。",
      owner: "CLIENT", blocking: true,
    });
  }
  if (kyc.outcome === "ESCALATE") {
    issues.push({
      layer: "COMPLIANCE", code: "KYC_FOUR_EYES", path: "/kyc",
      message: "Case is referred to a compliance officer for four-eyes review; submission is held.",
      messageZh: "個案需要由合規主任覆核,我們會盡快聯絡你。",
      owner: "SYSTEM", blocking: true,
    });
  }

  // CARRIER_RULE — the carrier's own product configuration, which changes
  const rules = deps.carrier.rulesFor(app.carrierId, app.productCode, app.productVersion);
  for (const r of rules) {
    const pass = r.test(stripIssues(app));
    if (!pass) {
      issues.push({
        layer: "CARRIER_RULE", code: r.code, path: r.path,
        message: r.message,
        messageZh: r.messageZh ?? r.message,
        owner: r.owner,
        blocking: r.blocking,
      });
    }
  }

  const blocking = issues.filter((i) => i.blocking);
  return blocking.length ? { ok: false, issues, blocking } : { ok: true };
}

Error taxonomy an agent will actually meet

CodeLayerOwnerTypical triggerWhat the agent does
PARTY_MINOR_NO_TRUSTEECROSS_FIELDagentlife assured under 18Stop. Get a trustee; this cannot be post-completed
DRIFT_SMOKERCONSISTENCYagentquoted non-smoker, disclosed smokerRe-rate, re-sign, restart the case with the correct premium
BENEFICIARY_IS_ASSUREDCROSS_FIELDagentnomination names the life assuredChange to an estate/trust arrangement or nominate a third party
HKID_CHECKSUM_FAILFIELDclientmistyped HKIDClient re-enters; the POS must not attempt to auto-correct a checksum failure
OCCUPATION_NOT_IN_CLASSCARRIER_RULEagentoccupation outside the plan's classesReclassify to the nearest eligible class, or move product; never invent a class
PREMIUM_MISMATCHCONSISTENCYsystemfirst premium ≠ accepted premiumCarrier finance must reconcile before issue; do not re-sign a client to fix a carrier's accounting error
KYC_ADDRESS_UNVERIFIEDCOMPLIANCEclientaddress proof missing or >3 months oldRequest a bank statement or utility bill (Lesson 09)
PRODUCT_WITHDRAWNPRODUCT_VERSIONsystemcarrier withdrew the version mid-applicationRe-quote on the successor version; the old quote is void
QUOTATION_EXPIREDCONSISTENCYagentsubmission attempted after quote expiryRe-quote (Lesson 07) or document the reason for proceeding
PREMIUM_MODE_UNSUPPORTEDCARRIER_RULEagente.g. 25-year pay on a plan that stops at 20Offer a supported mode; never approximate

The HKID point deserves a specific warning. The Hong Kong Identity Card number has a check-digit, and the POS validates it — but the correct response to a checksum failure is to ask the client to re-read the number from the card, not to attempt a correction algorithm that "fixes" a typo into a valid but wrong number. Fixing it produces an identity mismatch at eKYC time, which is worse than the original typo.

The resubmission trap

When a case is returned by the carrier for correction, the naive implementation re-opens the application in place. That destroys the evidence. The correct pattern is amend-and-version: the original signed application is preserved byte-for-byte with its hash; the correction creates a new documentVersion, a new signature, and a new row in StateTransition; and the carrier sees an amendment letter stating exactly what changed and why. Cases that are amended without re-signing are the cases that fail at claim stage.

Key Takeaways

  1. The application data model has four parties, not one. Proposer, life assured, beneficiary, and — for a minor life assured — a trustee or parent guardian who must be a first-class record with its own ID evidence, not a free-text field.
  2. productAnswers must be validated against a version-pinned JSON Schema. A savings client is not asked critical-illness questions. If the product version changes mid-application, the application must be re-rendered, not re-validated.
  3. Prefilled data from a quote is a hypothesis; the client's disclosure is a statement. Any field that changes the rating (smoker, height, weight, occupation) must be re-attested, and a mismatch forces a re-rate before submission.
  4. The agent types; the client declares. Money-laundering and health disclosure are the client's own declarations and are entered in a step the agent's console cannot read. The client reads back the party details; the agent reads back the disclosure answers.
  5. A signature is only evidence of consent to the exact text signed. Store the document hash, bind the signature to it, verify the match, and treat any post-signature change as invalidating — which is why a re-quoted premium requires a re-signature.
  6. Wet ink versus digital is an evidence-quality decision, not a convenience decision. Both are accepted in HK practice; the wet-ink route must capture 300 dpi, a page count that matches, and a custody record; the digital route must capture a certificate chain, a trusted timestamp, and a verified hash match.
  7. A witnessed signature matters. The POS records the agent as presenter at signing because the agent's duty is to ensure the client understood the contract — "I sent him a link" is not a reasonable step, and a signature taken from a confused client converts good faith into mis-sale.
  8. Every state transition is a row with a guard. DRAFT → PROPOSAL_PRESENTED → AWAITING_CLIENT_INPUT → SUBMITTED_TO_CARRIER → UNDER_REVIEW → (REFERRED | QUOTED_WITH_LOADING_OR_EXCLUSION | ACCEPTED | DECLINED) → AWAITING_PAYMENT → POLICY_ISSUED → DELIVERY_PENDING → DELIVERED → FREE_LOOK_ACTIVE, with DECLINED, WITHDRAWN_IN_FREE_LOOK and CANCELLED_BY_CARRIER as terminal branches.
  9. Three guards catch the most damage: the quote must still be live at submission, a loaded or excluded case needs a fresh signature on the revised terms, and the first premium must match the accepted premium exactly. Amendments are versioned, never edited in place.

課堂測驗 · 8 題

Question 1 of 8Answered 0 / 8
Question 1 of 8

一位客戶想幫 12 歲嘅兒子投保。申請表入面應該出現幾多個獨立嘅主體記錄?

Pick an answer to lock it in. We'll tell you immediately whether you got it right and show an explanation. Then press Enter or click Next to continue.

Shortcuts:ABCDpick answer on current questionEntergo to next unanswered
8 unanswered