Skip to main content

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 / ownerAccess Control.


TableRole
platform_legacy_sales_totalOne row per (platform_account_branch_id, billing_period) — what the Jobs table lists
platform_account_branchActive branch; FK target of the sales total
locationlegacy_location_code matches ingest locationCode; name shown in UI
businesslegacy_db_name matches ingest dbName; name shown in UI
platform_accountTenant billing account
customerLinks account → business via represents_business_id
sale / order_billNative 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

MethodPathAuthbillingPeriod
POST/platform-billing/legacy-sales-totalsAuthorization: Bearer <LEGACY_SALES_INGEST_SECRET> (optional IP allowlist)Required (YYYY-MM)
POST/platform-billing/jobs/legacy-sales-totalsFirebase ID token + PlatformAdminGuardRequired (YYYY-MM)
GET/platform-billing/jobs/legacy-sales-totalsFirebase ID token + PlatformAdminGuardOptional — defaults to prior calendar month
GET/platform-billing/jobs/projectionSame as GET aboveOptional — same default

Production push path: SQL Server Agent (PA_Monthly_Sales_1UploadMonthlySales) → 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 POST
  • LEGACY_SALES_INGEST_IP_ALLOWLIST — optional comma-separated IPs (empty = any IP in local/dev)

Close gate

PeriodCloser will not invoice a percentage branch that:

  1. Has legacy_location_code set, and
  2. Has zero native sales for the period, and
  3. Has no platform_legacy_sales_total row 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:

FieldRequiredNotes
billingPeriodYesYYYY-MM
rows[].dbNameYesExact business.legacy_db_name
rows[].locationCodeYesExact location.legacy_location_code (integer)
rows[].salesAmountYes≥ 0; legacy feed is tax-inclusive (T_Tra_M)
rows[].documentCountNo≥ 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
}'