Platform Billing — Testing and Iteration
How to validate the collection loop, change code, and re-test — locally and on staging.
Related: Collection Loop · Spec:
specs/047-platform-billing-collection-loop/
Critical constraint: idempotency
Closing a billing period is idempotent. After you close YYYY-MM for an account:
- Run now again will not recreate that invoice.
- Next run projection will show the account as Already closed.
To exercise the close path again you must either:
- Delete that period’s markers + AR invoices (local / carefully on staging), or
- Close a different
YYYY-MM, or use a throwaway account / rate change so other accounts get charges.
Next run projection (GET /platform-billing/jobs/projection) never writes — use it for most UI and math iteration.
Projection statuses (Projected, Already closed, Annual, No charges, Waiting on legacy sales) and how they relate to churn vs period state: Account Status and Churn. For percentage-plan accounts still on the legacy POS, load / inspect monthly totals via Legacy Sales Totals before expecting projection amounts.
What a successful close writes
For each billable account in the period (example pattern from a dry-run):
| Artifact | Typical count | Notes |
|---|---|---|
platform_billing_period | One per scanned monthly account | Marker via find-or-create — including accounts with no charges |
accounts_receivable_invoice | One per account with charges | Line items in invoice detail JSON; due_date = 5th of next month |
sale (+ related) | One per closed invoice | Always linked at close (non-AR credit tender). FEL certified at close only if commercial_invoice_timing = immediate; otherwise paid-sweep certifies after payment |
communication (+ email queue) | Up to one per new invoice | Pass 3 queues email notices; stamps notice_sent_at |
Pass 3 fails (no email) if the customer has no email or the platform_billing_sale_notice_email template is missing — see Collection Loop. For a complete consultas line, set FIXX Business settings → Contact (phone + contact email) before sending notices.
Local email delivery: set COMMUNICATION_EMAIL_OVERRIDE=you@company.com and DEPLOY_ENV=localhost (or leave DEPLOY_ENV unset with NODE_ENV=development). Without that, notices go to real customer addresses. Subject lines are prefixed with [intended: …]. The queue drains ~30s after enqueue.
PWA surface: Platform Billing → Collection Jobs (/platform-billing/jobs). Requires a FIXX platform-admin session (super or owner on the operator business — Access Control).
Local loop
0. Stack
docker compose up -d # Postgres + Redis
pnpm --filter backend run start:dev
pnpm --filter frontend-pwa run dev
- Backend:
http://localhost:4000 - PWA:
http://localhost:5173 - Open
/platform-billing/jobsas FIXXsuperorowner
1. Safe iteration (no cleanup)
- Change code (rates, projection UI, closer math, notices, etc.).
- Let hot reload apply.
- Click Refresh on the jobs page (or set/clear the period override).
- Re-check Next run projection — period, scanned count, billable accounts, amounts, line items.
This is the primary “make changes → test again” path.
2. Unit tests (fast)
pnpm --filter backend test -- close-due-periods
pnpm --filter backend test -- period-closer
pnpm --filter backend test -- send-platform-payment-notice
pnpm --filter backend test -- platform-invoice-factura
pnpm --filter backend test -- platform-billing-collection-report
Use these when changing pass logic; no DB wipe required.
Specs live under apps/backend/src/platform-billing/application/__tests__/.
3. Full three-pass run (writes + emails)
- Confirm projection looks right for the target period.
- Optionally set Billing period override (
YYYY-MM); blank = prior calendar month. - Run now → confirm in the modal.
- Verify:
- Toast (
closedcount / period) - Period totals and Close history for that month
- Email only if local SendGrid (or provider) + templates + customer emails are configured — the job queues email; it does not guarantee delivery
- Toast (
API equivalent:
POST /platform-billing/jobs/close-due?billingPeriod=YYYY-MM
(PlatformAdminGuard — FIXX super/owner; same as the PWA button.)
4. Reset so you can close the same period again
Inspect first (replace 2026-06):
SELECT id, customer_id, billing_period, total_amount, sale_id, status
FROM accounts_receivable_invoice
WHERE entity_type = 'platform_billing_period'
AND billing_period = '2026-06';
SELECT id, platform_account_id, billing_period, notice_sent_at, closed_at
FROM platform_billing_period
WHERE billing_period = '2026-06';
If any invoice has sale_id set (always after a successful close that linked a sale), delete or unlink those sales (and related sale/FEL rows) before deleting the AR invoices — follow your local env’s usual sale cleanup.
Then wipe the period:
DELETE FROM accounts_receivable_invoice
WHERE entity_type = 'platform_billing_period'
AND billing_period = '2026-06';
DELETE FROM platform_billing_period
WHERE billing_period = '2026-06';
Optional: remove queued/sent communication rows for those entities if you are retesting notice content.
Then: Refresh → projection should show billable accounts again (not already closed) → Run now.
Faster alternative: skip the wipe and close a different unused YYYY-MM.
5. Notice-only retest (no re-close)
- Set
notice_sent_at = NULLon the relevantplatform_billing_periodrows and run the job again (pass 3 only picks markers with an invoice and nullnotice_sent_at), or - Use Send notice on the account invoice in the PWA (
POST …/accounts/:accountId/invoices/:invoiceId/send-notice).
Staging
Deploy
- Feature branch from
main→ PR → merge tomain. - Staging Cloud Run deploys automatically (
deploy-staging-from-main). - Hotfix without GitHub (optional):
export GCP_PROJECT_ID=barto-dev
DB_POOL_MAX=6 ./scripts/deploy-backend-local.sh staging
# UI changes:
./scripts/deploy-frontend-pwa-local.sh staging
See Deployment workflow and Environments.
Test on staging
- Open staging PWA as platform admin → Platform Billing → Collection Jobs.
- Always run Next run projection first — confirm clients and amounts.
- Only then Run now (optional
billingPeriod). - Verify history/totals and email delivery (staging SendGrid).
Make changes → test again on staging
| What you changed | How to retest |
|---|---|
| Projection / display / rate math | Deploy → Refresh projection (no wipe) |
| Close / factura / notice services | Unit tests locally → deploy → projection → one close |
| Need a second full close of the same month | Prefer another month or a throwaway account. Wipe staging markers + AR invoices only with ops agreement — this is real tenant billing data |
Do not casually delete staging invoices if finance or ops already rely on them.
Day-of checklist
- Local: projection until numbers look right.
- Local: unit tests for the services you touched.
- Local: one Run now + verify history; wipe or change period if you need another close.
- Staging: deploy → projection must match local expectation → one Run now if you need a live write/email check.
- For more close iterations on staging, prefer a different period or a throwaway account, not repeated deletes of shared data.
Ops reminders
- Confirm FIXX FEL certifier before enabling
immediateoutside local. - FIXX needs an active non-AR payment method (
generates_accounts_receivable = false, e.g. seeded Efectivo) and an active service with IVA for every close (sale is always linked; FEL only when timing or paid sweep requires it). The bank-transfer method used to record receipts can stay AR-generating — that is a different purpose. - UI Factura column / history
facturaIssued= FEL certified, not merelysale_idpresent. - Billing customers need email for notice catch-up.
- Scheduled cron:
0 10 * * *America/Guatemala — manual runs are independent of registration status shown on the schedule card.