Customer revenue/concentration now filters bill_type IN (1,4,14) (XSD/XSTHD/XSHHD).
Non-sales docs (JHD purchase, YFKD prepay, YSKD accept, QTRK, SKD, YBFYD) carry SUPPLIER unit_id and were polluting 'customer concentration' (old Top1/Top2 were our biggest suppliers; root cause of 6.04x base gap).
Unify sign filter to (sum_money+sum_tax_money)>0 (equivalent to money_only on current data, diff 0 rows) and correct the withdrawn 407-row/17.2% wording in cpa_caliber docstring.
Deriver: anchors from cpa_caliber single source; new --concentration overwrite and --only concentration (minimal 9-row write face) with pre-overwrite backup.
Tests: fixture gains bill_type/sum_tax_money; +5 whitelist cases (44 passed targeted).
Task: finance-20261003-cpa-customer-profitability-mvp-fullstack (P0-a / P0-b)
- backend/app/utils/cpa_caliber.py (new): single source of truth for
customer-level revenue aggregation from zxt_masterbill. Reused by
alert_rules._customer_revenue_shares (dedupe: no second implementation).
- backend/scripts/migrate_cpa_snapshot_20261004.py (new): idempotent DDL for
customer_profitability_snapshot (UNIQUE entity_id+period+unit_id),
--check / --apply / --rollback / --status / --show-create + journal.
- backend/scripts/kpi_customer_profitability_deriver.py (new): monthly
snapshot + kpi_values upsert (single transaction, idempotent).
fail-closed: cogs_alloc / chan_sub / contrib / contrib_rate stay NULL and
are never written as 0 or as an apportioned value.
- backend/scripts/sql/cpa_kpi_defs_20261004.sql (new): C_CUST_REVENUE and
C_CUST_COUNT active; 5 P1 codes registered inactive with fail-closed reason.
- backend/app/api/customer_dashboard.py: new read-only endpoint
GET /api/cma/customer-dashboard/standard-indicators returning the 9-card
tri-state (value / no_data / undefined). Reads kpi_definitions via raw SQL
to stay decoupled from the in-flight KPI direction DDL.
- frontend/src/views/CustomerDashboard.vue: 9-card panel rendering the three
states (never renders an empty value as 0).
Anchor check 2026-01..09 (offline sqlite rehearsal, ALL_PASS=true):
73 customers / 4883.52 wan / top1 25.3835 / top2 23.4905 / top4 63.3249 /
top10 81.7675 - all six match the task acceptance anchors.
No production write and no deploy in this commit; release is gated.