Skip to main content

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:

  1. Delete that period’s markers + AR invoices (local / carefully on staging), or
  2. 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):

ArtifactTypical countNotes
platform_billing_periodOne per scanned monthly accountMarker via find-or-create — including accounts with no charges
accounts_receivable_invoiceOne per account with chargesLine items in invoice detail JSON; due_date = 5th of next month
sale (+ related)One per closed invoiceAlways 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 invoicePass 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/jobs as FIXX super or owner

1. Safe iteration (no cleanup)​

  1. Change code (rates, projection UI, closer math, notices, etc.).
  2. Let hot reload apply.
  3. Click Refresh on the jobs page (or set/clear the period override).
  4. 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)​

  1. Confirm projection looks right for the target period.
  2. Optionally set Billing period override (YYYY-MM); blank = prior calendar month.
  3. Run now → confirm in the modal.
  4. Verify:
    • Toast (closed count / 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

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 = NULL on the relevant platform_billing_period rows and run the job again (pass 3 only picks markers with an invoice and null notice_sent_at), or
  • Use Send notice on the account invoice in the PWA (POST …/accounts/:accountId/invoices/:invoiceId/send-notice).

Staging​

Deploy​

  1. Feature branch from main → PR → merge to main.
  2. Staging Cloud Run deploys automatically (deploy-staging-from-main).
  3. 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​

  1. Open staging PWA as platform admin → Platform Billing → Collection Jobs.
  2. Always run Next run projection first — confirm clients and amounts.
  3. Only then Run now (optional billingPeriod).
  4. Verify history/totals and email delivery (staging SendGrid).

Make changes → test again on staging​

What you changedHow to retest
Projection / display / rate mathDeploy → Refresh projection (no wipe)
Close / factura / notice servicesUnit tests locally → deploy → projection → one close
Need a second full close of the same monthPrefer 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​

  1. Local: projection until numbers look right.
  2. Local: unit tests for the services you touched.
  3. Local: one Run now + verify history; wipe or change period if you need another close.
  4. Staging: deploy → projection must match local expectation → one Run now if you need a live write/email check.
  5. 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 immediate outside 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 merely sale_id present.
  • 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.