Two Tables, One Idempotency Key: How InsightUW Adds `policy_bound` and `policy_issued` PAS Pushes Without Forking the Dispatch Service
From "the bind happened, then someone manually pushed bound to PAS, then someone else manually pushed issued, then PAS got both messages with no idempotency key, then the next quarterly reconciliation found 47 duplicate policy records, three of which had drifted because someone retried the push with an edited premium" to "cap #3 _bind() automatically queues policy_bound; the per-LOB issuance trigger decides when policy_issued fires (manual / on_bound / on_binder_delivered); the idempotency key sha256(connector|policy|message_type|version) means PAS dedupes; the ack reconciler runs every 3 minutes and flips Policy.is_bound / is_issued when send_status='sent' lands" — through two new wrapper functions, ten new columns on Policy, and the deliberate refusal to build a parallel PAS pipeline for "lifecycle" messages.
The Problem
The bind decision happens in the workstation. The policy lifecycle has to land in the policy admin system (PAS) — policy admin systems, whatever. There are two distinct lifecycle events:
- Bound — coverage is in force. PAS marks the policy bound; downstream billing / sub-ledger / claims systems can now respond to the policy id.
- Issued — the formal policy document is finalized in the cedent's books. Sometimes this is simultaneous with bound; sometimes it happens after binder delivery, validation sheets, subjectivity clearance.
In some shops these are one event; in others, they're two with a multi-day gap. The workstation has to support both flows.
The complications PAS pushes introduce:
- Network failures, timeouts, and eventual consistency. PAS endpoints can be slow, can return 503, can swallow a request and never ack. Retries have to be idempotent or PAS gets duplicate policy records.
- Two different events on the same policy. Bound and issued can't share an idempotency key — they'd dedupe each other.
- Retry semantics. A failed push needs to retry, but each retry has to look like a new attempt to the workstation (so audit is clear) while still being deduplicable on PAS's end (against policy guid).
- Lifecycle visibility. The UW needs to see "bound: pending PAS confirmation," "issued: failed — retry available," not raw outbox internals.
The usual fixes don't fix:
- Build a parallel PAS pipeline for "lifecycle messages." Quote cap #8 already shipped Pas Message, Pas Outbox,
pas_adapters/(policy admin system, policy admin system, generic REST), idempotency keys, retry with backoff, dead-letter. Cap #7 doesn't need new pipes; it needs new message types in the existing pipes. - One message type for "lifecycle" with a status field. Then PAS dedupes against a stale "lifecycle" key when bound and issued are different events. Wrong.
- Synchronous push at bind time. PAS can be slow. Holding the bind transaction until PAS acks introduces a 2-second-to-30-second window where the bind can fail because PAS is degraded.
- No version bump on retry. PAS can't distinguish a retry from a duplicate; the workstation can't track "we tried 3 times before it landed."
The root cause: the bind/issued lifecycle is two events through the same channel. Reuse the channel; differentiate the events; bump version on retry; reconcile asynchronously.
The InsightUW Approach
Two new wrapper functions in the existing PAS dispatch service; ten new columns on Policy; one ack-reconciler sweeper; one per-LOB issuance trigger config.
Two new wrappers — same dispatch service
Quote cap #8 already had queue policy create (general policy push) plus queue amendment push. Cap #7 adds two more thin wrappers in the same file:
Both use the existing queue push private function — same idempotency key generation, same connector resolution, same field-mapping logic, same Pas Message / Pas Outbox row insertion. Cap #7 is two extra functions in the dispatch service file plus the lifecycle service that orchestrates them.
Idempotency key embeds version + message type
The standard scheme:
Cap #7's policy bound and policy issued are different message type strings, so they can't collide. Two events on the same policy → two distinct keys.
Within a message type, retries bump version. The lifecycle service computes the next version by counting prior messages of that type:
The first push is version=1; a retry after terminal failure (manual via UI Retry button) is version=2 with bump_version=True. PAS sees a different idempotency key for each retry, but is expected to dedupe on its end against policy guid — the workstation makes the retry visible to PAS as "new attempt" while PAS owns "have I already seen this policy?"
Three issuance triggers, one config table
Issuance Config is per-LOB:
The lifecycle service auto push on bind always queues policy bound. If the LOB's trigger is on bound, it then queues policy issued with auto-generated notes ("Auto-issued on bound per LOB policy."). Otherwise issued sits idle until either cap #6 binder-delivered (if trigger=on binder delivered) or UW manual click (if trigger=manual).
auto push on binder delivered is called from cap #6's binder delivery sweeper; it short-circuits unless trigger is on binder delivered.
Manual push: UW clicks Mark issued on the policy-lifecycle-panel, types issuance notes (mandatory on first push), submits. Service runs push issued which checks is bound is true (or bound pas pushed at is set as a softer gate — bound is queued even if PAS hasn't ack'd yet).
Ack reconciler — flip the flags asynchronously
The bind transaction commits with bound queued, not bound-acknowledged. The ack reconciler runs every 3 minutes:
Two things flow downstream automatically:
- On is_bound=true ack: just an audit + notification (UW knows PAS confirmed).
- On is_issued=true ack: fires cap #8 hook to auto mark_booking_complete — PAS-confirmed issued is the canonical "this booking is done" signal; the validation/booking cadence reminders auto-cancel.
UI: two-row lifecycle panel with status chips
policy-lifecycle-panel on bind-request-detail (gated on resulting policy guid) shows two rows:
When pushed but not yet acknowledged: chip is amber "PAS bound — pending." When the message is in failed/dead letter: chip is red "PAS bound — failed" with a Retry button (which calls push bound). When manual trigger and not yet pushed: shows "Mark issued" button that opens the issuance-notes modal.
Worked Example: Acme Manufacturing $25M Property, Manual Issuance
P-2026-A8F4C2 was bound 2026-04-26 17:14 by sarah. Property LOB has no Issuance Config row → defaults to manual. Workflow:
Step 1 — Bind transaction commits with bound queued
Inside bind after Policy creation + Bind Request stamp:
Service:
1. Calls push bound → queue policy bound → queue push:
- Connector resolved for Property LOB = "Policy System A" connector.
- Idempotency key = sha256
- No existing Pas Message with that key → insert new.
- Pas Message(message_type='policy_bound', send_status='queued', payload_json={...}).
- Pas Outbox(send_status='pending', next_try_at=now).
2. Policy stamped: bound_pas_message_guid=<msg>, bound_pas_pushed_at=now. is bound stays false.
3. Resolves Issuance Config for lob='property' → no row → trigger='manual' (default) → auto push on bind does NOT queue policy_issued.
4. Commit.
Step 2 — PAS outbox worker dispatches
30 seconds later the existing PAS worker picks up the row, routes to the policy admin adapter:
policy admin endpoint posts the bound payload, returns 200 with provider_record_id. Worker sets Pas Message.send_status='sent', provider_record_id=<dragon-policy-id>, provider_response={...}.
Step 3 — Ack reconciler flips is_bound
3 minutes later the lifecycle sweeper runs:
Sarah's lifecycle-panel chip flips from amber "PAS bound — pending" to green "PAS bound ✓ pushed 17:14 · ack 17:17."
Step 4 — Three weeks later: validation sheet ready, manager signs off, UW marks issued
Cap #8 cadence reminders surfaced "Prepare validation sheet" and "Schedule booking" earlier; AC drove both to mark-done. UW Sarah is ready to push issued.
She clicks Mark issued on the policy-lifecycle-panel. Modal: "Notes are required on the first issuance push (regulator audit)." She types:
Submit. Service:
1. push issued:
- Verifies is_bound or bound_pas_pushed_at ✓
- Verifies issuance_notes present (first push) ✓
- version = count(prior policy_issued for this policy) + 1 = 1
- idempotency_key = sha256("dragon-conn|pol-a8f4c2|policy_issued|1")
- Insert Pas Message + Pas Outbox.
2. Policy stamped: issued pas message guid, issued_pas_pushed_at=now, issued_by='sarah', issuance_notes=<text>.
3. Audit + notification.
4. Commit.
Step 5 — Worker dispatches, sweeper acks, cap #8 hook fires
30s later: worker dispatches policy_issued via policy admin adapter, send_status='sent'.
3 min later: lifecycle sweeper:
Cap #8 hook auto-fires mark booking complete on the bind request. Bind Request gets booking_completed_at=now, booking_completed_by='system'. Any remaining cadence reminders for this bind (validation_sheet_prep / booking_scheduled / booking_complete) auto-cancel with reason auto: booking_complete.
Sarah's lifecycle-panel now shows:
The bind workflow is fully closed. PAS has the bound + issued flags. Cap #8 cadence is auto-completed via the issued-ack hook.
Step 6 — Hypothetical retry path
If Step 4's push had landed on a policy admin endpoint that was returning 503, the worker would have retried per existing backoff (3 attempts over 15 min); after max retries send_status='dead_letter'.
Sarah's lifecycle-panel chip would flip to red "PAS issued — failed." She clicks Retry issued. Service:
- push issued:
- version = count(prior policy_issued) + 1 = 2
- idempotency_key = sha256("dragon-conn|pol-a8f4c2|policy_issued|2") — DIFFERENT from version=1
- Insert NEW Pas Message (the prior failed message stays for audit).
- Policy.issued_pas_message_guid points at the new message guid.
- issued_pas_pushed_at = now.
PAS receives a fresh push; PAS-side dedupe logic (against policy guid) recognizes this is a retry of the same policy issuance and either acks immediately or processes as new — depending on PAS's own semantics. The workstation doesn't care; it stamps ack on its end when send_status flips to 'sent'.
What This Means for Underwriters
-
Two events, two message types, two idempotency keys. policy bound and policy issued never collide. Each has its own retry path with version bumping.
-
Quote-cap-#8 PAS infrastructure does the heavy lifting. Cap #7 is two new wrapper functions (queue policy bound, queue policy issued) + ten new columns on Policy + one ack-reconciler sweeper. No new pipes.
-
Per-LOB issuance trigger is configurable.
manual(default), on bound (auto same-transaction), or on binder delivered (auto when cap #6 binder advances). Different LOBs can have different policies. -
Async ack reconciliation. PAS confirmation lands within 3 minutes via the sweeper; UI shows pending → confirmed transition automatically.
-
First-issuance-notes mandatory. Regulators want documented "why this policy was issued today, by whom, against what." Empty notes on first push → 400. Retries can re-use prior notes.
-
Retry bumps version, preserves audit. The old failed Pas Message stays for audit; the new attempt is a new row with version+1. The chain is visible: "we tried 3 times before it landed."
-
Cap #8 booking-complete fires on issued-ack. PAS-confirmed issued is the canonical signal that the policy is fully booked. The validation/booking cadence auto-completes; cadence reminders auto-cancel. UW doesn't have to remember to mark-done.
-
No webhook receiver for PAS responses. The outbox worker writes send status and provider record id; the lifecycle reconciler reads it. Production-grade webhooks can be added if a PAS provider demands sub-3-min reconciliation.
-
Three timestamps, distinct semantics. bound pas pushed at (we queued), bound pas ack at (PAS confirmed), bound at on Bind Request (the bind decision itself). Each answers a different audit question.
What's Next
Want to see how two new wrappers + ten new columns + one 3-minute sweeper add policy_bound and policy_issued PAS pushes to your underwriting workstation without forking the dispatch service? Request a demo.