Skip to content

01 — Architecture Assessment

Current billing pipeline (actual, from repo analysis)

┌──────────────────────────────────────────────────────────────┐
│  Billable events accrue all month                            │
│  (ad serving impressions by type: ST / RM / CV)             │
│  Source: Redshift (stats), queried via Pdo_model            │
└──────────────────────────┬───────────────────────────────────┘
                           │ operator triggers month-end run
                           ▼ (manual UI action, not a cron)
┌──────────────────────────────────────────────────────────────┐
│  Billing.php::generateInvoice()                              │
│  POST to billing_service: m=createInvoice                    │
│  billing_service aggregates line items + calls Stripe        │
│  (billing_service is a separate internal API, not in repo)   │
└──────────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│  billing_service PDF renderer                                │
│  Generates custom-branded PDF invoice                        │
│  Stores at: billing.adventivecdn.com/{acct_id}/{inv_uuid}.pdf│
│  (S3 via CloudFront — billing.adventivecdn.com)              │
└──────────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│  Admin: Billing.php::sendInvoiceReminder()                   │
│  Downloads PDF from CDN to /tmp/invdir/                      │
│  Mailgun.php::sendTemplatedEmail()                           │
│  Templates: invoice_new, invoice_notify, invoice_pastdue     │
│  Sender: billing-noreply@notify.adventive.com                │
│  BCC: 2658568@bcc.hubspot.com (HubSpot CRM)                 │
└──────────────────────────┬───────────────────────────────────┘
                           │ operator triggers dunning manually
                           ▼ (or via emailInvoice() batch)
┌──────────────────────────────────────────────────────────────┐
│  6-step dunning sequence (admin-owned, operator-triggered)   │
│  invoiceNew → invoiceDue7 → invoiceDue0 →                   │
│  invoicePastDue5 → invoicePastDue15 → invoicePastDue30      │
└──────────────────────────────────────────────────────────────┘

Current state — confirmed findings

  • Runtime: CodeIgniter 3.1, PHP 7.x, running on EC2 (legacy admin server). The admin is the operator-facing UI only; billing logic lives in a separate billing_service internal API at billing.{domain}/api/billing/.

  • Rollup scheduling: There is no automated cron for invoice generation. Invoices are triggered manually by an operator via Billing.php::generateInvoice(), which POSTs to billing_service with m=createInvoice. The billing_service owns the actual aggregation and Stripe API calls. Automated scheduled jobs in Reporting.php are for partner analytics reports, not billing.

  • Stripe API integration: All Stripe calls originate from billing_service, not from the admin repo. The admin repo contains zero Stripe SDK calls. The admin uses billing_service as an internal API boundary: the admin POSTs form data (m=createInvoice, m=processPayment, m=getAchInfo) and billing_service executes the Stripe operations. The exact Stripe API version and SDK used by billing_service cannot be determined from the admin repo alone.

  • Pricing model (confirmed from Billing_model.php):

  • Tiered impression pricing via account_plan_usage table: usage_type (ST/RM/CV), usage_max (tier ceiling), usage_rate (per-unit rate). Tiers are graduated; the tier where actual_impressions < usage_max determines the rate.
  • Flat subscription fee via account_plan_sub table: sub_name, sub_invoice_line, sub_rate.
  • Managed service jobs: ManagedServiceJob.php / ManagedService_model.php — per-job flat amounts attached to a customer for a given month.

  • Custom invoice PDF renderer: PDF is generated by billing_service and stored in S3 at billing.adventivecdn.com/{acct_id}/{inv_uuid}.pdf. The admin downloads this file to /tmp/invdir/ (Billing.php::_getInvoiceFile()) and attaches it as a Mailgun multipart upload. The renderer's tech stack (wkhtmltopdf, Dompdf, tcpdf, or other) is in billing_service — not analyzable from this repo. Handlebars templates referenced in the kickoff context are in billing_service.

  • Mailgun send path: application/libraries/Mailgun.php — a custom curl-based client. sendTemplatedEmail() uses Mailgun's t:variables Handlebars variables API. Sender: billing-noreply@notify.adventive.com. BCC: HubSpot address 2658568@bcc.hubspot.com on all invoice emails. Three billing templates: invoice_new, invoice_notify, invoice_pastdue. Mailgun is a shared service — the same library handles auth emails, onboarding, partner report delivery, and general notifications.

  • Reconciliation: No reconciliation logic found in the admin repo. Paid/unpaid/failed invoice status is tracked in the billing_invoice table (billing DB) and surfaced via Billing_model.php::getBillingHistory(). No automated reconciliation against Stripe; operators check payment status manually via the Billing UI.

  • Dispute handling: No dispute handling in the admin code. Dashboard-managed via Stripe.

  • Retry / dunning: Adventive owns a custom 6-step dunning sequence in Billing.php::sendInvoiceReminder(). The sequence (invoiceNew → invoiceDue7 → invoiceDue0 → invoicePastDue5 → invoicePastDue15 → invoicePastDue30) is triggered by operators manually or via the emailInvoice() batch method. It downloads the PDF and attaches it to each Mailgun send. There is no evidence of Stripe Smart Retries being configured — dunning is entirely Adventive-owned today.

  • Failed-payment flow: Billing.php::processPayment() POSTs to billing_service with m=processPayment. Failed payment status is surfaced to operators via the invoice list UI. No automated retry or operator alerting logic found in the admin.

  • Payment link: Generated as md5($invoice->payment_customer_id . $invoice->inv_uuid . $invoice->acct_id) — MD5-based hash link passed into invoice templates. This is not Stripe-native payment link functionality.

  • Month-end Excel report: Billing.php::getMonthSummary() uses PHPExcel (abandoned library) to generate a .xlsx from MonthEndReportTemplate.xlsx. Reads impression data (ST/RM/CV types from Redshift), subscription info, and tiered pricing from account_plan_usage. This is independent of invoice generation and is used for accounting/ops review.

  • Stripe account state (confirmed via Stripe MCP):

  • Account: acct_17YHT3CSGXf35AB6 (Adventive)
  • Products: 5 total — Account Charges, Usage (Ad Serving), Managed Services, + 2 unnamed Usage products
  • Prices: 5 total — all type=one_time; 3 have amount=null (custom/variable per invoice); 0 recurring, 0 metered
  • Subscriptions: ZERO — all billing is one-off manual invoices
  • Invoices: All sampled invoices have billing_reason=manual; mix of open and paid; amounts range from $18.73 to $6,771.75

