學習目標 · Learning Objectives
- Classify any Hong Kong insurance product into one of eight families — life, medical, critical illness, savings, ILAS, accident, home, car — and understand why that classification drives which POS modules, disclosures and regulators become active for a case.
- Read a product attribute record and know which fields are legally load-bearing (sum assured, benefit definition, exclusions, premium mode, eligibility) versus presentational (marketing copy, colour, banner), so you can explain why the catalogue is versioned rather than edited in place — using effective dates, the "what did the client actually see" test, and the repainting risk that version immutability eliminates.
- Apply benefit tables and eligibility rules to a real client profile and predict whether a quote will come back standard, loaded, referred or declined.
Product Taxonomy & Catalogue (產品分類與產品目錄)
Learning Objectives
- Classify any Hong Kong insurance product into one of eight families — life, medical, critical illness, savings, ILAS, accident, home, car — and understand why that classification drives which POS modules, disclosures and regulators become active for a case.
- Read a product attribute record and know which fields are legally load-bearing (sum assured, benefit definition, exclusions, premium mode, eligibility) versus presentational (marketing copy, colour, banner), so you can explain why the catalogue is versioned rather than edited in place — using effective dates, the "what did the client actually see" test, and the repainting risk that version immutability eliminates.
- Apply benefit tables and eligibility rules to a real client profile and predict whether a quote will come back standard, loaded, referred or declined.
The Eight Families
In practice: a client says "I want a medical plan". You do not search for "medical". You filter the catalogue to family MEDICAL, which excludes the ILAS medical rider that would have looked right on a search, and includes the one that pays HK$0 for a pre-existing condition.
Hong Kong's product shelf splits cleanly into eight families. This is not a marketing taxonomy — it is the axis along which POS behaviour changes.
Family definitions
| Family | 中文 | 中文俗稱 | Core promise | Typical premium shape | POS modules engaged | Regulator lens |
|---|---|---|---|---|---|---|
| Life | 壽險 | 壽險 / 定期壽險 (term) | Pays a lump sum on death | Level or increasing, 10–40 yr | Catalogue, Quoting, App, UW, CRM | IA |
| Medical | 醫療險 | 醫療/住院醫療 | Reimburses hospital bills up to an annual limit | Annual, renewable, age-banded | Catalogue, Quoting, App, UW, Claims | IA |
| Critical illness | 危疾險 | 危疾 / 癌症 | Lump sum on diagnosis of a defined condition | Level, 10–25 yr, or attached to savings | Catalogue, Quoting, App, UW, Claims | IA |
| Savings | 儲蓄險 | 儲蓄 / 終身壽 | Accumulates a cash value; pays on death or maturity | Level premium, often 5–20 yr pay | Catalogue, Quoting, App, FNA, CRM | IA |
| ILAS | 投資壽險 | 投資壽險 / 投資連接壽險 | Separates premium into guaranteed + investment-linked parts | Flexible, long-pay | Catalogue, Quoting, FNA, App, App*, CRM | IA and SFC |
| Accident | 意外險 | 意外 | Pays on injury, often by schedule of fixed benefits | Cheap, annual, high-volume | Catalogue, Quoting, App | IA |
| Home | 家居保險 | 家居 / 家居保 | Covers building, contents, liability | Annual, short-term | Catalogue, Quoting, App, Claims | IA / HKMA if bank-distributed |
| Car | 汽車保險 | 汽車 | Mandatory third-party cover; optional own-damage | Annual, usage and driver rated | Catalogue, Quoting, App, Claims | HKMA if the insurer is in a banking group |
Three observations from that table that are worth more than the taxonomy itself:
- General-insurance families (accident, home, car) skip underwriting entirely in most cases. There is no health question, so the UW module in Lesson 06 never activates. But they do have heavy claims workflows, so the claims module is busier per case than for life.
- Car insurance in Hong Kong is compulsory, which inverts the sales relationship. The client is not shopping; they are renewing or they cannot drive. The POS job for car is retention and quote accuracy, not persuasion.
- ILAS is the only family that activates two regulators at once. That is why Lesson 05's suitability step has an ILAS-specific branch, and why the product record for an ILAS plan carries an SFC-shaped disclosure block.
Family membership is data, not logic
A product declares its families as an array. A dual-purpose product can belong to two. A savings-participating whole life plan is ["LIFE", "SAVINGS"] — which is exactly right, and it means the POS will run the FNA module for it (because of SAVINGS) while also offering the CI/simplified-underwriting track (because of LIFE).
The value of data-driven families shows up in the medical edge case:
export type ProductFamily =
| 'LIFE' | 'MEDICAL' | 'CRITICAL_ILLNESS' | 'SAVINGS'
| 'ILAS' | 'ACCIDENT' | 'HOME' | 'CAR';
/** Which POS behaviour each family switches on. */
export interface FamilyBehaviour {
requiresNeedsAnalysis: boolean;
requiresSuitabilityAssessment: boolean;
healthQuestions: 'full' | 'simplified' | 'none';
/** ILAS is the only family with a second regulator's product regime. */
secondaryRegulator: 'SFC' | null;
/** Which agent appointment class can sell it. */
appointmentClass: 'LONG_TERM' | 'GENERAL' | 'LONG_TERM_AND_GENERAL';
claimComplexity: 'LOW' | 'MEDIUM' | 'HIGH';
}
export const FAMILY_BEHAVIOUR: Record<ProductFamily, FamilyBehaviour> = {
LIFE: {
requiresNeedsAnalysis: true, requiresSuitabilityAssessment: false,
healthQuestions: 'full', secondaryRegulator: null,
appointmentClass: 'LONG_TERM', claimComplexity: 'LOW',
},
MEDICAL: {
requiresNeedsAnalysis: false, requiresSuitabilityAssessment: false,
healthQuestions: 'full', secondaryRegulator: null,
appointmentClass: 'LONG_TERM', claimComplexity: 'HIGH',
},
CRITICAL_ILLNESS: {
requiresNeedsAnalysis: true, requiresSuitabilityAssessment: false,
healthQuestions: 'full', secondaryRegulator: null,
appointmentClass: 'LONG_TERM', claimComplexity: 'MEDIUM',
},
SAVINGS: {
requiresNeedsAnalysis: true, requiresSuitabilityAssessment: false,
healthQuestions: 'full', secondaryRegulator: null,
appointmentClass: 'LONG_TERM', claimComplexity: 'LOW',
},
ILAS: {
requiresNeedsAnalysis: true, requiresSuitabilityAssessment: true,
healthQuestions: 'full', secondaryRegulator: 'SFC',
appointmentClass: 'LONG_TERM', claimComplexity: 'MEDIUM',
},
ACCIDENT: {
requiresNeedsAnalysis: false, requiresSuitabilityAssessment: false,
healthQuestions: 'none', secondaryRegulator: null,
appointmentClass: 'GENERAL', claimComplexity: 'LOW',
},
HOME: {
requiresNeedsAnalysis: false, requiresSuitabilityAssessment: false,
healthQuestions: 'none', secondaryRegulator: null,
appointmentClass: 'GENERAL', claimComplexity: 'MEDIUM',
},
CAR: {
requiresNeedsAnalysis: false, requiresSuitabilityAssessment: false,
healthQuestions: 'none', secondaryRegulator: null,
appointmentClass: 'LONG_TERM_AND_GENERAL', claimComplexity: 'MEDIUM',
},
};
The MEDICAL line is the interesting one. requiresNeedsAnalysis: false is not because medical insurance is unimportant — it is because a pure medical plan has no cash-flow projection to defend. Add a rider that converts it to a savings-linked medical product and that flips to true, because now the agent must project cash flows to show the premium gap is fundable. Family membership is what makes that switch automatic.
Attribute Schema
In practice: you open a product record in the POS and the first thing you check is not the headline benefit. It is the version, the effective-from date, and whether the definition block is marked "pre-2019 style" or "core definition".
A Hong Kong POS product record is a versioned document with roughly forty fields. They fall into four tiers, and only the first three are legally load-bearing.
The schema
{
"$schema": "https://pos.hk/schema/product-2.1.json",
"productCode": "HK-whole-life-spp-2024",
"version": 4,
"status": "APPROVED_FOR_SALE",
"carrierCode": "PRC-HK",
"carrierProductId": "LIFE-SPP-F4",
"displayNames": {
"zhHant": "裕悅终身壽險計劃",
"en": "Prosperity Whole Life Plan"
},
"families": ["LIFE", "SAVINGS"],
"productType": "PARTICIPATING_WHOLE_LIFE",
"termOptions": [10, 15, 20, 25, 30, 40],
"currency": { "base": "HKD", "settleOptions": ["HKD", "USD"] },
"premium": {
"modes": ["MONTHLY", "QUARTERLY", "SEMI_ANNUAL", "ANNUAL", "SINGLE"],
"discountableTo": 0.35,
"minimumAnnualPremium": 24000,
"premiumPayingYears": [5, 10, 15, 20, "TO_AGE_100"],
"waiverOfPremiumAvailable": true,
"paymentGraceDays": 31
},
"sumAssured": {
"mode": "MULTIPLE_OF_BASIC_SA",
"multiples": { "min": 1, "max": 20, "step": 1 },
"basicSaCurrency": "USD",
"minBasicSa": 12500
},
"benefits": {
"deathBenefit": {
"definition": "greater of sum assured or 101% of basic sum assured",
"boardApprovalReference": "BA-2024-11-03"
},
"maturityBenefit": { "applies": false },
"terminalIllnessBenefit": { "amountMultipleOfSumAssured": 1.0, "waitingDays": 30 },
"accidentalDeathBenefit": { "amountMultipleOfSumAssured": 1.0 }
},
"benefitTables": ["participating-bonus-2025q1", "sum-assured-sched-riders"],
"eligibility": {
"minIssueAge": 18,
"maxIssueAge": 70,
"minPremiumPayingAge": 18,
"smokerDeclared": "REQUIRED",
"residency": [
{ "labelZh": "香港居民", "mustHoldHkid": true },
{ "labelZh": "非香港居民(持有工作或學生簽證)", "mustHoldHkid": false, "allowed": true }
],
"occupationClasses": { "allowed": [1, 2, 3, 4], "referred": [5], "declined": [6] },
"residencyExclusions": { "sanctionedJurisdictions": true, "maxOffshorePolicyCountPerClient": 2 }
},
"underwriting": {
"track": "FULL",
"medicalEvidenceThresholdHKD": 250000,
"simplifiedTrackEligibleIfSumAssuredUnder": 250000,
"facultative": { "requiredFor": ["OCCUPATION_5_PLUS", "BMI_OVER_35", "DIABETES_ON_INSULIN"] }
},
"exclusions": [
{ "code": "EXC-SUICIDE-24M", "zh": "自殺或自導致傷亡:首 24 個月不賠" },
{ "code": "EXC-INTOX-24M", "zh": "受酒精或藥物影響下意外:首 24 個月不賠" },
{ "code": "EXC-WAR", "zh": "戰爭、暴動、恐怖活動" },
{ "code": "EXC-NUCLEAR", "zh": "核輻射或核污染" }
],
"risks": [
{ "code": "RISK-NON-GUAR", "zh": "非保證利益:紅利及終期紅利並非保證,非分紅基金表現不理想" },
{ "code": "RISK-ILLUSTRATION", "zh": "此為非保證利益,只作說明用途,實際結果可能與說明有重大差異" }
],
"fees": {
"policyFeeAnnualPctOfPremium": 0.08,
"surrenderCharge": [
{ "yearFrom": 1, "yearTo": 2, "pctOfBasicSa": 4.0 },
{ "yearFrom": 3, "yearTo": 5, "pctOfBasicSa": 2.0 },
{ "yearFrom": 6, "yearTo": 10, "pctOfBasicSa": 1.0 },
{ "yearFrom": 11, "yearTo": null, "pctOfBasicSa": 0.0 }
]
},
"lifecycle": {
"effectiveFrom": "2025-04-01",
"effectiveTo": null,
"supersedesVersion": 3,
"boardApprovedAt": "2025-03-12",
"boardApprovedBy": "Board Product Committee",
"productGovernanceRef": "IA-PPG-2025-0417"
},
"regulatory": {
"authority": "IA",
"secondaryAuthority": null,
"disclosureVersionRequired": "DISC-2025.03",
"coolingOffDays": 31,
"taxNoteZh": "退稅只適用於以保費作主要用途的合約,並非所有產品均合資格"
},
"commission": {
"fycMaxPctOfFirstYear": 0.42,
"renewalPctFromYear": { "from": 2, "to": 5, "pct": 0.10 },
"clawbackWindowMonths": 13,
"estimateOnly": true
}
}
The four tiers
| Tier | Examples | Load-bearing? | If you get it wrong |
|---|---|---|---|
| Identity | productCode, version, carrierProductId, lifecycle.* | Yes — determines which artefact is legally being quoted | The audit trail points at the wrong document |
| Contractual | benefits.*, exclusions[], fees.*, risks[], premium.modes, sumAssured.* | Yes — these are the contract terms | Mis-selling; the client can complain and can be right |
| Eligibility | eligibility.*, underwriting.* | Yes — determines whether the case can exist | A declined-at-underwriting case the agent promised would be fine |
| Presentational | displayNames, banners, highlight colours, marketing blurbs | No | Nothing, but it is what the agent actually looks at first |
That last row is the human factor worth naming: agents spend their time on tier 4 and are trained on tier 2. The POS's job is to surface tier 2 and 3 in a form an agent will actually act on — which is why exclusions are rendered as sentences rather than codes, and why EXC-SUICIDE-24M is accompanied by the zh string that goes on the proposal.
Cross-referencing: benefit tables
benefitTables in the product record points at versioned table documents rather than inlining them, because a table is updated far more often than the product definition. A participating policy's bonus illustration changes quarterly; the contract terms do not.
-- Resolution used at quote time. The join is ON VERSION, never "latest".
SELECT
p.product_code,
p.version,
bt.table_id,
bt.table_version,
bt.effective_from,
json_extract(bt.rows_json, '$."[31,0]"') AS row_for_age_31
FROM product p
JOIN product_benefit_table_ref r ON r.product_code = p.product_code
AND r.product_version = p.version
JOIN benefit_table bt ON bt.table_id = r.table_id
AND bt.table_version = :resolved_table_version
WHERE p.product_code = :productCode
AND p.version = :productVersion
AND p.status = 'APPROVED_FOR_SALE';
The AND p.version = :productVersion is load-bearing. A quote for version 4 must never resolve a table "as at latest", or a proposal can silently quote version 4 terms against a version 5 table.
Benefit Tables
In practice: a client asks whether HK$500,000 will still be HK$500,000 in 20 years' time under a participating policy. You open the benefit table version, show the projected value of 51% of it, and say out loud that the other 49% is not guaranteed.
A benefit table is a versioned, dated grid of how much a policy pays under specified circumstances. There are four shapes in active use in Hong Kong.
Shape 1: Sum assured schedule
Used in products where the benefit escalates with term to fight under-insurance at maturity.
| Policy year | Sum assured (indexed) | Pay on death | Pay on diagnosis of covered CI |
|---|---|---|---|
| 1 | 250,000 | 250,000 | 250,000 |
| 10 | 268,000 | 268,000 | 268,000 |
| 20 | 296,000 | 296,000 | 296,000 |
| 30 | 331,000 | 331,000 | 331,000 |
Shape 2: Participating / non-guaranteed bonus projection
The table every agent must learn to present honestly. It has three rows and only one of them is guaranteed.
| Policy year | Guaranteed benefit | Illustrative non-guaranteed benefit @ 4.0% | Illustrative @ 2.0% | Illustrative @ 6.0% |
|---|---|---|---|---|
| 10 | 251,300 | 279,100 | 268,400 | 290,600 |
| 20 | 300,900 | 384,600 | 350,200 | 423,800 |
| 30 | 349,900 | 512,400 | 428,700 | 611,200 |
Note what changes and what does not: the guaranteed column is identical in every scenario. That is the whole client conversation. An agent who shows only the middle column has mis-sold.
Shape 3: Rider schedule (fixed-benefit, schedule-of-benefits style)
Accident and hospital-cash products pay by schedule, not by percentage. This is why they are cheap and why claims are fast.
| Benefit | Amount (HKD) | Waiting period | Max days per policy year |
|---|---|---|---|
| 住院津貼 — 每日 | 800 | Day 1 | 120 |
| 住院津貼 — 首 30 日 | 8,000 | — | per claim |
| 重疾津貼 — 確診 | 50,000 | 30-day survival | per claim |
| 意外死亡 | 200,000 | 30 days | — |
| 意外殘疾 — 永久完全 | 200,000 | 30 days | — |
| 意外殘疾 — 部分(按比例) | 50%–100% of 200,000 | 30 days | — |
| 器官移植 | 100,000 | 30 days | — |
Shape 4: ILAS fund-and-charge table
ILAS benefit tables do not describe payments. They describe charges, because the value is unit-linked and no figure is guaranteed.
| Account | Guaranteed portion | Non-guaranteed portion | Annual policy fee | Fund ongoing charge (max) | Allocation |
|---|---|---|---|---|---|
| A | 20% | 80% | 0.08% p.a. of premium | 1.50% p.a. of fund value | Bond 60 / Equity 20 / Cash 20 |
| B | 10% | 90% | 0.08% p.a. of premium | 1.75% p.a. of fund value | Bond 40 / Equity 55 / Cash 5 |
| C | 0% | 100% | 0.08% p.a. of premium | 2.00% p.a. of fund value | Equity 100 |
The maximum ongoing charge figure is a regulatory disclosure that has to appear in the POS at quote time and on the proposal. An ILAS illustration that omits it is defective on its face.
Choosing a table: the rule
| Client's question | Table to open | Never |
|---|---|---|
| "Is my cover guaranteed?" | Guaranteed benefit row only | Never quote a total that blends guaranteed and non-guaranteed |
| "What if I live to 100?" | Whole-table projection across scenarios | Never present one scenario as expected |
| "How much for cash hospitalisation?" | Rider schedule | Never substitute a percentage-based rider |
| "What's the ILAS yield?" | Fund-and-charge table + live fund data | Never use a static projection for ILAS |
Eligibility Rules
In practice: a 34-year-old mainland resident in HK on a work visa opens the catalogue, and 41% of the long-term products on the shelf grey out with the reason "此產品不適用於非香港居民".
An eligibility rule answers one question: may this product be offered to this client, as of today, by this agent? It is the rule the POS enforces hardest, because every failure is a legally defective sale.
The rule vocabulary
| Rule type | Question | Example | Failure mode |
|---|---|---|---|
AGE | Is the client old/young enough? | minIssueAge 18, maxIssueAge 70 | Cover cannot be offered |
PREMIUM_FLOOR | Is the premium big enough? | minimumAnnualPremium 24,000 | Quote not produced |
RESIDENCY | Is the client resident? | Non-HK resident: product unavailable | Regulatory breach |
OCCUPATION | Is the occupation insurable? | Class 6 (military, commercial diving) declined | Quote refused |
HEALTH | Is the health history acceptable? | Diabetes on insulin → facultative | Case referred |
SMOKER | Declared smoking status | Smoker vs non-smoker rate class | Mispricing |
APPOINTMENT | Does this agent hold the appointment? | Not appointed with carrier X | Cannot submit |
EXISTING_COVER | Offshore holdings | Max 2 offshore policies per client | Regulatory breach |
SANCTIONS | Jurisdiction / name screening | Sanctioned country of residence | AML breach |
CHANNEL | Is this channel permitted for this product? | Online-only products excluded from face-to-face | Conduct breach |
Evaluating them in the right order
Order matters for two reasons: it is faster, and it produces a better error message. A 25-year-old Mainland resident applying for a HK$500,000 participating whole life plan should be told about the age and the residency, but they should be told about the residency first if the product is unavailable to them entirely.
export type RuleType =
| 'AGE' | 'PREMIUM_FLOOR' | 'RESIDENCY' | 'OCCUPATION'
| 'HEALTH' | 'SMOKER' | 'APPOINTMENT' | 'EXISTING_COVER'
| 'SANCTIONS' | 'CHANNEL';
export interface EligibilityContext {
client: {
ageAtNextBirthday: number;
residency: 'HK_RESIDENT' | 'NON_HK_WORK_VISA' | 'NON_HK_OTHER';
smoker: boolean;
occupationClass: 1 | 2 | 3 | 4 | 5 | 6;
offshorePolicyCount: number;
healthFlags: string[];
};
product: {
minIssueAge: number; maxIssueAge: number;
minimumAnnualPremium: number;
residencyAllowed: string[];
occupationAllowed: number[];
maxOffshorePolicyCount: number;
};
channel: 'FACE_TO_FACE' | 'PHONE' | 'ONLINE' | 'BROKER_REFERRAL';
agentCarrierAppointments: string[];
}
export interface RuleOutcome {
type: RuleType;
passed: boolean;
/** Hard failure => product cannot be offered. Soft => must be referred. */
severity: 'BLOCK' | 'REFER' | 'WARN';
messageZh: string;
}
export function evaluateEligibility(ctx: EligibilityContext): RuleOutcome[] {
const { client: c, product: p, channel } = ctx;
const out: RuleOutcome[] = [];
// 1. Appointment first: if we cannot sell it, no rule below is worth running.
if (!ctx.agentCarrierAppointments.length) {
out.push({ type: 'APPOINTMENT', passed: false, severity: 'BLOCK',
messageZh: '你未持有此保險公司的委任,不能推廣此產品' });
return out; // fail fast: stop here
}
// 2. Residency — regulatory, not commercial.
if (!p.residencyAllowed.includes(c.residency)) {
out.push({ type: 'RESIDENCY', passed: false, severity: 'BLOCK',
messageZh: '此產品不適用於目前客戶身分' });
}
// 3. Sanctions / jurisdiction: overrides everything below it.
if (c.residency === 'NON_HK_OTHER') {
out.push({ type: 'SANCTIONS', passed: false, severity: 'BLOCK',
messageZh: '需先完成制裁及居住地查核,未能通過前不可報價' });
}
// 4. Age.
if (c.ageAtNextBirthday < p.minIssueAge || c.ageAtNextBirthday > p.maxIssueAge) {
out.push({ type: 'AGE', passed: false, severity: 'BLOCK',
messageZh: `投保年齡須為 ${p.minIssueAge} 至 ${p.maxIssueAge} 歲` });
}
// 5. Occupation: the REFER band is where agents get caught out.
if (p.occupationAllowed.length && !p.occupationAllowed.includes(c.occupationClass)) {
const declined = c.occupationClass === 6;
out.push({ type: 'OCCUPATION', passed: false,
severity: declined ? 'BLOCK' : 'REFER',
messageZh: declined
? '該職業類別不獲承保'
: '該職業類別需轉介核保,POS 可報價但不可直接提交' });
}
// 6. Health flags that force facultative underwriting.
const facultative = c.healthFlags.filter((f) =>
['DIABETES_ON_INSULIN', 'BMI_OVER_35', 'CARDIAC_HISTORY'].includes(f));
if (facultative.length) {
out.push({ type: 'HEALTH', passed: false, severity: 'REFER',
messageZh: `需提交體檢報告作個別核保:${facultative.join('、')}` });
}
// 7. Premium floor (checked against the *intended* premium, not the minimum).
out.push({ type: 'PREMIUM_FLOOR', passed: true, severity: 'WARN',
messageZh: `年繳保費須不少於 HK$${p.minimumAnnualPremium.toLocaleString('en-HK')}` });
// 8. Offshore concentration limit.
if (c.offshorePolicyCount >= p.maxOffshorePolicyCount) {
out.push({ type: 'EXISTING_COVER', passed: false, severity: 'BLOCK',
messageZh: `客戶已持有 ${c.offshorePolicyCount} 份境外保單,已達上限` });
}
// 9. Channel restriction.
if (channel === 'ONLINE' && p.residencyAllowed.length === 0) {
out.push({ type: 'CHANNEL', passed: false, severity: 'BLOCK',
messageZh: '此產品不設網上投保渠道' });
}
return out;
}
Hard block, referral, warning — the three severities
Getting these apart is a skill. The POS should:
- BLOCK → grey the product out, do not offer it, and say why in one line.
- REFER → allow the quote, forbid straight-through submission, and show the agent exactly what to tell the client: "呢個 case 需要核保師批,我哋今日會交,兩個工作天內有答覆".
- WARN → allow everything, but render the caution on the comparison sheet so it does not get lost in a four-product table.
An agent who cannot distinguish "the POS says no" from "the POS says wait" will either lose a case or make a promise the carrier will not honour.
Versioning & Effective Dates
In practice: a client calls about a policy quoted in March. The POS does not look up "March". It looks up the product version whose effectiveFrom ≤ 2025-03-14 < effectiveTo, and shows the agent the exact document the client signed.
This is the heart of the lesson. A catalogue that can be edited in place is a catalogue that cannot answer "what did you sell me?".
Why never edit in place
The failure mode has a name: repainting history. The moment a benefit, exclusion, rate or age band can be changed on a live product record, three things break simultaneously:
- Audit. The client's signed proposal says benefit clause X. The catalogue now says clause Y. Neither the client nor the carrier can prove which was in force on 14 March.
- In-force business. A policy issued under version 3 must keep its version 3 terms forever. If version 3 was edited to fix a typo in an exclusion, and version 4 is "the corrected one", you have just silently altered a live contract — or, worse, retroactively corrected the client's paperwork.
- Premium disputes. If an exclusion was widened after 500 policies were issued on version 3, and the POS shows version 4 to a new client, the agent is knowingly misrepresenting the older book. If it shows version 3, it is proposing obsolete terms.
None of those is acceptable in a regulated sale. So the catalogue is append-only.
The version lifecycle
DRAFT ──────► INTERNAL_REVIEW ──────► BOARD_APPROVED ──────► APPROVED_FOR_SALE
│ │ │ │
│ ▼ ▼ ▼
└────────────► WITHDRAWN ◄────────── SUPERSEDED ◄───────────── WITHDRAWN
(sold, not for sale)
| Status | 中文 | Meaning | Can it be quoted? | Can it be quoted for submission? |
|---|---|---|---|---|
DRAFT | 草擬 | Being written | No | No |
INTERNAL_REVIEW | 內部審核 | Actuarial + compliance review | No | No |
BOARD_APPROVED | 董事會批准 | Approved, not yet effective | Preview only, clearly watermarked | No |
APPROVED_FOR_SALE | 核准銷售 | Effective and sellable | Yes | Yes |
SUPERSEDED | 已被取代 | Replaced by a newer version | Yes, for in-force and pending cases | No — re-quote on the new version |
WITHDRAWN | 撤回 | Pulled from sale | Yes, read-only, in-force only | No |
The SUPERSEDED row is the one that saves you. When a carrier improves a product mid-month and version 5 goes live, every case still sitting in your queue at version 4 keeps its version 4 terms. You are not obliged to re-quote; you are obliged to record which version you used.
The effective-dating fields that actually matter
| Field | Meaning | Trap if wrong |
|---|---|---|
effectiveFrom | First day this version may be quoted and submitted | Quoting from 1 day early = selling an unapproved product |
effectiveTo | Last day (inclusive) | Off-by-one on the boundary day |
boardApprovedAt | When governance approved | Disclosing "approved" for a version approved after the sale |
supersedesVersion | Explicit pointer to predecessor | Silent lineage breaks; a query for "all changes" misses a link |
rateCardVersion (Lesson 03) | Which pricing table applies | Term quoted on one card, priced on another |
disclosureVersionRequired | Which disclosure must be acknowledged | Client signs a disclosure that is not the one in force |
Why every quote stores the version triple
{
"quoteId": "QT-20881-A",
"productCode": "HK-whole-life-spp-2024",
"productVersion": 4,
"rateCardVersion": "PRC-LIFE-2025-03-14",
"benefitTableVersion": "participating-bonus-2025q1",
"assumptionsVersion": "ASSUMP-2025.02",
"quotedAt": "2025-03-14T11:38:22+08:00",
"quotedByAgentRef": "AG-4471",
"deviceId": "TBLT-07",
"validUntil": "2025-04-13T11:38:22+08:00",
"immutability": {
"payloadHash": "9f2c1a44e0b7",
"supersededBy": null,
"supersedes": "QT-20881-A-ORIG"
}
}
Five version pointers on one quote. An auditor asking "reconstruct this sale" needs all five, and the POS has all five because there was never a moment when they could have been overwritten.
The withdrawal drill
Every version needs a plan for the day it is withdrawn. A product withdrawn because of a regulatory concern must be handled differently from one withdrawn because it is being replaced:
| Withdrawal reason | 在架處理 | Existing quotes | In-force business | POS behaviour |
|---|---|---|---|---|
| Regulatory concern | Remove from sale immediately, flag in-force book | All pending quotes invalidated, client re-advised | Proactive review with the carrier | Block submission; generate a call list |
| Replacement with successor | Supersede, keep readable | Pending quotes stay valid until expiry | No action | Keep quoting version N-1 if the quote is unexpired |
| Data-entry error in the record | Correct by issuing N+1 | Pending quotes flagged for review | No change | Amber badge + human review |
| Carrier commercial decision | Withdraw | Pending quotes re-quoted onto successor | No change | Auto-migrate with a client notification task |
Catalogue Loading and Caching
In practice: you open the catalogue on the MTR at 8:40am and every product, including two that were launched yesterday afternoon, is there. The device synced for eleven minutes while you were on a night flight.
The catalogue is the largest static dataset in a POS, and because the POS is offline-first (Lesson 01), it is the thing that most needs a disciplined distribution strategy.
Sizes and shapes
| Dataset | Typical rows | Payload size | Refresh cadence |
|---|---|---|---|
| Product headers (identity + eligibility) | 400–1,500 | 1–4 MB | Daily, plus push on launch |
| Benefit tables | 2,000–6,000 | 6–20 MB | Weekly, plus push on board approval |
| Rate cards (Lesson 03) | 1,000–3,000 | 3–12 MB | On effective date, sometimes intraday |
| Disclosure templates | 20–60 | 0.5–2 MB | On regulatory change — must be immediate |
| Asset icons / illustrations | 5,000–40,000 | 20–120 MB | Weekly, image-heavy, biggest of the set |
The delta-sync model
The device keeps a watermark per dataset, and the server sends only what changed since the watermark. Watermarks are per dataset, not global — a disclosure change must not force a 120 MB asset resend.
CREATE TABLE device_catalogue_watermark (
device_id TEXT NOT NULL,
dataset TEXT NOT NULL, -- PRODUCTS | BENEFIT_TABLES | RATE_CARDS | DISCLOSURES | ASSETS
watermark TEXT NOT NULL, -- monotonic: ULID of the last applied change
applied_at TIMESTAMP NOT NULL,
rows_applied INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (device_id, dataset)
);
CREATE TABLE catalogue_change_log (
change_id TEXT PRIMARY KEY, -- ULID, sortable = insertion order
dataset TEXT NOT NULL,
entity_key TEXT NOT NULL, -- e.g. 'HK-whole-life-spp-2024@v4'
change_type TEXT NOT NULL, -- UPSERT | WITHDRAW | SUPERSEDE
effective_from TIMESTAMP NOT NULL,
payload TEXT, -- NULL for WITHDRAW
published_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_catalogue_change_watermark
ON catalogue_change_log (dataset, change_id);
The delete is the interesting part. In an append-only catalogue, "withdraw" is an UPSERT with status: WITHDRAWN, never a DELETE. A device that has been offline for a week must not lose a version; it must learn that the version is withdrawn.
Staleness policy per dataset
| Dataset | Max tolerated staleness | Beyond that |
|---|---|---|
| Disclosures | Zero. A stale disclosure is a defective sale | Block the application module entirely |
| Rate cards | 0 hours for promo-tied rates; 48 h otherwise | Re-quote required |
| Product headers | 24 hours | Show amber "資料可能過時"; block submission on withdrawn items only |
| Benefit tables | 7 days | Show the table version on every projection, always |
| Assets | 30 days | Cosmetic only; never blocks a transaction |
Why disclosures are the hard real-time case
A disclosure document is the one artefact whose staleness is immediately unlawful. If the IA issues a revised Code of Conduct disclosure at 09:00 and a device's disclosure cache is from yesterday, then every application signed on that device today is signed against the wrong document.
This is why the disclosure set must be:
- Pushed rather than polled, on publication.
- Version-pinned to the moment of signature (
DISCLOSURE_ACKNOWLEDGEMENT.artefact_version), never to the moment of reading. - Refused-if-unavailable — an agent who cannot fetch the current disclosure cannot proceed to signature.
/**
* Guard for the application module's signature step.
* Deliberately fails CLOSED: unknown disclosure state means no signature.
*/
export function canSignWith(
deviceDisclosureVersion: string | null,
serverDisclosureVersion: string,
loadedAt: Date | null,
now: Date,
): { allowed: boolean; reasonZh?: string } {
if (deviceDisclosureVersion === null || loadedAt === null) {
return { allowed: false, reasonZh: '此裝置未載入任何風險披露文件版本,請先連線更新' };
}
if (deviceDisclosureVersion !== serverDisclosureVersion) {
return {
allowed: false,
reasonZh: `披露文件版本不符(裝置 ${deviceDisclosureVersion},系統 ${serverDisclosureVersion}),請更新後再簽署`,
};
}
const ageMin = (now.getTime() - loadedAt.getTime()) / 60000;
if (ageMin > 30) {
return { allowed: false, reasonZh: '披露文件載入已逾 30 分鐘,請重新連線確認' };
}
return { allowed: true };
}
Note the 30-minute window. It is deliberately shorter than any real staleness risk, because a disclosure change is fast, cheap to check, and catastrophic to get wrong. This is the one place in the POS where "fail closed, retry later" beats "carry on and reconcile".
Worked Example: One Client, Four Shelves
In practice: you search the catalogue for a 34-year-old Mainland resident in HK on a work visa, and 58% of the long-term shelf greys out. Three different rules did it, and the client sees one screen that says "not available".
This is the single most instructive thing in the lesson, because it shows how independent rules compose into one outcome the client experiences as a wall.
The client
| Field | Value |
|---|---|
| Age at next birthday | 34 |
| Sex | F |
| Residency | NON_HK_WORK_VISA — mainland resident, HK work visa valid 22 months |
| HKID | Not held |
| Smoker | No (5 pack-years, stopped 3 years ago) |
| Occupation | Class 2 (registered nurse) |
| BMI | 22.1 |
| Existing cover | 1 offshore savings policy (Shanghai) |
| Budget | HK$4,000/month |
| Primary need | "If I can't work, my parents don't lose the house" |
What the catalogue returns
| Product | Family | Result | Rule that fired |
|---|---|---|---|
| HK Whole Life SPP v4 | LIFE, SAVINGS | Available | residency allows NON_HK_WORK_VISA |
| Evergreen Medical v2 | MEDICAL | Available | no residency restriction; note: offshore limit applies |
| Shield CI Basic v1 | CRITICAL_ILLNESS | Available | CI core definition, age 34 in band |
| Prosper ILAS v3 | ILAS | Referral | CHANNEL + SFC suitability step required; fund options restricted for non-HK residents at some carriers |
| Motor Accident Pro v9 | ACCIDENT | Available | general insurance, no health questions |
| Heritage Mortgage Life v7 | LIFE | Blocked | RESIDENCY — HK mortgage cover, HKID required |
| HK Term Life 30 v5 | LIFE | Blocked | RESIDENCY — HKID required for issue |
| Voyager Offshore v1 | ILAS | Blocked | EXISTING_COVER — client already holds 1 offshore policy, carrier limit is 1 for this class |
Three rules fired across eight products. From the client's side it was one screen. From the compliance side it was three separate decisions with three separate reasons, each of which must be recorded.
What the POS must show the client
符合你條件的方案:3 個
未能提供的方案及原因:
✗ Heritage Mortgage Life v7 — 此產品只接受香港身份證明持有人
✗ HK Term Life 30 v5 — 此產品只接受香港身份證明持有人
✗ Voyager Offshore v1 — 你已持有 1 份境外儲蓄保單,達此產品上限
⚠ Prosper ILAS v3 — 可申請,但需完成投資風險評估,
且投資選項較一般產品受限
That last block is the one an amateur POS omits, and it is the one that protects the agent. It converts a silent refusal into an explanation, and it separates blocked from referral visually — the difference between "no" and "yes, but with work".
The agent's follow-up, and why the POS must support it
The right move for this client is not a third medical plan. It is to notice that her actual need — income protection — is under-served by every product that just greyed out, and that the honest answer is a CI plan or an accident plan she can buy today, plus an explicit note that long-term savings products for non-HK residents are constrained.
A POS that only answers "is this product available?" makes it easy to sell the wrong thing. A POS that answers "is this product available, and what does that mean for the client's need?" teaches the right thing. That is the difference between a catalogue and an adviser tool.
Key Takeaways
In practice: nine sentences that will keep you out of trouble when a carrier rep, a compliance officer or a client asks you a question you were not expecting.
- Eight families — LIFE, MEDICAL, CRITICAL_ILLNESS, SAVINGS, ILAS, ACCIDENT, HOME, CAR — and family membership is data that switches POS behaviour on. A savings-participating whole life plan belongs to two families, which is why it runs both the FNA and the underwriting track.
- The four attribute tiers are identity, contractual, eligibility and presentational. Only the first three are legally load-bearing; the fourth is what agents actually look at, so the POS must re-render tier 2 and 3 in a form that gets read.
- A benefit table is versioned and referenced, never inlined. A quote resolves its table on
table_version, never "latest", or version 4 terms get priced against a version 5 projection. - Never blend guaranteed and non-guaranteed benefit in a single number. The guaranteed column is identical across every scenario in a participating projection — that identical column is the client conversation.
- ILAS benefit tables describe charges, not payments. The maximum ongoing charge is a regulatory disclosure that must appear at quote time and on the proposal.
- Eligibility has three severities, not two. BLOCK greys the product out, REFER lets you quote but forbids straight-through submission, WARN renders on the comparison sheet. Confusing REFER with BLOCK loses cases; confusing it with WARN makes promises the carrier will not keep.
- The catalogue is append-only and never edited in place. Because editing in place breaks the audit trail, alters in-force contracts, and creates a repainting-history exposure on every live book.
SUPERSEDEDis the status that saves you. A version replaced mid-month keeps its terms for pending quotes and in-force business; the successor is used for new cases only.- Every quote stores five version pointers — product, rate card, benefit table, assumptions, disclosure — because "what did you sell me?" is unanswerable without all five. Disclosures additionally fail closed at 30 minutes, which is the one place in the POS where retrying is correct and carrying on is not.
Glossary (zh-Hant-HK)
| Term | 中文 | Note |
|---|---|---|
| Catalogue | 產品目錄 | The versioned shelf of products |
| Product family | 產品分類 | One of the eight families |
| Sum assured | 基本保額 / 保額 | The death benefit base amount |
| Rider | 附加保障 / 附加險 | An optional add-on benefit |
| Participating | 分紅 | Policy with non-guaranteed bonuses |
| Non-participating | 非分紅 | Policy with only guaranteed benefits |
| Benefit table | 利益一覽表 | Versioned grid of payables |
| Effective date | 生效日期 | First day a version may be sold |
| Supersede | 取代 | A newer version replaces an older one |
| Eligibility | 投保資格 | May this product be offered to this client? |
| Occupation class | 職業類別 | 1–6 insurable-banding used by underwriting |
| Issue age | 投保年齡 | Age at the policy's in-force date |
| Persistency | 續期率 | Share of policies still in force |
| Onboarding | 迎新 / 載入 | Loading a dataset onto a device |
| Delta sync | 差異同步 | Send only what changed since a watermark |
| Fail closed | 封閉失敗 | Refuse the action when state is unknown |