學習目標 · 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.
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.
| Pattern | Proposer (投保人) | Life assured (受保人) | Typical use |
|---|---|---|---|
| Self | = life assured | same person | The majority of protection cases |
| Spousal | policyholder | spouse | Income protection bought by the working spouse |
| Parent | parent | child | Education/medical cover for a minor — raises the trustee question |
| Third-party (employer/association/company) | company | employee | Group or affinity business, often without an agent at all |
| Trust / company | trustee or director | individual | Estate 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
| Field | Why the carrier asks | What happens if it is wrong |
|---|---|---|
| Proposer ≠ life assured | Determines who controls the policy, who receives the benefit on assignment | Death benefit paid to the wrong person, or held pending a dispute |
| Minor life assured with no trustee | Death benefit for a child has no adult to hold or apply for it | Claim requires court/ODB involvement; weeks of delay at a time of grief |
| Source of funds | AML requirement (Lesson 09) | Application held for enhanced due diligence; payment can be frozen |
| Tax residence + TIN | CRS reporting; a HK policy is reportable to the client's tax jurisdiction | Client's accountant cannot complete their return; reputational damage |
| Payment account in the proposer's own name | Prevents third-party payment, which is a classic money-m laundering vector | Refund and clawback; carrier may report under the AMLO |
| Occupation of the life assured, not the proposer | Rating and eligibility | Wrong premium; decline at claim stage if the occupation was outside the allowed class |
| Existing-policy declaration | Multiple-policy surplus protection | Sickness benefits reduced or declined; also an anti-selection issue |
| Beneficiary relationship and consent | Whether the nomination is valid | Nomination 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
| Step | Who acts | Device | Why |
|---|---|---|---|
| Create the application from the accepted quote | Agent | Agent tablet | The quote is agent work; it establishes product + version + premium |
| Confirm party details, contact, occupation | Agent, client reading aloud | Agent tablet | Typing speed; but the client must hear the facts back |
| Money laundering answers (source of funds, purpose, expected activity) | Client, alone if requested | Client device or a private step on the tablet | These 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 answering | Client device, or a step-away step on the tablet | Non-solicitation and accuracy: the disclosure is the client's own statement about their body and habits |
| Occupation and financial justification | Agent, then client confirms | Agent tablet | Office-based occupations are the agent's job; unusual ones need evidence (Lesson 06) |
| Review and sign | Client | Client device or tablet | Must be the client's own act, witnessed by the agent as presenter |
| Submit to the carrier | System | — | 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:
- 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.
- Non-solicitation. If a field agent nudged the answers, the disclosure is tainted and the carrier may decline on the ground of mis-statement.
- 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:
| Aspect | Wet signature (紙本簽署) | Digital signature (電子簽署) |
|---|---|---|
| Physical artifact | Signed paper, scanned at 300 dpi minimum, stored as PDF/A with hash | No paper; signature vector data + certificate chain |
| Evidence trail | Scan, custody chain from the physical original, courier records | Cryptographic timestamp, signer identity verification method, IP/device, immutable log |
| Speed | Hours to days (print, sign, scan, upload, courier if required) | Seconds |
| Witnessing | Usually requires the agent present and countersigned | Agent present and recorded as presenter in the session log |
| Legibility for disputes | High; an original can be produced | Depends entirely on the evidence stored; a poorly implemented e-sign is worse than wet |
| Common acceptance | Universally accepted | Accepted by essentially all HK carriers for agency business, but product-level exceptions exist |
| Failure modes | Signature on the wrong page, missing pages, illegible scan, wrong version of the form | Signature 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
| Situation | Route |
|---|---|
| Client present, tablet available, product allows e-sign | Digital, agent-witnessed, instant |
| Client present, no tablet or the client will not use one | Wet ink, agent scans at 300 dpi on the spot, keeps the original |
| Client in a mainland city or abroad, wants to apply remotely | Digital 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 threshold | Check 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 document | Stop. 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.
| # | From | To | Trigger | Guard (must be true) | Actor | Reversible? | Approval |
|---|---|---|---|---|---|---|---|
| T1 | DRAFT | PROPOSAL_PREPARED | agent | Proposal complete; benefit schedule generated from the pinned product version; non-guaranteed illustrations present with assumed return stated | agent | yes (edit) | agent |
| T2 | PROPOSAL_PREPARED | PROPOSAL_PRESENTED | agent | Client present; view event logged; client acknowledged basis statement | agent | yes | agent |
| T3 | PROPOSAL_PRESENTED | AWAITING_CLIENT_INPUT | client | Resume token issued; KYC step (Lesson 09) not yet failed | client | n/a | — |
| T4 | AWAITING_CLIENT_INPUT | SUBMITTED_TO_CARRIER | client + system | All mandatory steps complete; signature hash matches document hash; no open blocking validation errors; productAnswers validated against the exact product version; quote still live | client, system | no | agent counter-signs |
| T5 | SUBMITTED_TO_CARRIER | UNDER_REVIEW | carrier | Carrier acknowledgement received with reference number | system | n/a | — |
| T6 | UNDER_REVIEW | REFERRED_TO_UNDERWRITER | carrier | A disclosure answer crossed the carrier's evidence threshold (Lesson 06) | system | no | underwriter |
| T7 | UNDER_REVIEW | QUOTED_WITH_LOADING_OR_EXCLUSION | carrier | Loading % or PES wording non-null; revised premium computed | system | no | underwriter |
| T8 | UNDER_REVIEW | ACCEPTED | carrier | All disclosures answered; no exclusion; premium matches the quote within tolerance (0%) | system | no | underwriter |
| T9 | UNDER_REVIEW | DECLINED | carrier | Adverse decision reason recorded; decline letter generated | system | no | underwriter |
| T10 | QUOTED_WITH_LOADING_OR_EXCLUSION | AWAITING_PAYMENT | client | Fresh signature on the revised terms. An old signature does not survive a changed premium or a new exclusion | client | no | agent |
| T11 | AWAITING_PAYMENT | POLICY_ISSUED | system | First premium received into the correct account and cleared (FPS T+0, bank T+1–2); amount equals the accepted premium to the cent | system | no | — |
| T12 | POLICY_ISSUED | DELIVERY_PENDING | system | Policy documents rendered; policy number assigned; in-force date set | system | no | — |
| T13 | DELIVERY_PENDING | DELIVERED | client | Delivery acknowledged (email receipt, or agent-confirmed hand delivery) | client/agent | no | — |
| T14 | DELIVERED | FREE_LOOK_ACTIVE | system | Free-look end date = delivery + 21 days, stored on the application | system | n/a | — |
| T15 | FREE_LOOK_ACTIVE | WITHDRAWN_IN_FREE_LOOK | client | Within 21 days of delivery; surrender value = full premium paid, less any stated non-refundable amount | client | no | agent |
| T16 | any | CANCELLED_BY_CARRIER | carrier/compliance | Material mis-statement, AML failure, or premium not paid after 3 retries over 14 days | system | no | compliance 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
| Code | Layer | Owner | Typical trigger | What the agent does |
|---|---|---|---|---|
PARTY_MINOR_NO_TRUSTEE | CROSS_FIELD | agent | life assured under 18 | Stop. Get a trustee; this cannot be post-completed |
DRIFT_SMOKER | CONSISTENCY | agent | quoted non-smoker, disclosed smoker | Re-rate, re-sign, restart the case with the correct premium |
BENEFICIARY_IS_ASSURED | CROSS_FIELD | agent | nomination names the life assured | Change to an estate/trust arrangement or nominate a third party |
HKID_CHECKSUM_FAIL | FIELD | client | mistyped HKID | Client re-enters; the POS must not attempt to auto-correct a checksum failure |
OCCUPATION_NOT_IN_CLASS | CARRIER_RULE | agent | occupation outside the plan's classes | Reclassify to the nearest eligible class, or move product; never invent a class |
PREMIUM_MISMATCH | CONSISTENCY | system | first premium ≠ accepted premium | Carrier finance must reconcile before issue; do not re-sign a client to fix a carrier's accounting error |
KYC_ADDRESS_UNVERIFIED | COMPLIANCE | client | address proof missing or >3 months old | Request a bank statement or utility bill (Lesson 09) |
PRODUCT_WITHDRAWN | PRODUCT_VERSION | system | carrier withdrew the version mid-application | Re-quote on the successor version; the old quote is void |
QUOTATION_EXPIRED | CONSISTENCY | agent | submission attempted after quote expiry | Re-quote (Lesson 07) or document the reason for proceeding |
PREMIUM_MODE_UNSUPPORTED | CARRIER_RULE | agent | e.g. 25-year pay on a plan that stops at 20 | Offer 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
- 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.
productAnswersmust 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.- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.