Appearance
v1.2 — 21/04/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
| Khóa mặc định implementation cho role seed, raw source seed v1, confidence rubric và hybrid refresh | C2, C3, C5, C6, C8, C11 | BE, FE, TL, QA |
Đồng bộ report.group_id, bổ sung state transition rules, Hasura permission metadata và trace tới test cases | C5, C6, C8, C12 | BE, FE, TL, QA |
| Khởi tạo dev spec cho LTV Phase 1 | Toàn file | BE, FE, TL |
Dev Spec — LTV Phase 1
Ref: PRD v1.2 | Date: 21/04/2026
Dev spec này chốt contract triển khai cho lớp canonical LTV, mapping version, backfill orchestration và report/query layer.
Nên đọc theo thứ tự C3 Rules, C4 Data Model, C5 Query Contract, C6 State Transition Rules, rồi đến C12 Traceability.
C1) Scope & Architecture Overview
text
┌──────────────┐ GraphQL / Action ┌─────────────────────┐
│ diva-admin │ ──────────────────────→ │ Hasura + Go service │
│ report FE │ │ / scheduler layer │
└──────┬───────┘ └─────────┬───────────┘
│ │
│ │
│ ┌────────────▼────────────┐
│ │ PostgreSQL (ecommerce) │
│ │ customer_revenue │
│ │ customer_revenue_stats │
│ │ ltv_* new tables │
│ └─────────────────────────┘
│
└──── Export / report queries đọc từ lớp snapshot + aggregate mớiDeliverable kỹ thuật phase 1
- Reuse
customer_revenue/customer_revenue_statslàm nền doanh thu customer-level. - Tạo lớp snapshot canonical cho:
LTV chuẩnNguồn gốc LTV chuẩnKênh nguồn chi tiếtmapping_version
- Tạo report queries cho:
- summary cards
- aggregate tables
- customer detail table
- data quality panel
- Tạo admin flows cho:
- quản lý mapping version/rules
- chạy backfill
- manual override source snapshot
Không thuộc scope phase 1
- Multi-touch attribution
- Forecast / predictive LTV
- Rebuild toàn bộ
customer_revenue - Dashboard chiến lược 6 bài toán đầy đủ
C2) Impact Map
| Area | Existing asset | Action |
|---|---|---|
| Revenue event store | customer_revenue | Reuse |
| Revenue aggregate | customer_revenue_stats | Extend / đọc nền |
| Raw customer source | ecommerce_user.customer_source | Reuse as raw only |
| Raw source catalog seed | master_data(type = customer_source) | Freeze toàn bộ active catalog vào mapping version v1 |
| Report group | customer_cycle_report_group | Extend FE route/tab + report registry |
| Report role seed | customer_cycle_report / customer_cycle_overview_report đang seed bod, hr_leader, it_leader | Reuse cùng role set cho customer_cycle_ltv_report ở phase 1 |
| Mapping / snapshot | Chưa có | Build mới |
| Backfill orchestration | Chưa có flow riêng | Build mới |
C3) Rules / Formulas (Implementation Delta)
FORMULA-001: Base LTV
- Ref: PRD A10
FORMULA-001 - Implementation note: không tính lại từ order total; ưu tiên đọc từ canonical revenue events ở
customer_revenue. - Revenue events được cộng:
invoice_payment,invoice_payment_walletnếu là actual revenue hợp lệ. - Reverse events được trừ:
refund_wallet,refund_cashvà các adjustment đảo chiều hợp lệ khác nếu đã ghi vàocustomer_revenue. - Source-of-truth cho payment eligibility: join từ
customer_revenue.reference_idsanginvoice.idcho các action typeinvoice_paymentvàinvoice_payment_wallet, sau đó đọcinvoice.payment_method_id. - Rule loại trừ ví khuyến mãi: chỉ cộng event wallet khi
invoice.payment_method_id = 'wallet'; nếuinvoice.payment_method_id = 'wallet_promotion'thì loại khỏi canonical LTV. - Rule an toàn: nếu payment event không join được về invoice hoặc không xác định được payment method hợp lệ thì không tự đoán; event đó phải bị loại khỏi canonical aggregate cho đến khi có evidence rõ.
FORMULA-002: First Paid Date
- Ref: PRD A10
FORMULA-002 - Implementation note: lấy min của payment event hợp lệ theo
invoice.paid_at. - Do not use:
order.created_at,order.paid_at.
FORMULA-003: Average LTV by Source Group
- Ref: PRD A10
FORMULA-003 - Implementation note: aggregate trên snapshot customer-level đã canonicalized, không aggregate trực tiếp từ raw source hiện tại.
FORMULA-004: Cohort Filter Semantics
- Ref: PRD A10
FORMULA-004 - Implementation note: filter thời gian chỉ lọc tập customer theo
first_paid_at;ltv_amountvẫn là all-time.
ATTR-001: Canonical Source Assignment
- Ref: PRD FR-002, FR-004
- Rule:
1 customer = 1 normalized source group = 1 detailed source channel. - Source of truth ưu tiên:
- manual override hợp lệ
- historical evidence khó sửa / record có timestamp gần
first_paid_at - raw source hiện tại nếu pass
safe-to-infer - fallback
unknown
- Output fields:
source_group,source_channel,determination_method,confidence_level,mapping_version_id
ATTR-002: Branch / Service Semantics
- Ref: PRD DEC-013, DEC-014
- Rule:
first_paid_branch_idvàinitial_service_groupđược derive từ giao dịch hợp lệ đầu tiên. - Not allowed: phân bổ multi-branch hoặc multi-service lifecycle trong phase 1.
ATTR-003: Confidence Rubric
- Ref: PRD PD-005, DEC-020
- Rule:
high: cómanual_overridehợp lệ hoặc có direct evidence gắn gầnfirst_paid_atđủ để map dứt khoátmedium: chỉ có đúng 1 raw source hiện hành hợp lệ và không có tín hiệu mâu thuẫnunknown: raw source trống, nhiều raw source, hoặc evidence mâu thuẫn / không đủ chắc
- Do not use: tự suy diễn
highchỉ vì raw source hiện tại có giá trị. - Expected output:
confidence_levelphải deterministic cho cùng 1 tập evidence.
C4) Data Model (SQL)
Existing tables / views
| Table / view | Purpose |
|---|---|
customer_revenue | Revenue events theo customer |
customer_revenue_stats | Aggregate revenue hiện có theo customer |
ecommerce_user | Raw source hiện tại, profile customer |
New tables đề xuất
1. ltv_customer_snapshot
| Column | Type | Notes |
|---|---|---|
customer_id | text | Align với customer_revenue.customer_id |
ltv_amount | bigint | Base LTV canonical |
first_paid_at | timestamptz | Derived |
last_paid_at | timestamptz | Derived |
first_paid_branch_id | uuid nullable | Branch semantic |
initial_service_group | text nullable | Service semantic |
snapshot_at | timestamptz | Last refresh time |
| audit fields | standard | created_at, updated_at, ... |
2. ltv_source_mapping_version
| Column | Type | Notes |
|---|---|---|
id | uuid | PK |
code | text | Human-readable version code |
status | text | draft, published, archived |
effective_from | timestamptz | Start effect |
effective_to | timestamptz nullable | End effect |
notes | text nullable | Notes |
| audit fields | standard |
3. ltv_source_mapping_rule
| Column | Type | Notes |
|---|---|---|
id | uuid | PK |
mapping_version_id | uuid | FK -> version |
raw_source_id | text | Master data ID / raw identifier |
raw_source_name_snapshot | text | Snapshot label |
normalized_source_group | text | Canonical group |
detailed_source_channel | text | Detailed channel |
priority | int | Tie-break nếu cần |
status | text | active/inactive |
| audit fields | standard |
4. ltv_customer_source_snapshot
| Column | Type | Notes |
|---|---|---|
customer_id | text | PK / unique |
source_group | text | Canonical group |
source_channel | text | Detailed channel |
mapping_version_id | uuid nullable | Version used |
determination_method | text | auto, manual_override, unknown |
confidence_level | text | high, medium, unknown |
evidence_payload | jsonb | Evidence used to determine |
determined_at | timestamptz | Snapshot time |
| audit fields | standard |
5. ltv_customer_source_audit
| Column | Type | Notes |
|---|---|---|
id | uuid | PK |
customer_id | text | customer |
old_source_group | text | before |
new_source_group | text | after |
old_source_channel | text | before |
new_source_channel | text | after |
reason | text | required |
changed_by | text/uuid | actor |
changed_at | timestamptz | audit time |
6. ltv_backfill_job
| Column | Type | Notes |
|---|---|---|
id | uuid | PK |
scope_type | text | full, customer_ids, cohort_range |
mapping_version_id | uuid | version applied |
status | text | queued, running, done, failed |
started_at | timestamptz | |
finished_at | timestamptz nullable | |
summary_payload | jsonb | totals, unknown count, error count |
Indexes đề xuất
ltv_customer_snapshot(customer_id)ltv_customer_snapshot(first_paid_at)ltv_customer_snapshot(first_paid_branch_id)ltv_customer_snapshot(initial_service_group)ltv_customer_source_snapshot(source_group)ltv_customer_source_snapshot(source_channel)ltv_customer_source_snapshot(determination_method, confidence_level)ltv_source_mapping_rule(mapping_version_id, raw_source_id)- unique active mapping guard theo
mapping_version_id + raw_source_id
C5) API / Hasura / Query Contract
Queries
| Name | Purpose | Output |
|---|---|---|
search_report_ltv_overview | Cards + aggregate tabs | summary + grouped rows |
search_report_ltv_customers | Bảng khách hàng | paginated rows |
search_report_ltv_data_quality | Panel data quality | unknown stats, unmapped raw source, last jobs |
search_ltv_source_mapping_versions | Drawer config | version list |
search_ltv_source_mapping_rules | Drawer config | rules by version |
search_ltv_source_mapping_preview | Preview runtime trong SCR-02 | mapped/unmapped count + sample mapped rows + effective window |
Actions / mutations
| Name | Purpose |
|---|---|
upsert_ltv_source_mapping_version | Tạo / sửa version |
upsert_ltv_source_mapping_rules | Seed / update rules |
publish_ltv_source_mapping_version | Publish version |
run_ltv_backfill | Tạo backfill job |
override_customer_ltv_source | Manual override một customer |
FE route / registry
- Extend
CustomerCycleReport.tsx:- thêm tab key
customer_cycle_ltv - render component mới
CustomerCycleLtvReport
- thêm tab key
- Extend report metadata:
- report id mới đề xuất:
customer_cycle_ltv_report - backend
report.group_id = customer_cycle_report_groupđể cùng nhóm seed report hiện có - backend
report_roleseed mặc định phase 1:bod,hr_leader,it_leader - FE route / placement vẫn nằm tại
/r/reports/customer_cycle_report_group - FE report tree / permission lookup phải map report mới vào nhóm
customer_cycle_report_groupđể card/menu và tab discoverability hoạt động đúng
- report id mới đề xuất:
- Export contract:
- reuse pattern export của report module hiện có
- export dùng đúng committed filter state của SCR-01
- cột export theo cùng thứ tự business của customer table / aggregate tab đang được chọn
C6) Scheduler / Backfill Strategy
Job types
| Job | Trigger | Purpose |
|---|---|---|
| Initial full backfill | Manual bởi Admin/IT | Tạo snapshot cho khách cũ |
| Incremental refresh | Event-driven từ payment/refund/override signal hợp lệ | Refresh customer vừa phát sinh thay đổi mới |
| Scheduled reconcile | Scheduler định kỳ | Soát drift, retry case miss event, không rewrite lịch sử nếu không rerun explicit |
| Mapping publish follow-up | Manual chọn rerun | Chỉ rerun scope hợp lệ khi business chủ động |
Refresh strategy default
- Runtime mặc định phase 1 là hybrid:
- event-driven để enqueue incremental refresh khi có payment/refund mới hoặc manual override
- cron reconcile để bắt case miss event / retry an toàn
- manual backfill cho initial load, rerun theo scope, và thay đổi mapping version đã publish
- Không dùng scheduler định kỳ như nguồn refresh duy nhất cho case payment/refund mới.
Algorithm mức cao
- Xác định tập customer cần xử lý.
- Tính lại
ltv_customer_snapshottừ revenue events canonical. - Tìm
first_paid_at,first_paid_branch_id,initial_service_group. - Xác định source evidence theo thứ tự ưu tiên.
- Map qua
mapping_versionđang áp dụng. - Upsert
ltv_customer_source_snapshot. - Ghi
ltv_backfill_job.summary_payload.
Safety rules
- Publish mapping version mới không tự động rewrite snapshot lịch sử.
- Rerun backfill phải explicit theo action của
Admin/IT. - Unknown là output hợp lệ.
- Scheduled reconcile chỉ sửa snapshot đang active khi evidence nguồn thay đổi hợp lệ; không remap snapshot lịch sử sang mapping version mới nếu chưa rerun explicit.
State Transition Rules
LIFECYCLE-001: Mapping version
| from_state | event | guard | to_state | side effects | error / notes |
|---|---|---|---|---|---|
none | create_draft | actor có quyền Admin/IT | draft | Tạo row mới ở ltv_source_mapping_version, có thể clone rule từ version published gần nhất | Không đụng snapshot lịch sử |
draft | save_draft | version code hợp lệ | draft | Persist rules + preview data | Validation fail giữ nguyên draft |
draft | publish | Có effective_from, không còn rule bắt buộc bị thiếu, actor có quyền | published | Lock row, mở hiệu lực cho khách mới / snapshot mới | Không auto tạo backfill job |
published | clone_to_draft | actor có quyền | draft (record mới) | Copy rules sang version mới để chỉnh tiếp | Source row published vẫn giữ nguyên |
published | archive | Không vi phạm guard active version của tenant/workflow | archived | Chỉ đổi trạng thái version nguồn | Không rewrite snapshot đã có |
LIFECYCLE-002: Backfill job
| from_state | event | guard | to_state | side effects | error / notes |
|---|---|---|---|---|---|
none | run_backfill | actor Admin/IT, chọn scope + mapping version | queued | Tạo ltv_backfill_job mới | Mỗi lần rerun tạo row job mới |
queued | worker_claimed | worker lock thành công | running | Ghi started_at | Nếu claim fail thì vẫn queued |
running | completed | Snapshot + summary ghi thành công | done | Upsert ltv_customer_snapshot, ltv_customer_source_snapshot, ghi summary_payload, finished_at | Report chỉ đọc số mới sau khi state = done |
running | failed | Có runtime / validation error | failed | Ghi lỗi + finished_at | Giữ snapshot hợp lệ gần nhất, không rollback về dữ liệu rỗng |
failed | rerun | actor Admin/IT xác nhận | queued (job mới) | Tạo job mới cùng/sửa scope | Không mutate row failed |
C7) Migration Plan
| Step | Migration / task | Owner |
|---|---|---|
| 1 | Tạo bảng ltv_source_mapping_version, ltv_source_mapping_rule | BE |
| 2 | Tạo bảng ltv_customer_snapshot, ltv_customer_source_snapshot, ltv_customer_source_audit, ltv_backfill_job | BE |
| 3 | Seed taxonomy nhóm nguồn chuẩn mặc định | BE + PO |
| 4 | Seed mapping version v1 từ toàn bộ active master_data(type = customer_source) với raw_source_id + raw_source_name_snapshot | BE + Admin/IT |
| 5 | Tạo query/functions/materialized layer cho overview/customers/data-quality | BE |
| 6 | Expose Hasura metadata / actions + hook incremental/event reconcile | BE |
| 7 | FE thêm tab report + mapping drawer + override dialog | FE |
| 8 | Chạy initial backfill trên staging | BE / Admin/IT |
| 9 | QA sanity + business UAT | QA / PO |
C8) Security / RBAC
| Capability | Rule |
|---|---|
| View report | Theo report_role của customer_cycle_report_group / report con; seed mặc định phase 1 dùng bod, hr_leader, it_leader |
| Export | Cùng quyền xem report |
| Mapping config | Chỉ Admin/IT |
| Publish mapping version | Chỉ Admin/IT |
| Run backfill | Chỉ Admin/IT |
| Manual override | Chỉ Admin/IT, bắt buộc reason |
Hasura permission metadata
| Artifact | Role | Permission shape |
|---|---|---|
search_report_ltv_overview, search_report_ltv_customers, search_report_ltv_data_quality | role có report_role hợp lệ của customer_cycle_report_group / report con | execute / select theo filter backend đã canonicalize |
search_ltv_source_mapping_versions, search_ltv_source_mapping_rules, search_ltv_source_mapping_preview | Admin/IT | select / action execute only |
ltv_source_mapping_version, ltv_source_mapping_rule | service role / Admin/IT | insert/update chỉ qua action layer; không expose edit trực tiếp cho role report thường |
ltv_customer_snapshot, ltv_customer_source_snapshot | service role, query layer | FE report chỉ đọc qua query contract, không update trực tiếp |
publish_ltv_source_mapping_version, run_ltv_backfill, override_customer_ltv_source | Admin/IT | action execute only, bắt buộc audit actor |
Audit requirements
- Mọi publish mapping version phải có audit actor + timestamp.
- Mọi override customer source phải ghi vào
ltv_customer_source_audit. - Backfill jobs phải lưu summary kết quả.
C9) NFR
| Area | Requirement |
|---|---|
| Timezone | Tất cả semantics date theo Asia/Ho_Chi_Minh |
| Performance | Query list/aggregate phải chịu được cohort filter phổ biến mà không full scan không kiểm soát |
| Consistency | Report không đọc trực tiếp ecommerce_user.customer_source để hiển thị canonical source |
| Recoverability | Có thể rerun backfill theo scope |
| Export | Export phản ánh đúng filter + semantics hiện tại |
C10) Observability
| Signal | Detail |
|---|---|
| Job logs | backfill_job_id, totals, unknown_count, error_count |
| Query metrics | latency cho overview/customers/data-quality |
| Audit logs | mapping publish, override submit |
| Data quality metrics | unknown_ratio, manual_override_ratio, unmapped_raw_source_count |
C11) Implementation Tasks
BE
- Tạo schema/migrations cho
ltv_*tables. - Tạo canonical calculator cho
ltv_customer_snapshot. - Tạo source attribution resolver + confidence model.
- Tạo mapping version CRUD + publish flow.
- Seed mapping version v1 từ full active
master_data(type = customer_source)và snapshot tên source tại thời điểm cutover. - Tạo preview query cho drawer mapping (
mapped/unmapped count, sample output). - Tạo backfill job orchestration + state handling
queued/running/done/failed. - Tạo event-driven incremental refresh cho payment/refund/override.
- Tạo scheduled reconcile job để quét drift / miss-event an toàn.
- Expose Hasura metadata/actions/queries.
FE
- Extend
CustomerCycleReport.tsxvới tabLTV Phase 1. - Build
CustomerCycleLtvReportpage. - Build aggregate tabs + customer table + data quality panel.
- Build mapping drawer cho
Admin/IT, gồm draft/published/archived state handling + preview block. - Build override dialog + audit display tối thiểu.
- Export theo dataset hiện tại.
- Deeplink từ publish success về SCR-01 để chạy backfill explicit.
QA / UAT
- Seed mapping version đầu tiên.
- Verify unknown / override / rerun cases.
- Verify semantics time/branch/service trên UI labels.
C12) Traceability
| FR | UI | BE / DB | QA TC | Notes |
|---|---|---|---|---|
| FR-001 | SCR-01 | ltv_customer_snapshot + calculator | TC-001-01 → TC-001-06 | Base LTV |
| FR-002 | SCR-01, SCR-03 | ltv_customer_source_snapshot | TC-002-01 → TC-002-03, TC-008-04 | Source snapshot |
| FR-003 | SCR-02 | ltv_source_mapping_version, ltv_source_mapping_rule, search_ltv_source_mapping_preview | TC-003-01 → TC-003-06 | Versioned mapping + lifecycle |
| FR-004 | SCR-01 | ltv_backfill_job + resolver | TC-004-01 → TC-004-09 | Backfill + confidence + explicit rerun + hybrid refresh |
| FR-005 | SCR-01 customer table | search_report_ltv_customers | TC-005-01 → TC-005-05 | Customer list |
| FR-006 | SCR-01 aggregate tabs | search_report_ltv_overview | TC-006-01 → TC-006-04 | Management summary |
| FR-007 | SCR-01 data quality | search_report_ltv_data_quality | TC-007-01 → TC-007-03 | Data quality |
| FR-008 | SCR-03 | override_customer_ltv_source + audit table | TC-008-01 → TC-008-05 | Manual override + confidence escalation |
| FR-009 | SCR-01 wording + tooltip | Query semantics by first paid / initial service | TC-009-01 → TC-009-04 | Semantic lock |
| FR-010 | Out-of-scope guard | No multi-touch / forecast | TC-010-01 → TC-010-02 | Scope guard |