Platform Billing — Legacy Sales Totals
Percentage-plan branches (sales_percentage / variable on sales) bill
% × period sales. Native FlowPOS sums come from sale / order_bill.
Branches still partly on the legacy POS have little or no native sales, so
FIXX pushes monthly legacy totals into
platform_legacy_sales_total. Period close sums native + legacy.
Design:
specs/045-platform-billing-architecture/§4.6
Bootstrap / backfill CLI: Legacy RPA-ERP migration runbook §3.7
PWA: Platform Billing → Collection Jobs (/platform-billing/jobs) —
Waiting on legacy sales summary, pending uploads (Set amount /
Upload CSV), and received totals table.
Access: FIXX super / owner — Access Control.
Related tables
| Table | Role |
|---|---|
platform_legacy_sales_total | One row per (platform_account_branch_id, billing_period) — what the Jobs table lists |
platform_account_branch | Active branch; FK target of the sales total |
location | legacy_location_code matches ingest locationCode; name shown in UI |
business | legacy_db_name matches ingest dbName; name shown in UI |
platform_account | Tenant billing account |
customer | Links account → business via represents_business_id |
sale / order_bill | Native sales (summed separately at close; not written by ingest) |
Ingest resolution
POST row: dbName + locationCode
→ business.legacy_db_name + location.legacy_location_code
→ active platform_account_branch for that location
→ upsert platform_legacy_sales_total
Unique key: (platform_account_branch_id, billing_period). Re-POSTing the
same branch/period updates the amount (idempotent).
A branch is gated on legacy totals when
location.legacy_location_code is set (requiresLegacyTotal).
Endpoints
| Method | Path | Auth | billingPeriod |
|---|---|---|---|
| POST | /platform-billing/legacy-sales-totals | Authorization: Bearer <LEGACY_SALES_INGEST_SECRET> (optional IP allowlist) | Required (YYYY-MM) |
| POST | /platform-billing/jobs/legacy-sales-totals | Firebase ID token + PlatformAdminGuard | Required (YYYY-MM) |
| GET | /platform-billing/jobs/legacy-sales-totals | Firebase ID token + PlatformAdminGuard | Optional — defaults to prior calendar month |
| GET | /platform-billing/jobs/projection | Same as GET above | Optional — same default |
Production push path: SQL Server Agent (PA_Monthly_Sales_1 →
UploadMonthlySales) → machine POST (source legacy_sql_push).
Ops can also set amounts from Platform Billing → Collection Jobs:
per-branch Set amount or Upload CSV → admin POST
(source always manual_ops). Max 500 rows per request.
Replacing a row whose source is not already manual_ops requires
confirmOverwrite: true (409 with conflict details otherwise).
Env (backend):
LEGACY_SALES_INGEST_SECRET— shared secret for machine POSTLEGACY_SALES_INGEST_IP_ALLOWLIST— optional comma-separated IPs (empty = any IP in local/dev)
Close gate
PeriodCloser will not invoice a percentage branch that:
- Has
legacy_location_codeset, and - Has zero native sales for the period, and
- Has no
platform_legacy_sales_totalrow yet for that branch/period
It defers with status waiting_legacy_sales instead of billing zero.
A row of 0 clears the gate. Once the branch has native FlowPOS sales,
the gate clears without a legacy row.
Check counts and accounts on Next run projection
(GET /platform-billing/jobs/projection) before Run now.
Ops curl examples (local)
1. Ingest (load rows into the Jobs table)
export API_URL="http://localhost:4000"
export INGEST_SECRET="$LEGACY_SALES_INGEST_SECRET" # from backend .env / Doppler
curl -sS -X POST \
"${API_URL}/platform-billing/legacy-sales-totals" \
-H "Authorization: Bearer ${INGEST_SECRET}" \
-H "Content-Type: application/json" \
-d '{
"billingPeriod": "2026-06",
"rows": [
{
"dbName": "POS_CTE_1_392",
"locationCode": 2,
"salesAmount": 5000,
"documentCount": 120
}
]
}' \
| jq .
Expect matchedCount >= 1 and empty unmatched. If
reason: "unmatched_branch", fix legacy_db_name /
legacy_location_code (or ensure an active
platform_account_branch exists) — nothing was written.
Payload fields:
| Field | Required | Notes |
|---|---|---|
billingPeriod | Yes | YYYY-MM |
rows[].dbName | Yes | Exact business.legacy_db_name |
rows[].locationCode | Yes | Exact location.legacy_location_code (integer) |
rows[].salesAmount | Yes | ≥ 0; legacy feed is tax-inclusive (T_Tra_M) |
rows[].documentCount | No | ≥ 0 |
2. List totals (same data as the Jobs table)
export TOKEN="<Firebase ID token of FIXX super/owner>"
curl -sS \
-H "Authorization: Bearer ${TOKEN}" \
"${API_URL}/platform-billing/jobs/legacy-sales-totals?billingPeriod=2026-06" \
| jq .
3. Manual set (PlatformAdmin / Jobs UI)
Row identity is either platformAccountBranchId or
dbName + locationCode (not both). Source is stamped manual_ops.
curl -sS -X POST \
"${API_URL}/platform-billing/jobs/legacy-sales-totals" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"billingPeriod": "2026-06",
"confirmOverwrite": false,
"rows": [
{
"platformAccountBranchId": "<uuid>",
"salesAmount": 5000,
"documentCount": 120
}
]
}' \
| jq .
If a conflict response includes
LEGACY_SALES_OVERWRITE_CONFIRM_REQUIRED, retry with
"confirmOverwrite": true.
Omit billingPeriod to use the prior calendar month. On the Jobs page,
set the period override to the same YYYY-MM and refresh.
3. Projection (waiting / received counts)
curl -sS \
-H "Authorization: Bearer ${TOKEN}" \
"${API_URL}/platform-billing/jobs/projection?billingPeriod=2026-06" \
| jq '{
billingPeriod,
waitingLegacySalesCount,
legacySalesReadyCount,
projectedAccountCount,
totalAmount
}'
Related guides
- Collection Loop — close job,
sales_percentagerates - Testing and Iteration — projection dry-run before close
- Legacy RPA-ERP migration §3.7 — CLI backfill, tax basis, reconcile notes