Skip to content

01 — Architecture Assessment

Current billing pipeline (actual, from repo analysis)

Section titled “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 │
└──────────────────────────────────────────────────────────────┘
  • 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

SubsystemCurrent (Adventive)Stripe-native optionCoverageNotes
Customer recordAdventive DB + Stripe Customer objectCustomer✓ FullAlready on Stripe Customers
Product / price catalogaccount_plan_usage + account_plan_sub tables; 5 Stripe Products existProduct + Price✗ None for recurring5 Products exist but all Prices are one-time with null amount — no metered/tiered Price objects
Line-item rollupManual operator trigger → billing_service → Stripe InvoiceInvoiceItem (add as-accrued) + Stripe Billing metered✗ NoneCurrently month-end lump; Stripe can accumulate as events occur
Invoice creationbilling_service calls Stripe Invoice API on operator triggerInvoice + auto-advance◑ PartialStripe Invoice already used, but creation is manual, not subscription-driven
Invoice PDFCustom PHP renderer in billing_service; stored in S3 CDNStripe Hosted Invoice Page + downloadable PDF✗ NoneCustom renderer non-negotiable (Jeffrey 5/5); Stripe’s branding not sufficient
Invoice deliveryAdmin + Mailgun.php + billing templatesStripe email or webhook-triggered custom✗ NoneMailgun stays; billing Worker handles delivery on invoice.finalized
Payment collectionStripe — unchangedStripe — unchanged✓ Full
Payment linkMD5 hash link in invoice templateStripe Payment Link or Hosted Invoice Page✗ NoneReplace with Stripe Hosted Invoice Page URL in billing Worker template
Failed payment retryNone — no automated retry in admin or billing_service foundSmart Retries (Stripe Billing)✗ NoneMust configure Smart Retries as part of B.3
Custom dunning6-step manual sequence in Billing.php; operator-triggeredStripe dunning email schedules✗ None (currently)Retire Adventive dunning; Stripe dunning takes over per ADR
Customer self-serviceNoneCustomer Portal✗ NoneB.9 optional; enables card update + invoice history self-service
TaxNoneStripe Tax✗ None (by design)Not collected today; design allows future enablement without re-architecture
ReconciliationNoneStripe reports + API✗ NoneBilling Worker Cron Trigger handles daily reconciliation against Acodei/QuickBooks
Dispute handlingDashboard-managedStripe Radar + API◑ PartialStripe Dashboard used today; no Adventive code; no change needed at cutover
Refunds / creditsDashboard-managedStripe Refunds + Credit Notes◑ PartialStripe used today; admin-api will expose operator-facing refund trigger in sibling project
Audit trailbilling_invoice table + Stripe eventsStripe events + API (90-day default)◑ PartialExport all historical events to R2 or BigQuery before cutover to satisfy 14-year retention

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

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

Section titled “Serverless viability assessment (residual Adventive-side billing)”
Residual subsystemCandidate targetViabilityNotes
Webhook receiver (Stripe → Adventive)Worker + Queue (Workers Queue for DLQ)✓ ExcellentStateless event handler; well within CPU/memory limits
Usage event ingestion (/usage)Worker endpoint✓ ExcellentIdempotent write to Stripe Usage Records API; no persistent state
PDF renderingCloudflare Browser Rendering API✓ GoodLeading candidate; ~$5/1000 renders; concurrency limit 2 per account — sufficient at ~200 invoices/month
Reconciliation batchCron Trigger Worker (wrangler.toml)✓ ExcellentDaily cron comparing Stripe totals to Acodei/QuickBooks
Month-end Excel exportCron Trigger Worker (long-term)✓ AcceptablePHPExcel replacement; lower priority; CSV via Worker sufficient
PDF storageCloudflare R2✓ ExcellentS3-compatible; replaces billing.adventivecdn.com CDN

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

  1. Invoice brand fidelityResolved: 5/5 non-negotiable, custom renderer retained.
  2. Mailgun usesResolved: shared service, stays.
  3. Tax postureResolved: no tax collected today; design allows Stripe Tax enablement later.
  4. Long-retention audit requirementsResolved: ~14 years; export pre-cutover.
  5. Accounting integrationResolved: 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 contractRefined: 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 migrationResolved: 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.