Skip to content

02 — Code Updates / Migration Plan

Stripe becomes the billing source-of-truth. Adventive retires the custom rollup job, custom billing calculations, and custom retry/dunning logic. Adventive keeps the custom invoice PDF renderer and Mailgun delivery path, reimplemented as Cloudflare Workers. Acodei → QuickBooks sync continues unchanged.

adventive-billing-worker/ (TypeScript / Hono on Cloudflare Workers)
├── src/
│ ├── index.ts # router + health
│ ├── webhooks/
│ │ ├── stripe.ts # signature verification, event dispatch
│ │ └── handlers/
│ │ ├── invoice-finalized.ts # pulls full invoice → renderer → Mailgun
│ │ ├── invoice-paid.ts
│ │ ├── payment-failed.ts
│ │ └── subscription-updated.ts
│ ├── usage/
│ │ └── report.ts # accepts Adventive product events → Stripe usage records
│ ├── render/
│ │ ├── templates/ # Handlebars, migrated from legacy admin
│ │ └── pdf.ts # Browser Rendering API OR external service
│ ├── email/
│ │ └── mailgun.ts # invoice email send
│ └── stripe/
│ └── client.ts
├── wrangler.toml # Cron Triggers for reconciliation
└── test/

This Worker runs alongside the admin-api Worker but has a distinct responsibility. It does not serve operator UI traffic — it serves Stripe webhooks, internal metering events, and scheduled reconciliation jobs.

Adventive events → internal store → month-end rollup → Stripe Invoice API
Custom PDF gen
Mailgun
Adventive events → Billing Worker /usage → Stripe InvoiceItem (immediate)
Stripe finalize + webhook
Billing Worker render
Mailgun

Stripe is in the middle of every billable event, not just at month-end.

Phase A — Viability Assessment (mostly docs)

Section titled “Phase A — Viability Assessment (mostly docs)”
StepOutput
A.1Inventory current pricing models (from adventive-admin repo + Stripe dashboard)
A.2Map each pricing model to Stripe Product / Price constructs
A.3Enumerate non-standard arrangements → per-customer migration notes
A.4Cost model: current run-cost vs. post-migration run-cost
A.5Risk register with mitigations
A.6Go / no-go sign-off (Jeffrey + Patrick)
StepOutputGate
B.1Scaffold billing-worker repo. Implement Stripe webhook receiver with signature verification, idempotency, DLQ.Webhook green in Stripe test mode
B.2Port Handlebars invoice template. Implement PDF renderer (leading: Cloudflare Browser Rendering). Attach to test-mode invoices.Test-mode invoice PDFs match current design
B.3Implement Mailgun send path for invoice emails.Test customers receive Adventive-branded PDFs
B.4Implement /usage endpoint for metering events. Backfill event emission in Adventive product.Metered usage flows end-to-end in test mode
B.5Migrate pricing catalog: create Stripe Product and Price for each current pricing model.Catalog parity review
B.6Migrate first cohort (low-risk, flat-pricing customers). Stripe live mode. Dual-run reconciliation for one billing cycle.Reconciliation clean
B.7Migrate subsequent cohorts (tiered, metered, custom) — one cohort per billing cycle.Reconciliation clean per cohort
B.8All customers on Stripe as source-of-truth. Retire rollup job + billing calculation code + any custom dunning logic from CodeIgniter admin (Stripe Billing dunning takes over).Stripe owns 100% billing
B.9Optional: enable Stripe Customer Portal for self-service.Customer comms complete
FilePurpose
src/index.tsHono app, routes /webhook/stripe, /usage, /admin/resend-invoice
src/webhooks/stripe.tsSignature verification + event dispatch
src/webhooks/handlers/invoice-finalized.tsPrimary invoice-presentation handler
src/render/templates/invoice.hbsMigrated from legacy admin
src/render/pdf.tsHTML → PDF rendering
src/email/mailgun.tsMailgun API client wrapper
src/usage/report.tsIdempotent metering event ingestion
src/reconcile/daily.tsCron-triggered reconciliation against Stripe

Retired from adventive-admin (CodeIgniter) — after B.8

Section titled “Retired from adventive-admin (CodeIgniter) — after B.8”
FileFunctions retiredPhase
application/controllers/Billing.phpgenerateInvoice() — operator trigger → billing_service; processPayment() — payment trigger; sendInvoiceReminder() — 6-step manual dunning; emailInvoice() — batch dunning trigger; _getInvoiceFile() — PDF download from CDN to /tmpB.3
application/models/Billing_model.phpgetBilling(), getRefunds(), getRemittance(), getRevenueHistory() (all proxy billing_service); getSubscriptionInformation() (queries account_plan_sub); getAccountUsagePrices() (queries account_plan_usage for tiered pricing)B.3–B.4
application/libraries/Mailgun.php (billing send paths only)sendTemplatedEmail() calls for invoice_new, invoice_notify, invoice_pastdue — billing send paths relocated to billing Worker; library retained for non-billing emailB.3

Retired from billing_service (separate internal API) — after B.8

