BillingOne Integration
PolicyOne publishes policy lifecycle events to BillingOne over the shared events:policy Redis Stream. BillingOne subscribes and reacts — creating billing accounts, activating payment schedules, and managing renewals without any direct API calls between the two systems.
The Big Picture
What Triggers an Event?
| Event | Fired By | BillingOne Action |
|---|---|---|
policy.bound | bindQuoteToPolicy() in DB transaction | Create billing account, apply payment plan |
policy.issued | issuePolicyRecord() in DB transaction | Activate payment schedule, send first invoice |
policy.cancelled | (planned) | Stop future charges, calculate refund |
policy.renewed | (planned) | Create new billing period |
Key Design Decisions
Outbox Pattern — Why?
The policy.bound event is written to policy.event_outbox inside the same PostgreSQL transaction as the policy bind. This means:
- If the bind transaction rolls back → no event is emitted. ✅
- If the server crashes after commit but before publishing to Redis → the
OutboxPublisherpicks it up on next boot. ✅ - No dual-write problem. ✅
No Direct API Calls
BillingOne never calls PolicyOne's API to confirm payment eligibility or fetch billing details. All data needed to create the billing account travels inside the event payload — billing.paymentPlanCode, billing.billingMethod, quote.premium.totalPremium. This keeps the two systems fully decoupled.
Agent Pre-Selects the Plan
Before bind, the agent navigates to the Payment Plan wizard step and selects:
- A payment plan (e.g.
quarterly,monthly-12,pif) from BillingOne's catalog - A billing method (
direct_bill,agency_bill,mortgage_bill) - Optional discount codes
These are saved to policy.quote_billing and attached to the policy.bound event at bind time.
Component Index
| Component | Location | Role |
|---|---|---|
PaymentPlanPanel | app/(shell)/submissions/new/PaymentPlanPanel.tsx | Agent UI for plan selection |
| BillingOne client | lib/integration/billingone/client.ts | HTTP client: getPaymentPlans, previewPaymentSchedule |
| Billing event schema | lib/integration/queue/billingone/policy-bound.schema.json | Canonical event shape |
saveQuotePaymentPlanAction | lib/actions/submissions.ts | Saves plan to policy.quote_billing |
bindQuoteToPolicy() | packages/db/src/queries/policy.ts | Writes policy row + outbox event atomically |
OutboxPublisher | apps/policyone/lib/workers/outbox-publisher.ts | Polls outbox → XADD events:policy |
PolicyBoundPayload | packages/events/src/policy-events.ts | TypeScript event payload type |
PolicyEvent<T> envelope | packages/events/src/envelope.ts | Standard wrapper around every event |
STREAM_KEYS.POLICY_EVENTS | packages/queue/src/stream-keys.ts | "events:policy" constant |