學習目標 · Learning Objectives
- Distinguish an agent-facing POS from a consumer comparison app. A POS is a licensed advisor's production console: it is where the sales journey (需求分析 → 報價 → 投保 → 服務 → 理賠) is recorded, not where a shopper browses products.
- Explain why Hong Kong, specifically, needed a consolidated POS. Many carriers under one regulator (IA), an offline-first field sales force, cross-border clients with Mainland / non-HK residency, and a regulatory regime where the application file is the auditable evidence of a compliant sale.
- Place the boundary between the POS and the carrier's policy admin system. The POS owns the sales journey up to (and including) submission; the carrier owns the policy lifecycle after acceptance. Almost every support ticket about a HK POS eventually traces back to a misunderstanding of this line.
What is an Insurance POS (保險 POS 係乜嘢)
Learning Objectives
- Distinguish an agent-facing POS from a consumer comparison app. A POS is a licensed advisor's production console: it is where the sales journey (需求分析 → 報價 → 投保 → 服務 → 理賠) is recorded, not where a shopper browses products.
- Explain why Hong Kong, specifically, needed a consolidated POS. Many carriers under one regulator (IA), an offline-first field sales force, cross-border clients with Mainland / non-HK residency, and a regulatory regime where the application file is the auditable evidence of a compliant sale.
- Place the boundary between the POS and the carrier's policy admin system. The POS owns the sales journey up to (and including) submission; the carrier owns the policy lifecycle after acceptance. Almost every support ticket about a HK POS eventually traces back to a misunderstanding of this line.
What a POS Is
In practice: an agent taps "開始報價" on a tablet in a Tseung Kwan O coffee shop and a versioned, timestamped, attributed sales record is created inside eight seconds — without a single page of paper.
An insurance POS (Point-of-Sale) is the transaction surface used by an authorised advisor at the moment of selling a policy. In Hong Kong the phrase covers everything from the 12-inch laptop at a brokerage desk to an 8-inch Android tablet used by a general agent sitting in a McDonald's with three bars of 4G.
The word "POS" is misleading in the insurance context, and worth unpacking early:
- In retail, a POS means the till: a device that takes money and prints a receipt.
- In insurance, the POS does not take the premium at all. It takes the application. The premium moves later, through a separately regulated and separately permissioned payment channel, and often a month later.
So the POS is better understood as an origin-of-record production console. Its unit of work is not a transaction but a case: a client, a need, a recommendation, a comparison, a signed proposal, and everything the regulator or the carrier would later need to prove that what you told the client was true, was approved, and was documented.
Three properties follow from that framing, and they explain most POS design decisions in the rest of this course:
- Attribution. Every record must resolve to exactly one agent, one agency, one carrier, one product version, and one moment in time. This is why quotes are immutable and versioned rather than "updated in place" (Lesson 02, Versioning & Effective Dates).
- Evidence. The screen state you showed the client is part of the compliance file. Which illustration was displayed, which benefit illustration the client was shown, and which product version was quoted all become material to a possible later complaint.
- Interoperability. A single agent commonly holds appointments with five to nine carriers. The POS is not one carrier's system; it is a hub that speaks each carrier's dialect.
The POS as a hub, not a monolith
A useful mental model: the POS is a broker-protocol gateway with a UI attached. It owns a normalised internal model — client, product, quote, application, policy reference, claim — and it owns a set of connectors that translate that model into each carrier's proprietary submission format.
{
"posInstance": "hk-pos-prod",
"normalisationVersion": "2024.11",
"region": "HK",
"carriers": [
{
"carrierCode": "AIA-HK",
"displayName": "美國友邦保險",
"protocol": "REST+JSON",
"supports": ["quote", "apply", "statusPoll", "servicing", "claimNotify"],
"auth": { "type": "mTLS", "certAlias": "aia-prod-client" },
"slaMs": { "p95Quote": 1800, "p95Submit": 4200 },
"offlineCapable": true,
"rateCardSync": "daily+push"
},
{
"carrierCode": "PRU-HK",
"displayName": "英國保誠",
"protocol": "SOAP+WS-Security",
"supports": ["quote", "apply", "statusPoll", "servicing"],
"auth": { "type": "OAuth2", "tokenEndpoint": "https://gwm.prudential.com.hk/oauth2/token" },
"slaMs": { "p95Quote": 3100, "p95Submit": 9800 },
"offlineCapable": false,
"rateCardSync": "daily+poll"
}
]
}
Note the asymmetry in that JSON. Not every carrier supports the same verbs, not every connector survives going offline, and the p95 latency budget differs by an order of magnitude. A UI that assumes every carrier behaves the same will be broken in production by exactly the slowest connector.
What a POS Is Not (consumer comparison app)
In practice: a client sends you a screenshot of a comparison site quoting a product you do not hold. You open your POS, search the same product by product code, and find the carrier's product — different benefit definitions, different loading, different definition of "critical illness".
This is the single most common misconception among new advisors, and it is worth being blunt about: the consumer comparison app and the agent POS optimise for opposite things.
| Dimension | Consumer comparison app | Agent-facing POS |
|---|---|---|
| Primary user | A member of the public shopping for cover | A licensed advisor producing a regulated case file |
| Unit of output | A shortlist, then a "get a quote" lead | A policy application, then a commissionable case |
| Who pays | Advertiser (the comparison site) | The carrier, via FYC (first year commission) / renewals |
| Product set | A few "popular" products, often subsidised or sponsored | The agent's appointed book: potentially hundreds of SKUs |
| Depth of questions | 5–8 screening questions | Full proposal + Financial Needs Analysis + disclosures |
| Regulatory posture | Minimal — general "compare" disclaimer | Full: client needs analysis, product suitability, disclosure, record-keeping |
| Success metric | Click-through, lead volume | Persistency (保單續期率), compliance pass rate |
| Price presentation | "From HK$xx / month" | Gross premium, discounts, loading, commission, illustration |
| Data model | Read-optimised catalogue | Append-only case file with full audit trail |
Read that table again for the row that matters most: price presentation. A comparison site shows you HK$312/month. That number is almost never the gross premium your client will actually be charged, and it is essentially never the number the POS will submit.
Why the numbers never line up
Three mechanisms cause the divergence, and an advisor who does not know them will eventually lose a client's trust in front of a claim:
- Discounts. Most HK life carriers offer agency or promotional discounts (a percentage off the gross premium). The comparison site shows the gross list price; your POS shows the discounted price after applying your agency's tier. A 35% first-year discount is normal for a mid-tier agency.
- Riders and bundles. The comparison site prices the base benefit. The agent is expected to cover the base benefit and the riders required to make the protection complete, plus savings riders that fund the premium later.
- Different product definitions. Two carriers' "critical illness" products may pay a lump sum on the same diagnosis, or may not — 2019-to-2023 was a period of major divergence in cancer and cardiac definitions, and the industry since moved toward a core definition plus optional riders. A side-by-side comparison that ignores definitions is a comparison of numbers, not of cover.
The right mental comparison
If you need an analogy: the agent POS is closer to a case management system with a quote engine attached than to an e-commerce front end. It behaves like a CRM that happens to be very good at pricing, and very disciplined about what it is allowed to write.
Why Hong Kong Specifically
In practice: you are standing in Mong Kok with a client who holds a mainland passport, a HK ID card, and a Shenzhen property. The POS decides which identity document to ask for, which product you may not sell them, and which regulatory clock is running.
Hong Kong is an unusual insurance market for reasons that all push in the same direction: fragmentation. If you are building an agent POS that only has to work in one market, this is the ideal market to build it in.
Fragmentation dimension 1: the carrier count
Hong Kong has an unusually dense long-term insurance market. A serious agency will hold appointments across a broad panel of pan-Asian carriers, plus local brands, plus specialist writers in accident (意外險) and ILAS. For a given advisor, that means several completely separate IT estates:
- Different submission formats (JSON, SOAP, fixed-width file, portal with manual upload)
- Different underwriting rule engines (some accept data online, some want a scanned proposal)
- Different e-approval mechanisms (some straight-through, some pending underwriter review)
- Different servicing and claims portals
- Different product nomenclatures for what is nominally the same coverage
None of these are willing to change for you. The POS exists precisely to absorb that heterogeneity so the agent does not have to.
Fragmentation dimension 2: the offline-first field reality
The other half of Hong Kong's geography — Kowloon, the New Territories, outlying islands, Lantau — is not well served by enterprise-grade connectivity in every stairwell. A traditional agency sales model is relational: the agent goes to the client. That means:
- Client sites with poor signal (basement car park, remote village house)
- Client sites with no signal preference (bedroom at 11pm)
- Reasonable client expectation that you will not need signal for a 20-minute meeting
So the POS in HK cannot be a thin client that assumes a live API. It must be a local-first application that can quote, build a comparison, capture a proposal, and record a client signature, and then reconcile.
That constraint is not a workaround. It is a first-class architectural requirement, and it explains a huge share of the data model in Lessons 08 and 09.
Fragmentation dimension 3: the regulatory perimeter
Hong Kong's long-term insurance regulation sits with the Insurance Authority (保險業監管局, IA), which became a separate statutory office on 1 April 2015 under the Insurance Authority Ordinance (Cap. 117), taking over from the Office of the Commissioner of Insurance. The main supervising ordinance remains the Insurance Ordinance (Cap. 106).
The implication for a POS is concrete:
- A sale file must be reconstructible years later. The application form, the needs analysis, the disclosures, the agent's identity at the time, and the product version quoted are all part of the evidence.
- Supervisory expectations are enforced through the systems: pre-contract disclosures must actually be captured, cooling-off rights must actually be registered, suitability checks must actually be logged.
- Because the IA is a separate regulator with its own supervision function, carriers' internal controls are tighter and more consistently audited than they were under the old Office-of-the-Commissioner regime.
Fragmentation dimension 4: the client mix
Hong Kong policyholders routinely are, in some combination: HK residents, mainland residents (with HKID via 磁咭易 / eID verification schemes), foreign domestic helpers, mainland professionals with HK cover, or dual-resident families. This creates:
- Product eligibility rules that differ by residency status (a mainland resident in HK may face different availability of certain savings products)
- Different permissible sales channels (in-person, online, via a Mainland branch)
- Different disclosure language requirements and signature rules
The POS carries these as data (eligibility rules per product per client-segment), not as code branches written by developers. Lesson 02 covers how that data is versioned; this is why it has to be.
System of Record Boundaries
In practice: a client calls two years later saying "you told me the sum assured was HK$500,000". You cannot answer from the POS — the POS stops at submission. You pull the carrier's policy admin system, which is the system of record for the sum assured.
This is the most important section in the lesson. Get it wrong and every downstream question becomes an argument.
The boundary, stated precisely
The POS is the system of record for the sales journey (需求分析, quotation, comparison, proposal, application capture, submission, and submission acknowledgement). The carrier's policy administration system is the system of record for the policy lifecycle (underwriting decision, issue, premium collection, billing, alterations, endorsements, claims, dividends, lapses, revivals).
The handover point is the submission acknowledgement: the carrier system accepts the application and returns an application reference. Before that, the POS is authoritative. After that, the carrier is authoritative — and the POS keeps a read-only projection of the carrier's state.
Ownership table
| Data object | System of record | POS role | Notes |
|---|---|---|---|
| Client / party record | POS (CRM) | R/W | Merged with carrier's party data on issue |
| Financial needs analysis | POS | R/W | Immutable once signed; revisions are new versions |
| Quote / illustration | POS | R/W, immutable | Bound to a product version + rate card version |
| Comparison / bid sheet | POS | R/W | Derived; never authoritative on its own |
| Proposal (建議書) | POS | R/W, immutable | Wet/digital signature captured by POS |
| Application (投保書) as submitted | Carrier | R (copy) | The carrier holds the accepted artefact |
| Submission acknowledgement | Carrier | R | Pollable status; drives POS case state |
| Underwriting decision | Carrier | R | POS only displays |
| Policy contract / 保單 | Carrier | R | Sum assured, exclusions, riders all carrier-authoritative |
| Premium ledger / billing | Carrier | R | POS may show a "last synced" snapshot only |
| Alterations / endorsements | Carrier | R | Service request originates in POS, executed in carrier |
| Claims | Carrier | R | Notification may originate in POS; adjudication never does |
| Commission | Carrier/agency system | R | Agreed in advance; POS is a reporting mirror, not the ledger |
That last row catches people. Agents routinely try to calculate their income from POS data. Commission is a function of premium actually collected (not premium quoted), of persistency at 12–24 months, and of the agency's own clawback rules (Lesson 12). The POS can show an estimate; it cannot show what you will be paid.
Why the split exists (three real reasons)
- Regulatory separation. The carrier is the licensed insurer and bears the reserving and solvency obligation; it must control the policy record itself. A third-party POS that owned the policy record would be an unlicensed insurer's core system.
- Technical reality. Policy admin systems are often decades old (mainframes, AS/400 lineage), tuned for batch correctness over decades of in-force business, not for a responsive UI on a tablet.
- Commercial reality. The POS serves many carriers; the admin system serves one. Mixing them would mean rewriting the POS per agency.
The anti-pattern: the POS that pretends to be admin
The failure mode is a "one-stop" POS that shows a policy screen that looks authoritative but was last synced three days ago, during which the client changed their address and added a rider. The agent tells the client something that is now wrong.
The rule for POS developers and for agents reading a POS screen:
-- Every carrier-sourced value in the POS must carry its provenance and freshness.
CREATE TABLE carrier_projection (
case_id TEXT NOT NULL,
policy_ref TEXT NOT NULL,
field_name TEXT NOT NULL,
field_value TEXT,
carrier_updated_at TIMESTAMP NOT NULL, -- when the carrier says it changed
projected_at TIMESTAMP NOT NULL, -- when we last pulled it
source_connector TEXT NOT NULL, -- which carrier API
sync_state TEXT NOT NULL, -- FRESH | STALE | FAILED
PRIMARY KEY (case_id, field_name)
);
CREATE INDEX idx_carrier_projection_stale
ON carrier_projection (projected_at)
WHERE sync_state <> 'FRESH';
-- The UI queries this view and must render the freshness badge from it.
CREATE VIEW v_policy_display AS
SELECT
c.policy_ref,
c.sum_assured,
c.billing_mode,
p.projected_at,
p.sync_state,
CASE
WHEN p.sync_state = 'FRESH' THEN '資料已同步'
WHEN p.sync_state = 'STALE' THEN '資料可能過時,請以保單為準'
ELSE '同步失敗,請致電保險公司確認'
END AS freshness_notice_zh
FROM carrier_case c
JOIN carrier_projection p
ON p.case_id = c.id AND p.field_name IN ('sum_assured', 'billing_mode');
An agent who sees "同步失敗" knows to phone the carrier. An agent who sees a clean-looking number with no freshness badge does not. That is the whole point of the view.
The POS Module Map
In practice: you can recognise which module you are in by its top-left breadcrumb, and — more usefully — by the identifier prefix on the record you are holding: CL- client, QT- quote, PR- proposal, AP- application, SV- service request.
Seven modules cover the seven things a POS does. Everything else is a feature of one of them.
The seven modules
/**
* Module contract shared by every POS module.
* The UI shell renders capability chips from `capabilities`, and the
* offline engine uses `offline` to decide what degrades gracefully.
*/
export interface PosModule {
id:
| 'catalogue'
| 'quoting'
| 'application'
| 'kyc'
| 'servicing'
| 'claims'
| 'crm';
displayNameZh: string;
displayNameEn: string;
/** Modules whose records are append-only after signature. */
immutableAfter?: 'signed' | 'submitted';
capabilities: ModuleCapability[];
offline: {
/** Can the agent open this module with zero connectivity? */
readableOffline: boolean;
/** Can they complete a *business transaction* offline? */
writableOffline: boolean;
/** What the client sees if writable is true. */
deferralNoticeZh?: string;
};
/** Carrier features this module knows it cannot do. */
knownGaps: string[];
}
export type ModuleCapability =
| 'catalogue.browse' | 'catalogue.compareVersions'
| 'quoting.calculate' | 'quoting.multiCarrier'
| 'application.capture' | 'application.sign'
| 'application.submit' | 'kyc.idRead' | 'kyc.addressProof'
| 'kyc.sanctionsScreen' | 'servicing.initiate' | 'servicing.track'
| 'claims.notify' | 'claims.track' | 'crm.pipeline';
export const MODULES: PosModule[] = [
{
id: 'catalogue',
displayNameZh: '產品目錄',
displayNameEn: 'Product Catalogue',
immutableAfter: 'submitted',
capabilities: ['catalogue.browse', 'catalogue.compareVersions'],
offline: { readableOffline: true, writableOffline: false },
knownGaps: ['ILAS fund performance graphs need a network round-trip'],
},
{
id: 'quoting',
displayNameZh: '報價',
displayNameEn: 'Quoting',
immutableAfter: 'submitted',
capabilities: ['quoting.calculate', 'quoting.multiCarrier'],
offline: {
readableOffline: true,
writableOffline: true,
deferralNoticeZh: '離線報價將於恢復網絡後送出,並以當時的費率版本為準',
},
knownGaps: ['Occupation-class questions for 6 carriers differ in wording'],
},
{
id: 'application',
displayNameZh: '投保',
displayNameEn: 'Application',
immutableAfter: 'signed',
capabilities: ['application.capture', 'application.sign', 'application.submit'],
offline: {
readableOffline: true,
writableOffline: true,
deferralNoticeZh: '離線簽署的建議書會先儲存,聯網後需由你手動確認送出',
},
knownGaps: ['Two carriers still require a scanned wet-signed proposal'],
},
{
id: 'kyc',
displayNameZh: '客戶身分識別',
displayNameEn: 'KYC & eKYC',
immutableAfter: 'submitted',
capabilities: ['kyc.idRead', 'kyc.addressProof', 'kyc.sanctionsScreen'],
offline: {
readableOffline: true,
writableOffline: true,
deferralNoticeZh: '制裁名單及政治人物名單需聯網查核,將於同步時補做',
},
knownGaps: ['Offline capture of an identity document photo is not a complete eKYC'],
},
{
id: 'servicing',
displayNameZh: '保單服務',
displayNameEn: 'Policy Servicing',
capabilities: ['servicing.initiate', 'servicing.track'],
offline: {
readableOffline: true,
writableOffline: true,
deferralNoticeZh: '服務申請將排隊,聯網後按提交次序送出',
},
knownGaps: ['Reinstatement requests are always routed to the carrier, never auto-decided'],
},
{
id: 'claims',
displayNameZh: '理賠',
displayNameEn: 'Claims',
capabilities: ['claims.notify', 'claims.track'],
offline: { readableOffline: true, writableOffline: true, deferralNoticeZh: '已登記,待同步後正式立案' },
knownGaps: ['Claim status is 100% carrier-sourced; offline mode shows last known only'],
},
{
id: 'crm',
displayNameZh: '客戶關係管理',
displayNameEn: 'CRM',
immutableAfter: 'submitted',
capabilities: ['crm.pipeline'],
offline: { readableOffline: true, writableOffline: true, deferralNoticeHtml: undefined },
knownGaps: ['Pipeline forecasts are stale the moment you leave the office'],
},
];
What each module owns
| Module | Owns | Does not own | Lesson |
|---|---|---|---|
| Product catalogue | Product definitions, attributes, benefit tables, eligibility, versions | Premium calculation (that is quoting) | 02 |
| Quoting | Rate cards, loading, discounts, illustrations, comparison matrices | The signed proposal | 03, 07 |
| Application | Proposal, disclosures, signature capture, submission, status polling | Underwriting decision | 08 |
| KYC | Identity evidence, screening results, four-eyes escalation | The answer to any eligibility question | 09 |
| Servicing | Service request intake, status, document requests | The endorsement itself | 10 |
| Claims | Notification, claim form capture, document checklist, status display | Adjudication, settlement, reserve | 11 |
| CRM | Pipeline, activities, birthdays, renewals, compliance attestations | Case files (those live in the other modules) | 12 |
Why the CRM is a module and not a bolt-on
A CRM that is not part of the POS is a different application that also has to integrate with the POS. In HK, small agencies that run a separate CRM typically end up with: a CRM that knows nothing about the carrier's acceptance status, and a POS that knows nothing about who the agent last spoke to. The result is duplicated data entry and a compliance module that cannot see the pipeline.
Embedding CRM into the POS also enables a genuinely useful pattern: renewal and persistency tasks generated from carrier status. When the projection shows a policy reaching its first anniversary in 34 days with sync_state = FRESH, the CRM module creates a follow-up task against the same client record. That is a "compliance-plus-sales" loop, and it is worth real money.
Offline-First Field Reality
In practice: the POS loses signal halfway through a proposal in Sai Kung. The fields the agent already typed stay typed, the quote stays open, the signature capture still works, and the client never sees a spinner.
What "offline-first" must mean
There are two different designs and the distinction matters enormously:
- Offline-capable — the app works without a network, but each action needs the network eventually. Simple: queue and retry.
- Offline-first — the local device holds the authoritative working copy; the network is a synchronisation partner. Every read and write goes through a local store (SQLite on the device), and a sync engine reconciles.
Insurance POS in HK needs the second, because quoting and illustration must render identically with or without a network, and because an agent who has shown a client an illustration offline must not be able to discover at submission time that the numbers changed.
The four things that must work offline
- Browsing the catalogue at whatever versions the device last downloaded.
- Quoting, using the rate card version bundled with the device. Quote IDs must therefore embed the rate card version, or be stored alongside it.
- Building and saving a comparison across carriers.
- Capturing a proposal and a client signature — the offline signature module must be tamper-evident (hash-chained) even though it is not yet evidence for the carrier.
What must not work offline, and must be refused with a clear message rather than silently queued:
- Sanctions / PEP screening (Lesson 09). The system must not record a screening result it never performed.
- Final submission to a carrier that has not acknowledged receipt of the required disclosures.
- ILAS suitability sign-off, which depends on live fund data.
- Anything that must reflect today's rate card if the device is more than a defined staleness threshold behind (e.g. 48 hours for a promo-tied rate).
The outbox pattern
// Sync engine core. Every mutation is written locally first, then enqueued.
export type MutationKind =
| 'client.upsert'
| 'quote.create'
| 'proposal.sign'
| 'kyc.evidence.attach'
| 'serviceRequest.create'
| 'claim.notify';
export interface OutboxEntry {
id: string; // ULID, client-generated: sortable + unique offline
caseId: string | null;
kind: MutationKind;
/** Hash of the exact payload bytes — the same value goes in the audit log. */
payloadHash: string;
payload: unknown;
/** Device clock at capture, plus a later server correction, not trusted blindly. */
capturedAt: string;
capturedByAgentRef: string;
/** Bumped on every retry; used for conflict reporting, not for ordering. */
attempt: number;
state: 'queued' | 'inflight' | 'acked' | 'rejected' | 'needsHuman';
lastError?: { code: string; messageZh: string };
}
export function enqueue(db: SqliteDb, entry: Omit<OutboxEntry, 'state' | 'attempt'>): OutboxEntry {
const full: OutboxEntry = { ...entry, state: 'queued', attempt: 0 };
db.run(
`INSERT INTO outbox (id, case_id, kind, payload_hash, payload, captured_at, agent_ref, state)
VALUES (?, ?, ?, ?, ?, ?, ?, 'queued')`,
full.id, full.caseId, full.kind, full.payloadHash,
JSON.stringify(full.payload), full.capturedAt, full.capturedByAgentRef,
);
return full;
}
export interface SyncOutcome {
acked: string[];
rejected: Array<{ id: string; code: string; messageZh: string }>;
conflicts: Array<{ id: string; field: string; local: unknown; server: unknown }>;
/** Rate cards the server pushed down while we were syncing up. */
rateCardsReceived: string[];
}
export async function drainOutbox(
db: SqliteDb,
carrier: CarrierConnector,
signal: AbortSignal,
): Promise<SyncOutcome> {
const out: SyncOutcome = { acked: [], rejected: [], conflicts: [], rateCardsReceived: [] };
// Strict FIFO per case_id: two mutations of the same kind must not overtake
// each other, because carriers routinely reject the older one first.
const queue = db
.all<OutboxEntry>(
`SELECT * FROM outbox WHERE state IN ('queued','inflight')
ORDER BY captured_at ASC, id ASC`,
)
.filter((e) => !e.caseId || !queueHasEarlier(db, e));
for (const entry of queue) {
if (signal.aborted) break;
db.run(`UPDATE outbox SET state='inflight', attempt=attempt+1 WHERE id=?`, entry.id);
try {
const res = await carrier.submit(entry);
out.acked.push(entry.id);
db.run(`UPDATE outbox SET state='acked' WHERE id=?`, entry.id);
} catch (err) {
const e = err as CarrierError;
if (e.permanent) {
out.rejected.push({ id: entry.id, code: e.code, messageZh: e.messageZh });
db.run(
`UPDATE outbox SET state='rejected', last_error=? WHERE id=?`,
JSON.stringify({ code: e.code, messageZh: e.messageZh }), entry.id,
);
} else {
// Transient: leave it queued, but surface it to the agent if it is old.
const ageMin = minutesSince(entry.capturedAt);
if (ageMin > 240) {
db.run(`UPDATE outbox SET state='needsHuman' WHERE id=?`, entry.id);
out.rejected.push({
id: entry.id,
code: 'STALE_PENDING',
messageZh: `此項已離線擱置超過 4 小時,請檢查後手動重送`,
});
} else {
db.run(`UPDATE outbox SET state='queued' WHERE id=?`, entry.id);
}
}
}
}
out.rateCardsReceived = await carrier.pullRateCards();
return out;
}
The offline state an agent must be able to read
CREATE TABLE device_sync_state (
device_id TEXT PRIMARY KEY,
agent_ref TEXT NOT NULL,
last_full_sync_at TIMESTAMP,
last_delta_sync_at TIMESTAMP,
rate_card_watermark TEXT NOT NULL, -- e.g. 'PRC-LIFE-2025-03-14T00:00Z'
catalogue_versions TEXT NOT NULL, -- JSON map productCode -> version
pending_evidence INTEGER NOT NULL DEFAULT 0,
blocked_reason_zh TEXT
);
-- The dashboard query behind "我的系統是否最新?" that agents are asked daily.
SELECT
d.device_id,
d.last_full_sync_at,
d.pending_evidence,
CASE
WHEN d.blocked_reason_zh IS NOT NULL THEN d.blocked_reason_zh
WHEN d.last_full_sync_at < datetime('now', '-24 hours')
THEN '超過 24 小時未完整同步,部分產品資料可能過時'
WHEN d.pending_evidence > 0
THEN '尚有 ' || d.pending_evidence || ' 項證明文件待上載'
ELSE '已是最新'
END AS sync_badge_zh
FROM device_sync_state d
WHERE d.agent_ref = :agentRef;
The conflict cases you will actually hit
| Situation | What happens | What the agent must do |
|---|---|---|
| Signed proposal offline, carrier rate card changed before sync | Submission rejected with RATE_CARD_SUPERSEDED | Re-run the quote on the new card; the signed proposal version is archived, not deleted |
| Same client edited on a laptop and on a tablet | Field-level conflict, server wins on KYC fields | Review the conflict card, confirm or discard the device edit |
| Identity document photo captured offline | Stored as evidence, screening not yet run | Do not tell the client "KYC 已完成"; say "文件已收,稍後完成核對" |
| Service request for a policy last synced 5 days ago | POS cannot verify the policy is in a state that permits the change | Route to the carrier's servicing line, log the attempt |
| Quote created offline 3 days before submission | Rate card older than the 48-hour promo threshold | Re-quote; do not submit on an expired promo |
Regulatory Frame: IA, IA Code, SFC/HKMA split
In practice: your client asks whether a savings product is guaranteed. You open the POS product record, which shows the guarantee status, the product's filing reference, and a warning that ILAS products are not deposit-protected — and that wording is versioned with the product.
Who regulates what
Hong Kong's financial system splits insurance regulation across more than one regulator, and the split is not academic — it determines which product you may sell, which disclosures apply, and which conduct rules your agency is held to.
| Regulator | Statutory basis | Scope relevant to this course |
|---|---|---|
| Insurance Authority (IA) 保險業監管局 | Insurance Authority Ordinance (Cap. 117); Insurance Ordinance (Cap. 106) | Authorised insurers and reinsurers; intermediaries; long-term and general insurance; product governance; agent conduct |
| Securities and Futures Commission (SFC) 證監會 | Securities and Futures Ordinance (Cap. 572) | ILAS — insurance-linked securities schemes, which are securities under Hong Kong law; also Type 1/4/9 licences |
| Hong Kong Monetary Authority (HKMA) 金管局 | Monetary Systems Ordinance, Banking Ordinance (Cap. 263) | Insurers that are banks or part of banking groups (general insurance); stored-value facilities; deposit-taking |
| Insurance Authority + MPFA interaction | MPFO (Cap. 431) | MPF schemes, including insurance-related MPF functions |
| Company Registry / IRD | Companies Ordinance (Cap. 622) | Incorporated intermediaries; business registration and disclosure of registered office |
The key practical line: IA regulates the insurance company and the sale; the SFC regulates ILAS as a securities product. An agent selling a savings-linked ILAS policy is making a securities recommendation as well as an insurance recommendation, and the product's fund choices, fee disclosure and risk classification must survive both lenses.
The Code of Conduct as a system requirement
The IA issues a Code of Conduct for Authorized Insurers (and separate ones for authorized reinsurers, insurance brokers, insurance advisers, and captive agents). Treat it not as legal reading but as a requirements catalogue that your POS must be able to evidence.
Operational obligations that translate into system behaviour include, among others:
- Client needs analysis before recommendation. Hence the FNA module (Lesson 05) is not optional; the POS must be able to show that a needs analysis preceded a recommendation.
- Product information and illustration transparency. Hence illustrations are versioned artefacts with assumptions attached (Lesson 03), not "screenshots".
- Pre-contract disclosure. Hence the disclosure step in the application module is a mandatory, signed, timestamped artefact (Lesson 08).
- Cooling-off rights. Hence the POS registers the free-look window per policy (Lesson 10) and surfaces an expiry countdown.
- Complaint handling and records retention. Hence records are retained with agent attribution and are exportable for a regulator's request.
- Suitability for ILAS. Hence the suitability assessment in Lesson 05 must include investment risk appetite and knowledge level.
/**
* Compliance gate. The application module refuses to advance to signature
* unless every applicable obligation has an evidenced artefact.
*/
export interface ComplianceObligation {
id: string;
authority: 'IA' | 'SFC' | 'HKMA' | 'MPFA';
instrument: string;
/** Does this obligation apply to this product x client segment x channel? */
appliesTo(c: CaseContext): boolean;
satisfiedBy: 'artefact' | 'systemAction' | 'attestation';
artefactKinds: string[];
}
export const OBLIGATIONS: ComplianceObligation[] = [
{
id: 'IA.NEEDS_ANALYSIS',
authority: 'IA',
instrument: 'Code of Conduct for Authorized Insurers — needs analysis',
appliesTo: (c) => c.productFamilies.some((f) => f !== 'accident'),
satisfiedBy: 'artefact',
artefactKinds: ['FNA_V1', 'FNA_SIGNED'],
},
{
id: 'IA.ILLUSTRATION',
authority: 'IA',
instrument: 'Code of Conduct — benefit illustrations & assumptions disclosure',
appliesTo: () => true,
satisfiedBy: 'artefact',
artefactKinds: ['ILLUSTRATION_PDF', 'ASSUMPTIONS_SCHEDULE'],
},
{
id: 'IA.PRECONTRACT_DISCLOSURE',
authority: 'IA',
instrument: 'Insurance Ordinance (Cap.106) — disclosure before contract',
appliesTo: () => true,
satisfiedBy: 'systemAction',
artefactKinds: ['DISCLOSURE_ACKNOWLEDGEMENT'],
},
{
id: 'IA.COOLING_OFF',
authority: 'IA',
instrument: 'Code of Conduct — cooling-off period for individual life policies',
appliesTo: (c) => c.channel === 'face-to-face' && c.productFamilies.includes('life'),
satisfiedBy: 'systemAction',
artefactKinds: ['FREE_LOOK_REGISTRATION'],
},
{
id: 'SFC.ILAS_SUITABILITY',
authority: 'SFC',
instrument: 'Code on Unit Trusts and Mutual Funds / ILAS product governance',
appliesTo: (c) => c.productFamilies.includes('ILAS'),
satisfiedBy: 'artefact',
artefactKinds: ['ILAS_SUITABILITY_ASSESSMENT', 'FUND_CHOICE_RECORD'],
},
{
id: 'AML.CDD',
authority: 'IA',
instrument: 'Guideline on Anti-Money Laundering and Counter-Terrorist Financing for Insurers',
appliesTo: () => true,
satisfiedBy: 'artefact',
artefactKinds: ['KYC_PACKET', 'SANCTIONS_SCREEN', 'BENEFICIAL_OWNER_DECL'],
},
];
export function unmetObligations(ctx: CaseContext): ComplianceObligation[] {
const present = new Set(ctx.artifacts.map((a) => a.kind));
return OBLIGATIONS.filter(
(o) => o.appliesTo(ctx) && !o.artefactKinds.some((k) => present.has(k)),
);
}
Why "which regulator" changes the system, not just the paperwork
Three concrete differences that show up in the POS:
- Age band and investment-risk disclosure. For ILAS, the SFC lens adds an explicit investment risk profile step that an ordinary medical product does not have. The POS shows a different questionnaire, and a different sign-off language.
- Guarantee and capital-at-risk wording. ILAS benefits are not guaranteed and are not deposits; the product record must carry that classification and the UI must render the warning on the illustration, not only in the PDF.
- Complaint escalation path. IA-regulated complaints and SFC-regulated complaints follow different routes. A CRM module that files one complaint type with the wrong route is a compliance defect.
The record you must be able to produce
If the IA, the carrier, or a client's solicitor asks "what exactly happened in this sale, and who authorised what", the POS must be able to reconstruct:
| Question | Answered from |
|---|---|
| What product version was quoted, and on what rate card? | quote.rate_card_version, quote.product_version |
| What did the agent show the client? | Illustration artefact hash + render log |
| What did the client disclose about health? | Proposal health section, as captured, with per-field timestamps |
| Which disclosure version did the client sign? | DISCLOSURE_ACKNOWLEDGEMENT.artefact_version |
| Who was logged in? | Agent ref + device id + session token on every mutation |
| Was this agent authorised for this product at that moment? | agency_entitlement snapshot stamped onto the case |
| What did the POS say at 14:32 on that date? | Append-only audit log (Lesson 08) |
That last row is why the POS's own logs are regulated artefacts and why "we only keep 90 days of logs" is not an acceptable storage policy in this domain.
Who Logs In: Roles and Entitlements
In practice: a trainee agent signs into the POS on the agency tablet and the quotation module is visible but greyed out, with a single line of grey text explaining that appointment is a three-month process away.
Authorship in a POS is not the same as "who is logged in". A real Hong Kong agency POS has at least four distinct principals, and the entitlement set attached to a case is stamped at case creation and never re-derived later.
| Role | 中文 | Can create a case | Can submit to a carrier | Can see commission | Can approve exceptions |
|---|---|---|---|---|---|
| Trainee agent | 見習代理 | Yes (draft only) | No — requires a qualified agent co-sign | No, aggregate only | No |
| Qualified agent | 正式代理 | Yes | Yes, for entitled carriers | Yes, own book | No |
| Agency compliance officer | 合規主任 | No | No | Yes, whole agency | Yes |
| Agency principal / compliance officer | 總監 / 合規主管 | Yes | Yes | Yes | Yes, incl. four-eyes escalations |
| Carrier underwriter (in POS as limited user) | 核保員 | No | n/a | No | Decides only, in the carrier system |
| POS platform auditor (read-only, tenant-level) | 系統審計 | No | No | No | No, read-only export only |
Two consequences that show up in real incidents:
- Entitlement is time-varying and case-scoped. A trainee becomes a qualified agent; a qualified agent leaves the agency; an agent's appointment with a specific carrier is suspended for compliance reasons. A case submitted under an entitlement that was valid at submission time stays valid; one submitted after suspension must be blocked.
- Commission visibility is a privilege, not a data leak. Showing a trainee aggregate commission is a feature; showing a peer agent's individual case premium is a data protection incident.
export interface Entitlement {
agencyRef: string;
agentRef: string;
role: 'trainee' | 'agent' | 'compliance' | 'principal' | 'auditor';
/** Appointments are per carrier and are themselves time-bounded. */
carrierAppointments: Array<{
carrierCode: string;
validFrom: string;
validTo: string | null;
/** Some carriers are appointed for read-only advice but not for submission. */
canSubmit: boolean;
}>;
modules: PosModule['id'][];
visibility: 'own_cases' | 'agency_cases' | 'tenant_readonly';
}
export interface CaseEntitlementSnapshot {
/** Immutable copy of the Entitlement as it stood when the case was created. */
frozen: Entitlement;
frozenAt: string;
frozenBy: string;
}
export function canSubmitCase(
snap: CaseEntitlementSnapshot,
carrierCode: string,
at: Date,
): { allowed: boolean; reasonZh?: string } {
if (snap.frozen.role === 'trainee') {
return { allowed: false, reasonZh: '見習代理不可向保險公司提交個案,需由正式代理共同簽署' };
}
const appt = snap.frozen.carrierAppointments.find((a) => a.carrierCode === carrierCode);
if (!appt) return { allowed: false, reasonZh: '未有該保險公司的委任' };
if (!appt.canSubmit) return { allowed: false, reasonZh: '此委任只限提供意見,不可提交投保' };
const from = new Date(appt.validFrom);
const to = appt.validTo ? new Date(appt.validTo) : null;
if (at < from || (to && at > to)) {
return { allowed: false, reasonZh: '此委任在提交當日並未生效' };
}
return { allowed: true };
}
-- The audit query a compliance officer runs when asked: "which cases did this
-- agent submit while under review, and on what evidence?"
CREATE TABLE case_entitlement_snapshot (
case_id TEXT PRIMARY KEY,
agent_ref TEXT NOT NULL,
role_at_creation TEXT NOT NULL,
frozen_json TEXT NOT NULL,
frozen_at TIMESTAMP NOT NULL
);
CREATE TABLE entitlement_change_event (
id TEXT PRIMARY KEY,
agent_ref TEXT NOT NULL,
carrier_code TEXT,
change_type TEXT NOT NULL, -- GRANTED | SUSPENDED | EXPIRED | REVOKED
effective_from TIMESTAMP NOT NULL,
effective_to TIMESTAMP,
authorised_by TEXT NOT NULL,
reason_zh TEXT NOT NULL
);
SELECT
s.case_id,
s.agent_ref,
s.frozen_at,
GROUP_CONCAT(c.change_type || '@' || c.effective_from) AS entitlement_changes_after_freeze
FROM case_entitlement_snapshot s
LEFT JOIN entitlement_change_event c
ON c.agent_ref = s.agent_ref
AND c.carrier_code = :carrierCode
AND c.effective_from > s.frozen_at
WHERE s.agent_ref = :agentRef
GROUP BY s.case_id
HAVING entitlement_changes_after_freeze LIKE '%SUSPENDED%'
OR entitlement_changes_after_freeze LIKE '%REVOKED%';
One Case, End to End
In practice: you can follow a single Hong Kong case across all seven modules using the identifiers in this table — and when a client asks "what exactly happened?", you read the table, not your memory.
This is the trace a senior advisor should be able to reconstruct from memory, because "I handled it last March" is not evidence. This particular case is deliberately ordinary: a 38-year-old office manager, non-smoker, buying a savings-participating whole life plan plus a rider, sold by a general agent through a bank-owned agency, quoted against four carriers.
| # | Stage | Module | Record created | What the POS asserted | What the carrier asserted |
|---|---|---|---|---|---|
| 1 | Meeting | CRM | CL-20881 (client upserted, merged on HKID hash) | Client identity as declared | — |
| 2 | Needs analysis | CRM → Quoting | FNA-20881-V1 | Budget HK$9,000/mo, 2 dependants, 25-year horizon | — |
| 3 | Shortlist | Catalogue | SEED-4471 (5 product versions pinned) | Which versions were on the shelf today | — |
| 4 | Quote A | Quoting | QT-20881-A (rate card PRC-LIFE-2025-03-14) | Gross 41,880 / disc 35% = 27,222 | — |
| 5 | Quote B | Quoting | QT-20881-B | Gross 38,400 / disc 22% = 29,952 | — |
| 6 | Quote C | Quoting | QT-20881-C | Referral required — occupation class 4 | — |
| 7 | Quote D | Quoting | QT-20881-D | Loading 35% applied, medical req. | — |
| 8 | Comparison | Quoting | CMP-20881 (4 rows, normalised benefit definitions) | Which definition each number assumes | — |
| 9 | Recommendation | Quoting → Application | REC-20881 (linked to CMP-20881) | Why A over B on persistency + riders | — |
| 10 | Proposal | Application | PR-20881-V1 + illustration ILL-7710 | Assumptions printed on the page the client saw | — |
| 11 | Disclosure | Application | DISC-2025.03 acknowledged 11:42 | Disclosure version in force at that timestamp | — |
| 12 | KYC | KYC | KYC-20881 (HKID OCR 98.2%, liveness pass, sanctions clear) | Screening performed at 11:51 | — |
| 13 | Signature | Application | SIG-20881 (hash-chained, device TBLT-07) | Client signed proposal and disclosure | — |
| 14 | Submit | Application | SUB-20881 → APP/PRC/2025/118203 | Payload hash 9f2c…, agent entitlement snapshot attached | Application accepted 13:07 |
| 15 | Underwriting | Application (read-only) | UW_STATUS poll | Underwriting pending | Additional medical evidence requested |
| 16 | Decision | Application (read-only) | LOADING_APPLIED 35% | "Carrier applying a loading — discuss with client" | Standard terms with loading |
| 17 | Re-quote | Quoting → Application | QT-20881-E | New illustration reflecting the loading | — |
| 18 | Re-submit | Application | SUB-20881-2 → APP/PRC/2025/118203-R1 | Revised case, both versions retained | Accepted |
| 19 | Issue | Servicing (read-only) | POL/PRC/2025/884417 | Policy in force date 2025-04-02 | Sum assured, exclusions, riders |
| 20 | First premium | Servicing (read-only) | BILL-01 | "Awaiting first premium — due 2025-04-02" | Collected 2025-03-29, USD debit |
| 21 | Cooling-off | Servicing | FL-884417 expiring 2025-05-02 | Countdown shown to agent and client | — |
| 22 | Renewal task | CRM | TASK-RENEW-884417 | Generated from carrier status at T+30 | — |
| 23 | Claim (year 2) | Claims | CLM-2026-44120 | Notification logged 2026-02-11 19:40 | Adjudication, settlement 2026-04-08 |
| 24 | Commission | CRM (mirror) | FYC-EST-884417 | Estimate only | First year commission statement issued |
Three things to notice in that trace:
- Step 18 creates a new submission, it does not overwrite step 14. The revised case references the original. A carrier or regulator asking "was the client ever quoted without the loading?" is answered with a version reference, not a reconstructed memory.
- Step 16 is where agent skill shows. The POS could not decide the loading, but it could and did stop the file from advancing silently. An agent who ignores the flag and submits anyway has created a mis-selling exposure.
- Step 24 is the only one where the POS label says "estimate" out loud. Everything else in this table has a firm answer except that one.
Key Takeaways
In practice: these are the nine sentences worth being able to say out loud on your first day in front of a client and a carrier.
- An insurance POS is an agent-facing production console, not a consumer app. Its unit of work is a case file, not a transaction; it does not take the premium — it takes the application.
- The POS is the system of record for the sales journey; the carrier is the system of record for the policy lifecycle. The handover is the submission acknowledgement. Everything before it lives in your POS; everything after it is a read-only projection.
- Hong Kong needed a consolidated POS because of fragmentation, not because carriers lacked systems. Many carriers with incompatible protocols, an offline-first field force, an unusually mixed client population, and an IA-regulated perimeter where the application file is evidence.
- Seven modules make up a POS: catalogue, quoting, application, KYC, servicing, claims, CRM. Learn to recognise them by the record prefix (
CL-,QT-,PR-,AP-,SV-) as much as by the menu — and remember that entitlement, not login, is what authorises a submission. - Offline-first is a requirement, not an optimisation. Quoting, comparison and signature capture must work with no signal; screening and final submission must refuse rather than silently queue.
- Every mutation goes through an append-only outbox with a payload hash, and sync is strict FIFO per case. Two changes of the same kind must not overtake each other, because carriers reject the older one first.
- Carrier-sourced data must always carry a freshness badge. A policy screen without "synced 3 days ago" on it is a liability, and the projection view in this lesson is the pattern to insist on.
- The IA regulates the insurer and the sale; the SFC regulates ILAS as a securities product; the HKMA covers bank-owned general insurers. That split changes the disclosures, the suitability step and the complaint route — not just the paperwork.
- Treat the IA Code of Conduct as a requirements catalogue. Every obligation maps to an artefact the system can produce on demand: needs analysis, illustration and assumptions, pre-contract disclosure, cooling-off registration, sanctions screen, agent entitlement snapshot.**
Glossary (zh-Hant-HK)
| Term | 中文 | Note |
|---|---|---|
| Point-of-Sale | 銷售點系統 | In insurance: the agent's production console |
| System of record | 權威記錄系統 | The system that owns a given fact |
| Proposal | 建議書 | The agent's formal recommendation to the client |
| Application | 投保書 | The client's application for cover |
| Underwriting | 核保 | Assessing risk and deciding terms |
| Authorised insurer | 獲認可保險公司 | Licensed by the IA |
| Insurance intermediary | 保險中介人 | Agents, brokers, advisers |
| Electronic KYC | 電子客戶身分識別 | Document OCR, liveness, screening |
| ILAS | 投資壽險 | Insurance-linked securities scheme |
| Persistence / Persistency | 保單續期率 | Share of policies still in force at year N |
| First year commission (FYC) | 首年佣金 | Paid on first-year premium |
| Cooling-off period | 冷靜期 | Right to cancel after taking out cover |
| Claim | 理賠 | A demand for payment under a policy |
| Endorsement | 批註 | A change to a live policy |
| Lapse | 失效 | Policy that stopped paying premium |
| Underwrite | 承保 | Accept risk and issue the contract |