Stripe-native capability map

Subsystem Current (Adventive) Stripe-native option Coverage Notes
Customer record Adventive DB + Stripe Customer object Customer ✓ Full Already on Stripe Customers
Product / price catalog account_plan_usage + account_plan_sub tables; 5 Stripe Products exist Product + Price ✗ None for recurring 5 Products exist but all Prices are one-time with null amount — no metered/tiered Price objects
Line-item rollup Manual operator trigger → billing_service → Stripe Invoice InvoiceItem (add as-accrued) + Stripe Billing metered ✗ None Currently month-end lump; Stripe can accumulate as events occur
Invoice creation billing_service calls Stripe Invoice API on operator trigger Invoice + auto-advance ◑ Partial Stripe Invoice already used, but creation is manual, not subscription-driven
Invoice PDF Custom PHP renderer in billing_service; stored in S3 CDN Stripe Hosted Invoice Page + downloadable PDF ✗ None Custom renderer non-negotiable (Jeffrey 5/5); Stripe's branding not sufficient
Invoice delivery Admin + Mailgun.php + billing templates Stripe email or webhook-triggered custom ✗ None Mailgun stays; billing Worker handles delivery on invoice.finalized
Payment collection Stripe — unchanged Stripe — unchanged ✓ Full
Payment link MD5 hash link in invoice template Stripe Payment Link or Hosted Invoice Page ✗ None Replace with Stripe Hosted Invoice Page URL in billing Worker template
Failed payment retry None — no automated retry in admin or billing_service found Smart Retries (Stripe Billing) ✗ None Must configure Smart Retries as part of B.3
Custom dunning 6-step manual sequence in Billing.php; operator-triggered Stripe dunning email schedules ✗ None (currently) Retire Adventive dunning; Stripe dunning takes over per ADR
Customer self-service None Customer Portal ✗ None B.9 optional; enables card update + invoice history self-service
Tax None Stripe Tax ✗ None (by design) Not collected today; design allows future enablement without re-architecture
Reconciliation None Stripe reports + API ✗ None Billing Worker Cron Trigger handles daily reconciliation against Acodei/QuickBooks
Dispute handling Dashboard-managed Stripe Radar + API ◑ Partial Stripe Dashboard used today; no Adventive code; no change needed at cutover
Refunds / credits Dashboard-managed Stripe Refunds + Credit Notes ◑ Partial Stripe used today; admin-api will expose operator-facing refund trigger in sibling project
Audit trail billing_invoice table + Stripe events Stripe events + API (90-day default) ◑ Partial Export all historical events to R2 or BigQuery before cutover to satisfy 14-year retention

Target architecture — chosen

Hybrid: Stripe owns billing logic, Adventive owns invoice presentation + email delivery. Full rationale in decisions/2026-04-23-stripe-billing-scope.md and decisions/2026-04-23-custom-invoice-pdf-retained.md.

Adventive product events (ST / RM / CV impressions)
         ▼ (as they accrue — not month-end rollup)
┌─────────────────────────────────────────────────┐
│ Billing Worker: POST /usage                      │
│ Idempotent metering event ingestion              │
│ → Stripe Usage Record API (metered Price)        │
└─────────────────────────────────────────────────┘
         │ also: managed service jobs → InvoiceItem
         │ flat subscription → Stripe Subscription
