Saltar al contenido principal

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.