Skip to content

02 — Code Updates / Migration Plan

Summary of changes

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.

Target repository layout

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.

Before / after

Before

Adventive events → internal store → month-end rollup → Stripe Invoice API
                                                     Custom PDF gen
                                                       Mailgun

After

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.

Migration phases

Phase A — Viability Assessment (mostly docs)

Step Output
A.1 Inventory current pricing models (from adventive-admin repo + Stripe dashboard)
A.2 Map each pricing model to Stripe Product / Price constructs
A.3 Enumerate non-standard arrangements → per-customer migration notes
A.4 Cost model: current run-cost vs. post-migration run-cost
A.5 Risk register with mitigations
A.6 Go / no-go sign-off (Jeffrey + Patrick)

Phase B — Build & Cutover

Step Output Gate
B.1 Scaffold billing-worker repo. Implement Stripe webhook receiver with signature verification, idempotency, DLQ. Webhook green in Stripe test mode
B.2 Port Handlebars invoice template. Implement PDF renderer (leading: Cloudflare Browser Rendering). Attach to test-mode invoices. Test-mode invoice PDFs match current design
B.3 Implement Mailgun send path for invoice emails. Test customers receive Adventive-branded PDFs
B.4 Implement /usage endpoint for metering events. Backfill event emission in Adventive product. Metered usage flows end-to-end in test mode
B.5 Migrate pricing catalog: create Stripe Product and Price for each current pricing model. Catalog parity review
B.6 Migrate first cohort (low-risk, flat-pricing customers). Stripe live mode. Dual-run reconciliation for one billing cycle. Reconciliation clean
B.7 Migrate subsequent cohorts (tiered, metered, custom) — one cohort per billing cycle. Reconciliation clean per cohort
B.8 All 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.9 Optional: enable Stripe Customer Portal for self-service. Customer comms complete

File-by-file change list

New (billing-worker repo)

File Purpose
src/index.ts Hono app, routes /webhook/stripe, /usage, /admin/resend-invoice
src/webhooks/stripe.ts Signature verification + event dispatch
src/webhooks/handlers/invoice-finalized.ts Primary invoice-presentation handler
src/render/templates/invoice.hbs Migrated from legacy admin
src/render/pdf.ts HTML → PDF rendering
src/email/mailgun.ts Mailgun API client wrapper
src/usage/report.ts Idempotent metering event ingestion
src/reconcile/daily.ts Cron-triggered reconciliation against Stripe

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

File Functions retired Phase
application/controllers/Billing.php generateInvoice() — operator trigger → billing_service; processPayment() — payment trigger; sendInvoiceReminder() — 6-step manual dunning; emailInvoice() — batch dunning trigger; _getInvoiceFile() — PDF download from CDN to /tmp B.3
application/models/Billing_model.php getBilling(), 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 email B.3

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

Component Description Phase
Month-end rollup job Aggregates billable items per customer; triggers Stripe Invoice creation B.3
Stripe API calls (create Invoice, InvoiceItem, etc.) All Stripe API interactions move to billing Worker B.3
Custom invoice PDF renderer PHP PDF library (tech stack not analyzable from admin repo); replaced by Cloudflare Browser Rendering in billing Worker B.4
PDF S3 storage (billing.adventivecdn.com) CloudFront CDN delivery of invoice PDFs; replaced by R2 + Cloudflare serving B.4

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

Table Current role Phase
account_plan_sub Flat subscription fee per account B.3 (read-only during migration; retired as SoR when Stripe Subscription is authoritative)
account_plan_usage Tiered impression pricing tiers per plan B.4 (read-only during migration; retired as SoR when Stripe metered Prices are authoritative)
billing_invoice Invoice history (14 years) Retained read-only post-cutover; export to R2 / archive before full decommission

Pricing-catalog migration detail

Current pricing models → Stripe constructs

Adventive Model DB Source Stripe Product Stripe Price Type Phase
Flat monthly subscription fee account_plan_sub.sub_rate Account Charges prod_TvOSKLTnPZEzkr recurring, interval=month, usage_type=licensed, billing_scheme=per_unit B.5
ST impression tiers account_plan_usage (usage_type=ST) Usage prod_TvORrZbbLw4cOc recurring, usage_type=metered, billing_scheme=tiered, tiers_mode=graduated B.5
RM impression tiers account_plan_usage (usage_type=RM) Usage — separate Price recurring, usage_type=metered, billing_scheme=tiered, tiers_mode=graduated B.5
CV impression tiers account_plan_usage (usage_type=CV) Usage — separate Price recurring, usage_type=metered, billing_scheme=tiered, tiers_mode=graduated B.5
Managed service line items ManagedService_model.php per-job amounts Managed Services prod_TvOQzylfxDc5v7 InvoiceItem (one-off, attached before invoice finalization) B.5
Custom / bespoke per-account Manual InvoiceItem in billing_service Account Charges or ad-hoc Manual InvoiceItem added pre-finalization B.6

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)

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).

Metering signal contract

  • 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

Dependencies

  • 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.

Breaking changes — customer-facing

  • 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.

Testing strategy

  • 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.