┌─────────────────────────────────────────────────┐
│ Stripe Billing                                   │
│ - Subscriptions (flat + metered prices)         │
│ - InvoiceItems (managed services, custom)        │
│ - Discounts / coupons                            │
│ - Smart Retries + dunning emails                 │
│ - Payment collection                             │
│ - Disputes / refunds / credit notes              │
│ - Customer Portal (B.9)                          │
└──────────────────────┬──────────────────────────┘
                       │ webhook: invoice.finalized
┌─────────────────────────────────────────────────┐
│ Billing Worker: POST /webhook/stripe             │
│ - Verify Stripe-Signature header                 │
│ - Pull full invoice from Stripe API              │
│ - Render Adventive-branded PDF                   │
│   (Cloudflare Browser Rendering API)             │
│ - Upload PDF to R2                               │
│ - Attach PDF to Stripe invoice (optional)        │
│ - Trigger Mailgun: invoice_new / invoice_notify  │
└──────────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Mailgun (shared service, unchanged sender)       │
│ billing-noreply@notify.adventive.com             │
│ BCC: 2658568@bcc.hubspot.com                     │
│ Templates: invoice_new, invoice_notify           │
│            invoice_pastdue (now Stripe-owned)    │
└─────────────────────────────────────────────────┘

Parallel (unchanged):
Stripe events → Acodei → QuickBooks

Rejected options

  • Full Stripe-native (Stripe sends the invoice): rejected — invoice brand fidelity is non-negotiable (Jeffrey rated 5/5). Stripe's template customization (logo, colors, custom fields) does not meet Adventive's bar.
  • Status quo with cleanup: rejected — leaves the rollup, pricing calculations, dunning, and subscription handling in Adventive's codebase, defeating the leverage case.
  • Partial Stripe (keep dunning in admin): rejected per ADR — Stripe owns dunning end-to-end. The existing 6-step manual dunning in Billing.php is retired; Stripe dunning configuration replaces it.

Mailgun is staying

Mailgun is a shared service used for billing emails, auth emails, new-user onboarding, partner report delivery, and general notifications — all with established Handlebars templates. It stays. A future migration to a Cloudflare-native email service is revisitable once Cloudflare offers a production-ready outbound email product. The billing Worker will call Mailgun directly, removing the admin as an intermediary.


Serverless viability assessment (residual Adventive-side billing)

Residual subsystem Candidate target Viability Notes
Webhook receiver (Stripe → Adventive) Worker + Queue (Workers Queue for DLQ) ✓ Excellent Stateless event handler; well within CPU/memory limits
Usage event ingestion (/usage) Worker endpoint ✓ Excellent Idempotent write to Stripe Usage Records API; no persistent state
PDF rendering Cloudflare Browser Rendering API ✓ Good Leading candidate; ~$5/1000 renders; concurrency limit 2 per account — sufficient at ~200 invoices/month
Reconciliation batch Cron Trigger Worker (wrangler.toml) ✓ Excellent Daily cron comparing Stripe totals to Acodei/QuickBooks
Month-end Excel export Cron Trigger Worker (long-term) ✓ Acceptable PHPExcel replacement; lower priority; CSV via Worker sufficient
PDF storage Cloudflare R2 ✓ Excellent S3-compatible; replaces billing.adventivecdn.com CDN

Open architectural questions

All questions from the original stub have been resolved by analysis:

  1. ~~Invoice brand fidelity~~ — Resolved: 5/5 non-negotiable, custom renderer retained.
  2. ~~Mailgun uses~~ — Resolved: shared service, stays.
  3. ~~Tax posture~~ — Resolved: no tax collected today; design allows Stripe Tax enablement later.
  4. ~~Long-retention audit requirements~~ — Resolved: ~14 years; export pre-cutover.
  5. ~~Accounting integration~~ — Resolved: Acodei → QuickBooks, unchanged.
  6. PDF-rendering implementation — Cloudflare Browser Rendering is the leading candidate. Must confirm concurrency limit (2 simultaneous renders per account) does not create backlog during batch invoice finalization. At ~200 customers, 200 renders/month is well within capacity at non-simultaneous pacing.
  7. ~~Metering signal contract~~ — Refined: ST/RM/CV impression counts from Redshift are the metering signal. Billing Worker /usage accepts these per-account, per-type, per-period with idempotency key = {acct_id}:{period}:{type}.
  8. ~~Pricing catalog migration~~ — Resolved: see Chapter 02 pricing-catalog migration detail and Checkpoint 1 matrix.
  9. Cutover cohorting — Recommended order: (1) flat-only customers (simplest migration, no metered complexity), (2) tiered impression customers, (3) managed-service-heavy and custom-arrangement customers. Driven by pricing model complexity, not account size.
  10. Customer Portal — B.9 optional. Enables self-service card update + invoice history. Low implementation cost; deferred until post-B.8 stability is confirmed.
  11. Stripe Smart Retries configuration — Must be explicitly configured before any customer cutover. No evidence Smart Retries is currently enabled on this Stripe account.
  12. Historical invoice export — ~14 years of invoice history in billing_invoice (billing DB). Must be exported to R2 (or preserved in the legacy DB with read-only access) before the billing_invoice table is retired as the SoR.