GET /api/cma/alert-rules/dynamic-thresholds 在 {c.name: getattr(c, c.name) for c in
DynamicThresholdCache.__table__.columns} 里把 Column 对象当 ORM 行取属性,触发
AttributeError: 'Comparator' object has no attribute 'id'。改为列名取自表元数据、
值取自 query 结果行;entity_id 过滤与 limit 50/kpi_id 分支不变。
(cherry picked from commit 4130726be411a74a3318d9dc6bbc518a6c60d0ff)
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.
task dama-entity-isolation-sweep-20260923 (sections 2-2 4/5, merged by projectBot 2026-09-23 18:2x)
4) operation_logs write side: every audit write carries the account (no DDL, no column added)
- new app/utils/oplog.py::log_op() single wrapper: sets detail.entity_id; scope=global for
by-design non-entity writes (platform dict / role-level); scope=batch + OPLOG_BATCH_NO_ENTITY
sentinel for cross-account batches; no context -> OPLOG_NO_ENTITY sentinel (never guess)
- 16 remaining construction points converted (data/alert_rules/bi_reports/kpi_causality/
bot_bridge x2/budget_generate/driver_budget/kpis _log x3/templates _log x2/erp_sync);
pre-4 state had 20 sites without entity_id, now 0 outside the wrapper
- read side untouched (knowledge_service still user_id fallback); history not backfilled
5) docs/entity-dimension-data-dictionary.md: 8-table three-state registry
(kpi_templates/driver_factor_templates/scenario_suggestions/bot_bridge_config = non-entity;
kpi_subject_map = inherited via kpi_id; analysis_results/subjects = account dimension,
legacy rows still NULL -> other task; operation_logs = non-entity column + detail.entity_id)
Evidence: RED 2 failed/7 passed (reverted call sites) -> GREEN full 1428 passed; 9 new tests
including two read-back rows; app md5 restore verified identical; no cross-account writes
LOCAL commit only - push is gated (ahead contains the P1 entity-dim batch commit fb4609b)
- alert_entity_scope: add is_attributable_period / guard_alert_period (R5) and entity_alert_guard (R1, any write path)
- alert_rules.run_all_alert_checks: skip non-YYYY-MM period alert rows (R5)
- bot_bridge / bot_bridge_v2: R1 demo-entity guard + R5 period guard on write paths
- tests: new R5 suite (10 cases)
Not pushed / not deployed (release gate held by 任总).
R1: exclude status=demo entities from alert traversal (alert_generator / deviation_engine / kpi_bep_deriver / alert_rules / cash engine)
R2: only generate alerts within the last 6 calendar months (incl. current)
R3: only regular month periods (YYYY-MM) participate
R4: decouple generation from push - empty notification_channels never blocks
Single source of truth: app/utils/alert_entity_scope.py