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.
Kaliber: wecom-finance 2026-10-02 ruling (Boss 11:59 CST "li; but (1) add real ROI").
- kpi_definitions seed scripts/sql/profitability_kpi_defs_20261003.sql:
F_ROI renamed to F_ROA (same kpi_id=20, all 8 historical values kept), new F_ROI
(real IMA caliber = operating profit / invested capital), F_EVA activated with unit
and thresholds aligned to 万元, new F_RI (residual income).
- parameters are NOT hardcoded in formula text: they live in
kpi_definitions.data_source_config.params (tax_rate / wacc / min_return_rate /
include_factory_subsidy / include_channel_subsidy / include_sales_discount /
annualize / capital_basis); single reader = app/utils/profitability_params.py.
- deriver scripts/kpi_profitability_deriver.py: YTD operating profit from
voucher_details (carry_forward=0 net), invested capital from subject_balances
level-1 rows only, idempotent upsert into kpi_values (source_type=derived, no
data_status downgrade, fail-closed when invested capital is 0).
- read-only endpoint GET /reports/value-management reads the derived rows only
(single source of truth, no recomputation) and exposes the parameter block.
- DupontAnalysis.vue: value management section (ROI / ROA / ROE / EVA / RI).
- caliber_units.py: RATIO_CALIBER_TABLE key F_ROI renamed to F_ROA, new F_ROI row.
- tests: backend/tests/test_profitability_indicators.py (21 cases).
3 new tables + idempotent DDL migration (prod guard: refuses DB name cma without approval): tax_filing_log (P_TAX_TIMELY), hr_monthly_ledger (L_EMPLOYEE_TURNOVER), licence_ledger (P_LICENCE_OK).
Entry API /api/cma/ext-ledger: create / CSV batch import / query by entity+period / derive trigger. on_time is server-derived only; HR rows rejected when end != begin + hires - separations.
Deriver writes kpi_values only (source_type=derived, idempotent upsert); denominator 0 returns None and writes nothing; never creates or changes kpi_definitions.
Tests: 22 new pytest cases (22 passed); scratch-DB e2e battery 28 assertions PASS with zero production write.