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:
- CloseDuePeriods — paginated
findBillablePage; closes prior calendar month via existingPeriodCloser; always links a non-AR commercialsaleto each AR invoice for the period (listForPeriod); certifies FEL only when timing isimmediate. Summary countssalesLinked(new sales) andfacturasIssued(FEL requested this run). Withinvoice_granularity=location, one account can produce multiple invoices (and sales/facturas) in the same period.
Invoice granularity
platform_account.invoice_granularity (default account):
| Value | Period close |
|---|---|
account | One AR invoice + sale/factura; branch fees/% are lines |
location | One 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 sweep — entity_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
| Timing | At close | On payment (paid sweep) |
|---|---|---|
immediate | sale + FEL + AR(+) + notice | receipt + AR(-) |
on_payment | sale (uncertified) + AR(+) + notice | receipt + 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-noticewith{ channel }. - Templates:
platform_billing_sale_notice_{email,sms,whatsapp}(system rows; Spanish copy — documento de venta).documentNumberis the linked commercialsale.document_number(e.g.000511), with AR invoice number as fallback only — not the FEL authorization UUID. Legacyplatform_billing_payment_notice_*rows remain in DB but are unused by the job. Vars includeamount,dueDate,businessPhone/businessEmail, plusbusinessContactLine(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.saleIdis set. Onedocument_templaterow per FIXX business id (2026-07-24t11-00-00-flag-fixx-platform-operator.mjslocal/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 locationsaleprint config (no Detalle de Pagos — AR is internal). System Sale Standard A4 is unchanged for other tenants. SendPlatformPaymentNoticeServicemaps FIXXbusiness.contact→ those vars (neveremailSender).
Ops checklist
- Confirm FIXX FEL certifier before enabling
immediatein 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.