Skip to main content

Platform Billing — Collection Loop

Spec: specs/047-platform-billing-collection-loop/

Closes FIXX → tenant SaaS invoices the same way tenant Subscriptions close merchant → customer charges: scheduled period close, payment notices, FEL factura by commercial timing, and PWA Send notice.

Daily job (BullMQ)

Queue: platform-billing · Cron: 0 10 * * * (after subscription billing at 03:00)

Three passes in order:

  1. CloseDuePeriods — paginated findBillablePage; closes prior calendar month via existing PeriodCloser; always links a non-AR commercial sale to each AR invoice for the period (listForPeriod); certifies FEL only when timing is immediate. Summary counts salesLinked (new sales) and facturasIssued (FEL requested this run). With invoice_granularity=location, one account can produce multiple invoices (and sales/facturas) in the same period.

Invoice granularity

platform_account.invoice_granularity (default account):

ValuePeriod close
accountOne AR invoice + sale/factura; branch fees/% are lines
locationOne AR invoice + sale/factura per active branch; SMS/WhatsApp/discount on a companion account invoice when present

Toggle from account detail (next to commercial invoice timing). Applies on the next close only. 2. PlatformInvoiceFactura paid sweepentity_type=platform_billing_period, status=paid, and missing or uncertified FEL; refreshes tenders from posted receipts, then certifies (creates sale if still missing). 3. SendPlatformPaymentNotice catch-up — email only; stamps platform_billing_period.notice_sent_at.

Manual ops: POST /platform-billing/jobs/close-due?billingPeriod=YYYY-MM (PlatformAdminGuard — FIXX super/owner). First production run: confirm which month to close; the scheduled job is idempotent for the prior month.

PWA: Platform Billing → Collection Jobs (/platform-billing/jobs) — schedule metadata, next-run projection (dry-run), manual trigger, period totals, and close history.

Local / staging retest workflow (projection → unit tests → close → wipe or change period): see Testing and Iteration.

Sale + FEL timing

TimingAt closeOn payment (paid sweep)
immediatesale + FEL + AR(+) + noticereceipt + AR(-)
on_paymentsale (uncertified) + AR(+) + noticereceipt + AR(-) + FEL

facturaIssued in reports and the account invoices UI means FEL certified (sale.fel_status = certified), not merely that a sale_id exists. After close, on_payment invoices show factura pending until payment + paid sweep.

Due date on new AR invoices: 5th of the month after the billing period (e.g. period 2026-06 → due 2026-07-05). No backfill of older rows.

Factura spike (locked)

AccountsReceivableInvoicesService.processSaleEvent creates AR for every submitted-sale tender whose payment method has generatesAccountsReceivable=true. Platform invoices are AR-first; platform sales must use a payment method with generatesAccountsReceivable=false so a second AR row is never created. Do not copy subscriptions’ sale-first immediate path.

The close-time credit tender is not cash settlement — it is a non-AR placeholder so the commercial sale can exist (and optionally certify) while the open AR invoice remains the collectible balance.

Timing resolution

platform_account.commercial_invoice_timing → else customer.commercial_invoice_timing → else on_payment.

Branch fees and rate overrides

Flat branch fees are one invoice line per active flat branch. Amount comes from RateResolver: branch rate_override → account rate_overrides → plan price. Operators set a custom amount on the account detail page (PATCH …/branches/:id with customUnitAmount); null clears the override. Changing billing mode clears any override. Percentage-mode branches already resolve rates per branch for sales_percentage.

Percentage (sales_percentage) branches need a period sales base: native FlowPOS sales plus optional legacy monthly totals (platform_legacy_sales_total). Close defers with waiting_legacy_sales until a legacy row lands when the branch still has a legacy_location_code and zero native sales — see Legacy Sales Totals.

Notices

  • Auto catch-up: email only.
  • Manual: POST …/accounts/:accountId/invoices/:invoiceId/send-notice with { channel }.
  • Templates: platform_billing_sale_notice_{email,sms,whatsapp} (system rows; Spanish copy — documento de venta). documentNumber is the linked commercial sale.document_number (e.g. 000511), with AR invoice number as fallback only — not the FEL authorization UUID. Legacy platform_billing_payment_notice_* rows remain in DB but are unused by the job. Vars include amount, dueDate, businessPhone / businessEmail, plus businessContactLine (email wraps it in <p> via Handlebars {{#if}} so empty contact omits the line).
  • Email notices attach the FIXX-scoped Platform Billing Sale — A4 PDF when invoice.saleId is set. One document_template row per FIXX business id (2026-07-24t11-00-00-flag-fixx-platform-operator.mjs local/staging/production). Soft-fails if the template is missing or PDF generation fails — the notice still queues without an attachment.
  • Selection is explicit via resolvePlatformBillingSalePdfTemplateId(fixxBusinessId) for notice attachments. For FIXX Share / PDF / email attachment from the sale UI, that same template is the business default sale A4 (is_default) and location sale print config (no Detalle de Pagos — AR is internal). System Sale Standard A4 is unchanged for other tenants.
  • SendPlatformPaymentNoticeService maps FIXX business.contact → those vars (never emailSender).

Ops checklist

  • Confirm FIXX FEL certifier before enabling immediate in production.
  • Ensure FIXX has at least one active non-AR payment method (generates_accounts_receivable = false, e.g. Efectivo) and one active service with IVA (factura line catalog) — required for every close (sale is always linked). Keep a separate method for recording tenant payments if needed.
  • Ensure billing customers have email for notice catch-up.
  • Configure FIXX Business settings → Contact phone and contact email (not sender/noreply) so payment-notice consultas lines are complete.
  • Before a first close of a month, review Next run projection on the jobs page; after a close, re-running the same period is a no-op for those invoices — see Testing and Iteration.
  • Tenants who closed or switched POS: set account status to Cancelled (or deactivate branches for partial exits). Projection no charges is a review signal only — see Account Status and Churn.