Section titled “Retired from billing_service (separate internal API) — after B.8”
ComponentDescriptionPhase
Month-end rollup jobAggregates billable items per customer; triggers Stripe Invoice creationB.3
Stripe API calls (create Invoice, InvoiceItem, etc.)All Stripe API interactions move to billing WorkerB.3
Custom invoice PDF rendererPHP PDF library (tech stack not analyzable from admin repo); replaced by Cloudflare Browser Rendering in billing WorkerB.4
PDF S3 storage (billing.adventivecdn.com)CloudFront CDN delivery of invoice PDFs; replaced by R2 + Cloudflare servingB.4

Retired DB tables (decommissioned as source-of-truth) — after phase noted

Section titled “Retired DB tables (decommissioned as source-of-truth) — after phase noted”
TableCurrent rolePhase
account_plan_subFlat subscription fee per accountB.3 (read-only during migration; retired as SoR when Stripe Subscription is authoritative)
account_plan_usageTiered impression pricing tiers per planB.4 (read-only during migration; retired as SoR when Stripe metered Prices are authoritative)
billing_invoiceInvoice history (14 years)Retained read-only post-cutover; export to R2 / archive before full decommission

Current pricing models → Stripe constructs

Section titled “Current pricing models → Stripe constructs”
Adventive ModelDB SourceStripe ProductStripe Price TypePhase
Flat monthly subscription feeaccount_plan_sub.sub_rateAccount Charges prod_TvOSKLTnPZEzkrrecurring, interval=month, usage_type=licensed, billing_scheme=per_unitB.5
ST impression tiersaccount_plan_usage (usage_type=ST)Usage prod_TvORrZbbLw4cOcrecurring, usage_type=metered, billing_scheme=tiered, tiers_mode=graduatedB.5
RM impression tiersaccount_plan_usage (usage_type=RM)Usage — separate Pricerecurring, usage_type=metered, billing_scheme=tiered, tiers_mode=graduatedB.5
CV impression tiersaccount_plan_usage (usage_type=CV)Usage — separate Pricerecurring, usage_type=metered, billing_scheme=tiered, tiers_mode=graduatedB.5
Managed service line itemsManagedService_model.php per-job amountsManaged Services prod_TvOQzylfxDc5v7InvoiceItem (one-off, attached before invoice finalization)B.5
Custom / bespoke per-accountManual InvoiceItem in billing_serviceAccount Charges or ad-hocManual InvoiceItem added pre-finalizationB.6

Price objects to create (net-new in Stripe)

Section titled “Price objects to create (net-new in Stripe)”

The 5 existing Stripe Prices are all type=one_time and unsuitable for subscription billing. New Prices must be created:

  1. 1 × recurring licensed Price — flat subscription (one Price per plan tier, or unit_amount set per-subscription). Maps to account_plan_sub.sub_rate.
  2. 3 × recurring metered graduated Prices — one each for ST, RM, CV impression types. Each Price encodes the tier thresholds from account_plan_usage.
  3. No standing Price for managed services — InvoiceItem created at billing time with the exact job amount.
  4. Existing one-time Prices archived after all customers are migrated off them (B.5 → B.7).

Non-standard arrangements (flag for individualized migration)

Section titled “Non-standard arrangements (flag for individualized migration)”

Confirmed from repo analysis: the mix of pricing models includes custom per-account arrangements handled as manual InvoiceItems in billing_service. These must be inventoried per-customer from the Stripe invoice history and billing_invoice DB before B.6. Each requires a migration note in the cohort runbook (see Chapter 03).

  • Source: Redshift impression counts, queried by billing_service via Pdo_model
  • Event shape: {account_id, period (YYYY-MM), usage_type (ST|RM|CV), quantity}
  • Idempotency key: {account_id}:{period}:{usage_type} — prevents double-counting on retry
  • Target: Billing Worker POST /usage → Stripe Usage Record API (Meter or legacy create_usage_record)
  • Frequency: Can be as-accrued (daily push from stats pipeline) or end-of-period summary — either works with metered Prices; daily push reduces cutover risk
  • Added: stripe (Workers-compatible), handlebars (or a Workers-compatible fork), PDF rendering lib or Cloudflare Browser Rendering binding, mailgun.js or HTTP client.
  • Removed (end of B.8): legacy rollup job, billing calculation code, PHP PDF renderer, any custom dunning scheduler.
  • Invoice email sender: stays as Adventive (via Mailgun) — no customer-visible change if routed through existing billing@ address.
  • Invoice PDF design: unchanged (same Handlebars template, re-rendered).
  • Invoice cadence: should remain the same per-customer (Stripe billing cycles configured to match current cycles).
  • Self-service portal: only visible if Customer Portal is enabled in step B.9 — explicit comms needed.
  • Unit: Vitest for handler logic, signature verification, idempotency keys, RBAC on /admin/* endpoints.
  • Integration (Stripe test mode): seeded fixture customers + prices; run full invoice lifecycle end-to-end; assert rendered PDF bytes match golden snapshot (≥ tolerance for date fields).
  • Mail deliverability: Mailgun test mode / sandbox domain before live cutover.
  • Reconciliation: daily Cron Trigger compares Stripe totals to Acodei / QuickBooks totals; alerts on drift.
  • Cohort gate: per-cohort dual-run. A cohort is “migrated” only after one full billing cycle with zero reconciliation drift.
  • Rollback per cohort: cohort can be moved back to legacy rollup until B.8 retires the legacy code. After B.8, rollback is recovery-from-backup only — so B.8 should come with extra scrutiny.