@sguild/dispatcher 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/contracts/README.md +30 -0
- package/contracts/coach-availability/README.md +355 -0
- package/contracts/coach-availability/README.v2.md +263 -0
- package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
- package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
- package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
- package/contracts/coaching-confirmation/README.md +96 -0
- package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
- package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
- package/contracts/credit-reservation-funding-state/README.md +147 -0
- package/contracts/credit-reservation-lock/README.md +433 -0
- package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
- package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
- package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
- package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
- package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
- package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
- package/contracts/event-envelope/README.md +205 -0
- package/contracts/event-envelope/schema/envelope-v1.json +2 -2
- package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
- package/contracts/event-types-registry.json +337 -24
- package/contracts/external-actions/README.md +338 -0
- package/contracts/finance-mart/README.md +238 -0
- package/contracts/finance-mart/cac-payback.md +113 -0
- package/contracts/finance-mart/cash-position.md +98 -0
- package/contracts/finance-mart/cohort-summary.md +72 -0
- package/contracts/finance-mart/customer-journey-audit.md +92 -0
- package/contracts/finance-mart/ltv.md +92 -0
- package/contracts/finance-mart/margin.md +87 -0
- package/contracts/finance-mart/pnl.md +83 -0
- package/contracts/finance-mart/reconciliation.md +98 -0
- package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
- package/contracts/finance-mart/unit-economics.md +94 -0
- package/contracts/growth-warehouse-api/README.md +162 -0
- package/contracts/identity/README.md +184 -0
- package/contracts/identity/person-canonical-fields.md +120 -0
- package/contracts/identity/person-externals.md +267 -0
- package/contracts/identity/person-resolution-semantics.md +144 -0
- package/contracts/identity/person-role-taxonomy.md +120 -0
- package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
- package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
- package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
- package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
- package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
- package/contracts/identity/validation/client-table.md +131 -0
- package/contracts/identity/validation/coach-handling.md +100 -0
- package/contracts/identity/validation/person-graph.md +140 -0
- package/contracts/lead-lifecycle/README.md +187 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
- package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
- package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
- package/contracts/lesson-lifecycle/README.md +118 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
- package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
- package/contracts/lesson-lifecycle/validation/README.md +5 -0
- package/contracts/mart-consumer-api/README.md +108 -0
- package/contracts/order-flow/README.md +106 -0
- package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
- package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
- package/contracts/payment-flow/README.md +157 -0
- package/contracts/platform-comms/README.md +84 -0
- package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
- package/contracts/platform-geography-snapshot/README.md +205 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
- package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
- package/contracts/portfolio-mart/README.md +133 -0
- package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
- package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
- package/contracts/portfolio-mart/cross-market-performance.md +84 -0
- package/contracts/portfolio-mart/health-composites.md +88 -0
- package/contracts/portfolio-mart/org-topology.md +70 -0
- package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
- package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
- package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
- package/contracts/refund-flow/README.md +136 -0
- package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
- package/contracts/sales-scheduling-surface/README.md +532 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
- package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
- package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
- package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
- package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
- package/contracts/warehouse-silver/README.md +118 -0
- package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
- package/dist/events.d.ts +63 -0
- package/dist/events.js +293 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/postgres-consumer.js +2 -1
- package/dist/validator.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Finance Mart CAC Payback Manifest
|
|
2
|
+
|
|
3
|
+
**Status:** authoritative for finance-mart v2.0.x
|
|
4
|
+
**Date:** 2026-05-19
|
|
5
|
+
**Owner:** finance
|
|
6
|
+
**Sub-section:** cac_payback
|
|
7
|
+
**Related:** finance-mart README §4.6, 2026-05-19-finance-100-percent-deployment-decisions (Decisions 1 and 2: CAC cost decomposition and payback definition/granularity), 2026-05-19-platform-mart-round-status-and-next-silver-asks (Round 6 cac_per_first_lock wiring), 2026-05-19-platform-revenue-silver-wiring-landed (Round 7 canonical 4-class source_class enum)
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This manifest defines the `cac_payback` sub-section in the finance gold section. The sub-section answers: what does it cost to acquire a customer, and how long does the customer take to pay back that cost?
|
|
12
|
+
|
|
13
|
+
CAC composes Growth attribution silver with Sales journey silver (specifically `sales.lead_stage_history` for first-lock counts). Payback composes CAC with per-customer cumulative gross margin. Both are cohort-grain metrics, keyed by `(org_market_id, intake_cohort_week)`.
|
|
14
|
+
|
|
15
|
+
## Served sub-section
|
|
16
|
+
|
|
17
|
+
`GET /api/mart/sections/finance` exposes this sub-section at `groupings.cac_payback`. Three metrics: `cac` (with `cac_per_first_lock` as the source-class-split form Platform wired in Round 6), `cac_payback_weeks`, `payback_distribution`.
|
|
18
|
+
|
|
19
|
+
## Served columns
|
|
20
|
+
|
|
21
|
+
### `cac` / `cac_per_first_lock__<source_class>`
|
|
22
|
+
|
|
23
|
+
Grain: one row per `(org_market_id, intake_cohort_week)` for the aggregate `cac`; one row per `(org_market_id, intake_cohort_week, source_class)` for the source-class split form `cac_per_first_lock__<source_class>`.
|
|
24
|
+
|
|
25
|
+
| Column | Type | Required | Source | Notes |
|
|
26
|
+
|---|---|---:|---|---|
|
|
27
|
+
| `org_market_id` | string | yes | spine | |
|
|
28
|
+
| `intake_cohort_week` | string | yes | sales journey | ISO week, the cohort key. |
|
|
29
|
+
| `source_class` | string enum or null | conditional | revenue ad-spend-reconciled | Required on split metric; null on aggregate `cac`. One of `paid_social`, `paid_search`, `direct`, `other`. |
|
|
30
|
+
| `total_acquisition_cost_cents` | integer or null | yes | finance composition | Numerator: marketing + sales-SDR cost attributable to the cohort. |
|
|
31
|
+
| `first_lock_count` | integer or null | yes | sales.lead_stage_history | Denominator: cohort first-lock count. |
|
|
32
|
+
| `cac_cents` | integer or null | yes | finance composition | `total_acquisition_cost_cents / first_lock_count`. |
|
|
33
|
+
| `_cac_cost_decomposition` | string | yes | finance composition | `marketing_plus_sales_sdr`. |
|
|
34
|
+
| `null_reason` | string or null | yes | finance composition | `null:upstream_unavailable` while Sales SDR allocation silver pending; `null:insufficient_data` below cohort floor of 5 first-locks. |
|
|
35
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
|
|
36
|
+
|
|
37
|
+
Policy: `_cac_cost_decomposition: marketing_plus_sales_sdr`. Rollup rule: `weighted_mean_by_cohort_size`.
|
|
38
|
+
|
|
39
|
+
### `cac_payback_weeks`
|
|
40
|
+
|
|
41
|
+
Grain: one row per `(org_market_id, intake_cohort_week)`.
|
|
42
|
+
|
|
43
|
+
| Column | Type | Required | Source | Notes |
|
|
44
|
+
|---|---|---:|---|---|
|
|
45
|
+
| `org_market_id` | string | yes | spine | |
|
|
46
|
+
| `intake_cohort_week` | string | yes | sales journey | Cohort key. |
|
|
47
|
+
| `weeks_to_payback` | integer or null | yes | finance composition | First week W where cumulative gross margin per cohort customer ≥ `cac_cents`. |
|
|
48
|
+
| `_payback_definition` | string | yes | finance composition | `cumulative_gross_margin_geq_cac`. |
|
|
49
|
+
| `null_reason` | string or null | yes | finance composition | `null:insufficient_observation_window` for cohorts that haven't paid back yet but are observable. |
|
|
50
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
|
|
51
|
+
|
|
52
|
+
Policy: `_payback_definition: cumulative_gross_margin_geq_cac`. Rollup rule: `weighted_mean_by_cohort_size`.
|
|
53
|
+
|
|
54
|
+
### `payback_distribution`
|
|
55
|
+
|
|
56
|
+
Grain: one row per `(org_market_id, intake_cohort_week, payback_month_bucket)`.
|
|
57
|
+
|
|
58
|
+
| Column | Type | Required | Source | Notes |
|
|
59
|
+
|---|---|---:|---|---|
|
|
60
|
+
| `org_market_id` | string | yes | spine | |
|
|
61
|
+
| `intake_cohort_week` | string | yes | sales journey | |
|
|
62
|
+
| `payback_month_bucket` | string | yes | finance composition | `M1` through `M24+` per `_payback_bucket_granularity: monthly`. |
|
|
63
|
+
| `cohort_share_bps` | integer | yes | finance composition | Basis points of the cohort that paid back within this bucket. |
|
|
64
|
+
| `_payback_bucket_granularity` | string | yes | finance composition | `monthly`. |
|
|
65
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
|
|
66
|
+
|
|
67
|
+
Policy: `_payback_bucket_granularity: monthly`, `_payback_definition: cumulative_gross_margin_geq_cac`. The bucket dimension is dimensional, not summed.
|
|
68
|
+
|
|
69
|
+
## Source silver columns
|
|
70
|
+
|
|
71
|
+
| Silver object | Target column | Used for | Required |
|
|
72
|
+
|---|---|---|---:|
|
|
73
|
+
| Revenue `/api/v1/silver/ad-spend-reconciled` | `ad_spend_cents`, `source_class`, `week`, `org_market_id` | CAC numerator (marketing slice) | yes |
|
|
74
|
+
| `sales.lead_intake` (or equivalent) | `intake_week`, `customer_id`, `org_market_id` | cohort identity | yes |
|
|
75
|
+
| `sales.lead_stage_history` | first-lock event per customer | denominator | yes |
|
|
76
|
+
| `sales.sdr_time_allocation` (or equivalent) | per-cohort SDR cost allocation | CAC numerator (sales-SDR slice) | yes (Sales-side commitment, may be pending) |
|
|
77
|
+
| `revenue.revenue_recognition` | per-customer cumulative recognized revenue | payback observation | yes |
|
|
78
|
+
| `coaching.coach.lesson_rate`, `coaching.coach.travel_comp`, `delivery.lesson_event` | per-customer cumulative gross margin (revenue minus cost-of-service) | payback definition | yes |
|
|
79
|
+
| spine | `org_market_id` resolution | grain key | yes |
|
|
80
|
+
|
|
81
|
+
## Composition rule
|
|
82
|
+
|
|
83
|
+
`cac_cents`:
|
|
84
|
+
|
|
85
|
+
1. Sum `ad_spend_cents` from the Revenue `ad-spend-reconciled` endpoint over weeks falling within `intake_cohort_week`'s attribution window, per `(org_market_id, source_class)`. Filter to acquisition source classes (`paid_social`, `paid_search`); `direct` and `other` excluded.
|
|
86
|
+
2. Add Sales SDR-time allocation per cohort from `sales.sdr_time_allocation` (denominator: weeks × SDR-hours-per-week × SDR-rate-per-hour, attributed to the cohort).
|
|
87
|
+
3. Sum to `total_acquisition_cost_cents`.
|
|
88
|
+
4. Read `first_lock_count` from `sales.lead_stage_history` for the cohort.
|
|
89
|
+
5. If `first_lock_count >= 5`, return `total_acquisition_cost_cents / first_lock_count`; otherwise emit `null:insufficient_data`.
|
|
90
|
+
|
|
91
|
+
`weeks_to_payback`:
|
|
92
|
+
|
|
93
|
+
1. For each cohort customer, walk weekly cumulative gross margin = sum of (recognized revenue − cost-of-service) per customer per week since intake.
|
|
94
|
+
2. For each cohort, find the earliest week W where average per-customer cumulative gross margin ≥ `cac_cents`.
|
|
95
|
+
3. Return W − intake_cohort_week as `weeks_to_payback`. If no week W is reached within the observation window, emit `null:insufficient_observation_window`.
|
|
96
|
+
|
|
97
|
+
`payback_distribution.cohort_share_bps[Mk]`:
|
|
98
|
+
|
|
99
|
+
1. Per cohort, count customers whose cumulative gross margin reaches `cac_cents` within month bucket Mk (M1 = first 4 weeks, M2 = weeks 5-8, ..., M24+ = beyond 24 months).
|
|
100
|
+
2. Return `round(count_in_bucket / total_cohort_size × 10000)` as basis points per bucket.
|
|
101
|
+
|
|
102
|
+
## Reconciliation tolerance
|
|
103
|
+
|
|
104
|
+
- `ad_spend_cents` numerator slice reconciles to the Revenue `ad-spend-reconciled` silver with tolerance `0` cents.
|
|
105
|
+
- Sales SDR-time allocation reconciles to `sales.sdr_time_allocation` with tolerance `0` cents once that silver lands.
|
|
106
|
+
- `first_lock_count` reconciles to `sales.lead_stage_history` with tolerance `0`.
|
|
107
|
+
- Cumulative gross margin per customer reconciles to the same Revenue + Coaching + Delivery composition the `margin` sub-section uses; tolerance `0` cents.
|
|
108
|
+
- The derived ratios (`cac_cents`, `weeks_to_payback`, `cohort_share_bps`) have no external reconciliation target.
|
|
109
|
+
|
|
110
|
+
## Known gaps
|
|
111
|
+
|
|
112
|
+
- **Sales SDR-time allocation silver is the outstanding gate** for the full CAC numerator. Today's Round 6 wiring of `cac_per_first_lock` uses marketing-only CAC (the `ad-spend-reconciled` slice); the marketing+sales-SDR form requires Sales to expose SDR allocation per cohort. Per Finance's `2026-05-19-finance-round-6-next-silver-asks-response` ask 3, Sales confirms whether the cohort-triangle silver carries SDR allocation; if not, Finance falls back to marketing-only CAC and supplements via a registry edit memo.
|
|
113
|
+
- **`cac_payback_weeks` and `payback_distribution`** stay at `null:upstream_unavailable` until cumulative gross margin per cohort customer is composable (gates on the same cost-of-service silver as `gross_margin` and `pnl_summary`, all of which are now live per Revenue's pushback routing).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Finance Mart Cash Position Manifest
|
|
2
|
+
|
|
3
|
+
**Status:** authoritative for finance-mart v1.0.0
|
|
4
|
+
**Date:** 2026-05-18
|
|
5
|
+
**Owner:** finance
|
|
6
|
+
**Grouping:** cash_position
|
|
7
|
+
**Related:** finance-mart README section 4.5, warehouse-silver v1.1.0, 2026-05-17-revenue-finance-mart-contract-reply
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This manifest defines the `cash_position` grouping in the finance gold section. The grouping answers: what cash position does the business hold as of a reporting date, by Organization and Market?
|
|
12
|
+
|
|
13
|
+
This is a cash-event reporting surface, not a bank-statement contract. It composes from Revenue's authoritative payment, refund, reservation, and credit ledger facts. If provider settlement data later becomes authoritative, that lands as an additive manifest revision or a minor contract change depending on the served surface.
|
|
14
|
+
|
|
15
|
+
## Served grouping
|
|
16
|
+
|
|
17
|
+
`GET /api/mart/sections/finance` exposes this grouping at `groupings.cash_position`.
|
|
18
|
+
|
|
19
|
+
The grouping grain is one row per:
|
|
20
|
+
|
|
21
|
+
- `reporting_date`
|
|
22
|
+
- `organization_id`
|
|
23
|
+
- `market_id`
|
|
24
|
+
|
|
25
|
+
`org_market_id` is the stable mart coordination key for the Organization and Market pair.
|
|
26
|
+
|
|
27
|
+
## Served columns
|
|
28
|
+
|
|
29
|
+
| Column | Type | Required | Source | Notes |
|
|
30
|
+
|---|---|---:|---|---|
|
|
31
|
+
| `reporting_date` | date string | yes | finance request or materialization date | As-of date for the position. |
|
|
32
|
+
| `organization_id` | string | yes | spine | Platform Organization ID for the discipline. |
|
|
33
|
+
| `organization_slug` | string | yes | spine | Stable display slug for the Organization. |
|
|
34
|
+
| `market_id` | string | yes | spine | Platform Market ID. |
|
|
35
|
+
| `market_slug` | string | yes | spine | Stable display slug for the Market. |
|
|
36
|
+
| `org_market_id` | string | yes | `OrgMarket.slug` | Stable mart key for the Organization and Market pair. |
|
|
37
|
+
| `cash_received_cents` | integer | yes | `revenue.order.amount_paid_cents` | Paid orders with `paid_at` on or before `reporting_date`. |
|
|
38
|
+
| `cash_refunded_cents` | integer | yes | `revenue.refund_item.amount_cents` | Refund cash out with `refunded_at` on or before `reporting_date`. |
|
|
39
|
+
| `net_cash_cents` | integer | yes | finance composition | `cash_received_cents - cash_refunded_cents`. |
|
|
40
|
+
| `reserved_credit_liability_cents` | integer | yes | `revenue.credit_reservation` and `revenue.credit_ledger` | Credits reserved against locks and not yet earned, released, forfeited, or refunded. |
|
|
41
|
+
| `unearned_credit_balance_cents` | integer | yes | `revenue.credit_ledger` | Purchased credits not yet recognized, refunded, forfeited, or adjusted away. |
|
|
42
|
+
| `cash_in_transit_cents` | integer | yes | `revenue.credit_reservation.funding_status`, payment status fields | Cash expected from provider or funding flow but not confirmed. |
|
|
43
|
+
| `refunds_in_transit_cents` | integer | yes | refund status fields | Refund cash out initiated but not confirmed. |
|
|
44
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | Refresh point of the materialized grouping. |
|
|
45
|
+
|
|
46
|
+
## Source silver columns
|
|
47
|
+
|
|
48
|
+
The implementation reads the Revenue silver face with these source-backed conformed columns:
|
|
49
|
+
|
|
50
|
+
| Silver object | Silver column | Required | Use |
|
|
51
|
+
|---|---|---:|---|
|
|
52
|
+
| `revenue.order` | `order_id` | yes | Cash receipt row identity. |
|
|
53
|
+
| `revenue.order` | `person_id` | yes where source row is person-related | Join to the spine. |
|
|
54
|
+
| `revenue.order` | `amount_paid_cents` | yes | Cash received. |
|
|
55
|
+
| `revenue.order` | `paid_at` | yes | Cash receipt date. |
|
|
56
|
+
| `revenue.order` | `payment_status` | yes | Distinguish paid, pending, failed, and reversed states where source-backed. |
|
|
57
|
+
| `revenue.order` | `provider_reference` | yes when source-backed | Variance explanation and provider tie-back. |
|
|
58
|
+
| `revenue.refund_item` | `refund_item_id` | yes | Refund row identity. |
|
|
59
|
+
| `revenue.refund_item` | `person_id` | yes where source row is person-related | Join to the spine. |
|
|
60
|
+
| `revenue.refund_item` | `amount_cents` | yes | Cash refunded. |
|
|
61
|
+
| `revenue.refund_item` | `refunded_at` | yes | Refund date. |
|
|
62
|
+
| `revenue.refund_item` | `refund_status` | yes | Distinguish confirmed and in-transit refunds. |
|
|
63
|
+
| `revenue.credit_reservation` | `credit_reservation_id` | yes | Reserved credit obligation identity. |
|
|
64
|
+
| `revenue.credit_reservation` | `person_id` | yes where source row is person-related | Join to the spine. |
|
|
65
|
+
| `revenue.credit_reservation` | `funding_status` | yes | `PENDING_FUNDING`, `FUNDED`, `REFUNDING`, `REFUNDED`, or equivalent conformed values. |
|
|
66
|
+
| `revenue.credit_reservation` | `reserved_amount_cents` | yes if source-backed | Reserved liability amount. |
|
|
67
|
+
| `revenue.credit_ledger` | `credit_ledger_entry_id` | yes | Ledger movement identity. |
|
|
68
|
+
| `revenue.credit_ledger` | `person_id` | yes where source row is person-related | Join to the spine. |
|
|
69
|
+
| `revenue.credit_ledger` | `type` | yes | Movement classification. |
|
|
70
|
+
| `revenue.credit_ledger` | `delta_credits` | yes | Credit movement. |
|
|
71
|
+
| `revenue.credit_ledger` | `amount_cents` | yes if source-backed | Money-denominated movement. |
|
|
72
|
+
| `revenue.credit_ledger` | `occurred_at` | yes | Ledger movement date. |
|
|
73
|
+
|
|
74
|
+
If Revenue silver carries credit quantities but not a money-denominated `amount_cents` for ledger movements, Finance may compute unearned credit liability only where the purchase price basis is source-backed through order items or credit purchase records. Otherwise that figure must surface a gap in reconciliation.
|
|
75
|
+
|
|
76
|
+
## Composition rule
|
|
77
|
+
|
|
78
|
+
For each grouping row:
|
|
79
|
+
|
|
80
|
+
`cash_received_cents = sum(amount_paid_cents where paid_at <= reporting_date and payment_status is confirmed paid)`
|
|
81
|
+
|
|
82
|
+
`cash_refunded_cents = sum(amount_cents where refunded_at <= reporting_date and refund_status is confirmed refunded)`
|
|
83
|
+
|
|
84
|
+
`net_cash_cents = cash_received_cents - cash_refunded_cents`
|
|
85
|
+
|
|
86
|
+
`reserved_credit_liability_cents` includes funded credit reservations that have not moved to earned, released, forfeited, or refunded outcomes by the reporting date.
|
|
87
|
+
|
|
88
|
+
`unearned_credit_balance_cents` is the money-denominated balance of purchased credits that remain unearned as of the reporting date.
|
|
89
|
+
|
|
90
|
+
`cash_in_transit_cents` and `refunds_in_transit_cents` are status-specific explanatory figures. They do not change `net_cash_cents` until the source marks the cash movement confirmed.
|
|
91
|
+
|
|
92
|
+
## Reconciliation tolerance
|
|
93
|
+
|
|
94
|
+
Confirmed cash receipt and refund figures reconcile to Revenue with a tolerance of `0` cents. In-transit and unearned-credit figures reconcile to Revenue's status and ledger facts with a tolerance of `0` cents once the required source-backed money amount exists.
|
|
95
|
+
|
|
96
|
+
## Known gaps
|
|
97
|
+
|
|
98
|
+
Provider settlement state is not part of this v1.0.0 manifest. The grouping reports cash based on Revenue's confirmed payment and refund facts, not on external bank settlement. If settlement reporting becomes required, Finance will file a finance-mart change rather than overloading this manifest.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Finance Mart Cohort Summary Manifest
|
|
2
|
+
|
|
3
|
+
**Status:** authoritative for finance-mart v2.0.x (proposed sub-section pending Platform §8 spec text confirmation)
|
|
4
|
+
**Date:** 2026-05-19
|
|
5
|
+
**Owner:** finance
|
|
6
|
+
**Sub-section:** cohort_summary
|
|
7
|
+
**Related:** finance-mart README §4.9, 2026-05-19-finance-cohort-panel-financial-columns-position (Finance's option-2 position), 2026-05-19-portfolio-mart-scope-resolution-path-1-accepted (Portfolio's cohort_funnel_panel v1.2 commitment for the non-financial columns), 2026-05-19-platform-finance-round-6-three-asks-confirmations (Platform's encoding flexibility on cohort_summary placement)
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This manifest defines the `cohort_summary` sub-section in the finance gold section. The sub-section answers: what is the per-cohort cumulative revenue (earned and paid) at each current week since intake, sliced by org_market?
|
|
12
|
+
|
|
13
|
+
The sub-section exists because Portfolio's `cohort_funnel_panel` (v1.2 in portfolio-mart) cannot serve `revenue_earned` and `revenue_paid` columns per portfolio-mart §4.4's no-financial-metrics rule. The Them OS spec §9 `cohort_cross_domain_panel` calls for both non-financial and financial columns at the same cohort grain; Portfolio serves the non-financial slice, Finance serves the financial slice via this sub-section. The Them OS caller joins Portfolio's `cohort_funnel_panel` to Finance's `cohort_summary` on `(org_market_id, intake_cohort_week, current_week)` to assemble the full §9 panel.
|
|
14
|
+
|
|
15
|
+
If Platform reads the §8 spec text as placing cohort-grain financial views inside `unit_economics` instead of as a separate sub-section, this manifest migrates under `unit-economics.md` in a v2.0.x patch and `cohort-summary.md` retires. Finance's option-2 position from `2026-05-19-finance-cohort-panel-financial-columns-position` and Platform's flexibility from `2026-05-19-platform-finance-round-6-three-asks-confirmations` both allow either placement; this manifest is the separate-sub-section form.
|
|
16
|
+
|
|
17
|
+
## Served sub-section
|
|
18
|
+
|
|
19
|
+
`GET /api/mart/sections/finance` exposes this sub-section at `groupings.cohort_summary`. Two metrics: `revenue_earned_by_cohort` and `revenue_paid_by_cohort`.
|
|
20
|
+
|
|
21
|
+
## Served columns
|
|
22
|
+
|
|
23
|
+
Grain: one row per `(org_market_id, intake_cohort_week, current_week)`.
|
|
24
|
+
|
|
25
|
+
| Column | Type | Required | Source | Notes |
|
|
26
|
+
|---|---|---:|---|---|
|
|
27
|
+
| `org_market_id` | string | yes | spine | |
|
|
28
|
+
| `intake_cohort_week` | string | yes | sales journey | ISO week, the cohort key. |
|
|
29
|
+
| `current_week` | string | yes | finance composition | ISO week, the observation point. |
|
|
30
|
+
| `weeks_since_intake` | integer | yes | finance composition | `current_week − intake_cohort_week` in weeks. |
|
|
31
|
+
| `cohort_size` | integer | yes | sales.lead_stage_history | Distinct customers in the cohort with first-lock by `current_week`. |
|
|
32
|
+
| `revenue_earned_cents` | integer or null | yes | revenue silver | Cumulative GAAP earned revenue for the cohort from `intake_cohort_week` through `current_week`. |
|
|
33
|
+
| `revenue_paid_cents` | integer or null | yes | revenue silver | Cumulative cash revenue for the cohort from `intake_cohort_week` through `current_week`. |
|
|
34
|
+
| `null_reason` | string or null | yes | finance composition | `null:upstream_unavailable` for cohorts preceding the silver face's earliest observation. |
|
|
35
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
|
|
36
|
+
|
|
37
|
+
Rollup rule: `sum` (cumulative revenue is additive across geography).
|
|
38
|
+
|
|
39
|
+
## Source silver columns
|
|
40
|
+
|
|
41
|
+
| Silver object | Target column | Used for | Required |
|
|
42
|
+
|---|---|---|---:|
|
|
43
|
+
| `sales.lead_stage_history` | first-lock event per customer | cohort identity and size | yes |
|
|
44
|
+
| `revenue.revenue_recognition` | `recognized_amount_cents`, `recognition_period`, `customer_id`, `org_market_id` | revenue_earned_cents per cohort customer | yes |
|
|
45
|
+
| `revenue.orders` (or equivalent) | `amount_paid_cents`, `paid_at`, `customer_id`, `org_market_id` | revenue_paid_cents per cohort customer | yes |
|
|
46
|
+
| spine | `org_market_id`, `customer_id` to `person_id` resolution | grain and join keys | yes |
|
|
47
|
+
|
|
48
|
+
## Composition rule
|
|
49
|
+
|
|
50
|
+
For each row at `(org_market_id, intake_cohort_week, current_week)`:
|
|
51
|
+
|
|
52
|
+
1. Identify cohort customers: distinct `customer_id` rows from `sales.lead_stage_history` with their first-lock in `intake_cohort_week` at `org_market_id`. Cohort size as of `current_week` counts customers whose first-lock landed at or before `current_week`.
|
|
53
|
+
2. `revenue_earned_cents` = sum of `recognized_amount_cents` from `revenue.revenue_recognition` for cohort customers where `recognition_period` falls between `intake_cohort_week` and `current_week`.
|
|
54
|
+
3. `revenue_paid_cents` = sum of `amount_paid_cents` from `revenue.orders` for cohort customers where `paid_at` falls between `intake_cohort_week` and `current_week`.
|
|
55
|
+
4. The two figures are cumulative since intake, not per-period; consumers compute per-period deltas client-side by subtracting consecutive `current_week` rows.
|
|
56
|
+
|
|
57
|
+
## Join with Portfolio's `cohort_funnel_panel`
|
|
58
|
+
|
|
59
|
+
The Them OS caller assembles the full §9 panel by joining Portfolio's `cohort_funnel_panel` to this sub-section on `(org_market_id, intake_cohort_week, current_week)`. Portfolio serves: `intake_count`, `first_lock_count`, `completed_lessons_count`, `status_as_of_now`. Finance serves: `revenue_earned_cents`, `revenue_paid_cents`. The caller-side join produces the §9 row shape end-to-end.
|
|
60
|
+
|
|
61
|
+
Cohort identity (the join key `intake_cohort_week`) is derived consistently across both sub-sections from `sales.lead_stage_history`; both Portfolio and Finance compute the cohort from the same Sales silver. No identity drift expected; if a divergence is observed in production, it's a Sales silver consistency issue, not a Finance or Portfolio composition issue.
|
|
62
|
+
|
|
63
|
+
## Reconciliation tolerance
|
|
64
|
+
|
|
65
|
+
- `revenue_earned_cents` reconciles to `revenue.revenue_recognition` filtered to cohort customers, with tolerance `0` cents.
|
|
66
|
+
- `revenue_paid_cents` reconciles to `revenue.orders.amount_paid_cents` filtered to cohort customers, with tolerance `0` cents.
|
|
67
|
+
- `cohort_size` reconciles to `sales.lead_stage_history` with tolerance `0`.
|
|
68
|
+
|
|
69
|
+
## Known gaps
|
|
70
|
+
|
|
71
|
+
- **The sub-section is `not_started` pending Platform's confirmation** that the separate `cohort_summary` sub-section is the right encoding placement (vs. migrating under `unit_economics` per Platform's three-asks-confirmations flexibility note). Finance's option-2 position preferred a separate sub-section; this manifest lands the separate form, with the migrate-under-unit_economics fallback available as a v2.0.x patch.
|
|
72
|
+
- **All upstream silver is live**: Sales journey, Revenue recognition, Revenue orders are all in silver today. The sub-section is compute-wirable as soon as Platform's encoding confirmation lands; metrics flip from `not_started` to `beta` then.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Finance Mart Customer Journey Audit Manifest
|
|
2
|
+
|
|
3
|
+
**Status:** authoritative for finance-mart v2.0.x
|
|
4
|
+
**Date:** 2026-05-19
|
|
5
|
+
**Owner:** finance
|
|
6
|
+
**Sub-section:** customer_journey_audit (§4.10 customer-grain audit carve-out)
|
|
7
|
+
**Related:** finance-mart README §4.10, 2026-05-19-finance-customer-audit-consolidation-cleanness (row-shape resolution; consolidation of Portfolio's §9.3 `cohort_customer_audit`), 2026-05-19-finance-mart-registry-portfolio-consolidation-position (three conditions on posture, scope, initiative-attribution), 2026-05-19-portfolio-mart-scope-resolution-path-1-accepted (Portfolio's removal of §9.3 from portfolio-mart)
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This manifest defines the `customer_journey_audit` carve-out in the finance gold section. The carve-out answers (audit-only, rate-limited): for a specific customer, what is the journey snapshot across intake, lock, delivery, revenue, and current status?
|
|
12
|
+
|
|
13
|
+
The carve-out is the ONLY customer-grain row shape in finance-mart. Aggregate sub-sections never expose customer grain. The carve-out consolidates Portfolio's §9.3 `cohort_customer_audit` per the 2026-05-19 path-1 resolution; Portfolio's contract forbids person-grain rows per portfolio-mart §4.4 + §8, and Finance is the only domain with the rate-limit and audit-only posture appropriate for a customer-grain audit.
|
|
14
|
+
|
|
15
|
+
The carve-out preserves three Finance-staked conditions from `2026-05-19-finance-mart-registry-portfolio-consolidation-position`:
|
|
16
|
+
|
|
17
|
+
1. **Posture preserved**: audit-only, rate-limited, tagged `cross_domain_customer_grain=true`. The carve-out is not a general customer-grain reporting surface.
|
|
18
|
+
2. **`current_initiative_attribution` excluded**: initiative attribution is a coordination-layer artifact, not a financial fact. Consumers needing initiative attribution join to a Platform-owned coordination surface at audit time.
|
|
19
|
+
3. **Surface inside Finance's contract bound**: Portfolio consumers reading the audit do so through Finance's access pattern (rate-limit, firewall bound, deprecation discipline).
|
|
20
|
+
|
|
21
|
+
## Served carve-out
|
|
22
|
+
|
|
23
|
+
`GET /api/mart/sections/finance` exposes the carve-out at a separate, rate-limited audit endpoint (Platform's `lib/mart/sections/finance.ts` shape; the exact endpoint path under Platform's routing convention). The endpoint accepts a customer-identifier filter (`customer_id` or `person_id`), an optional cohort slice (`intake_cohort_week`, `intake_cohort_week_range`), and returns one row per customer matching the filter.
|
|
24
|
+
|
|
25
|
+
Posture: `cross_domain_customer_grain=true` tag on every served row. Rate-limit enforced per the Finance access pattern. Audit-log every read.
|
|
26
|
+
|
|
27
|
+
## Served columns
|
|
28
|
+
|
|
29
|
+
Grain: one row per `customer_id`.
|
|
30
|
+
|
|
31
|
+
| Column | Type | Required | Source | Notes |
|
|
32
|
+
|---|---|---:|---|---|
|
|
33
|
+
| `customer_id` | string | yes | spine | Primary key. The audit row is identified by customer. |
|
|
34
|
+
| `org_market_id` | string | yes | spine | Customer's org-market attribution. |
|
|
35
|
+
| `intake_week` | string | yes | sales journey | ISO week. Doubles as the cohort slicing dimension. |
|
|
36
|
+
| `first_lock_week` | string or null | yes | sales.lead_stage_history | ISO week of first-lock; null if customer hasn't locked. |
|
|
37
|
+
| `first_delivery_lesson_week` | string or null | yes | delivery silver | ISO week of first completed Delivery lesson; null if no lesson yet. |
|
|
38
|
+
| `lessons_completed_to_date` | integer | yes | delivery silver | Count of completed lessons as of `as_of`. |
|
|
39
|
+
| `revenue_paid_to_date_cents` | integer | yes | revenue silver | Cumulative `amount_paid_cents` for the customer as of `as_of`. |
|
|
40
|
+
| `current_status` | string enum | yes | finance composition | Derived from stage state: one of `pre_lock`, `active`, `dormant`, `churned`, `lapsed`. |
|
|
41
|
+
| `_cross_domain_customer_grain` | boolean | yes | finance composition | Always `true`. |
|
|
42
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | Audit-time snapshot timestamp. |
|
|
43
|
+
|
|
44
|
+
**Field deliberately excluded**: `current_initiative_attribution`. Coordination-layer artifact, not a financial fact. Consumers needing initiative attribution join to a Platform-owned coordination surface at audit time. The exclusion is named here so the contract is explicit; consumers should not request the column.
|
|
45
|
+
|
|
46
|
+
## Source silver columns
|
|
47
|
+
|
|
48
|
+
| Silver object | Target column | Used for | Required |
|
|
49
|
+
|---|---|---|---:|
|
|
50
|
+
| `sales.lead_intake` (or equivalent) | `customer_id`, `intake_week`, `org_market_id` | identity + intake | yes |
|
|
51
|
+
| `sales.lead_stage_history` | first-lock event per customer | `first_lock_week` | yes |
|
|
52
|
+
| `delivery.lesson_event` | `customer_id`, `completed_at` | `first_delivery_lesson_week`, `lessons_completed_to_date` | yes |
|
|
53
|
+
| `revenue.orders` (or equivalent) | `customer_id`, `amount_paid_cents`, `paid_at` | `revenue_paid_to_date_cents` | yes |
|
|
54
|
+
| `delivery.customer` | `status`, `last_lesson_at` | `current_status` derivation | yes |
|
|
55
|
+
| spine | `customer_id`, `person_id`, `org_market_id` | grain and join | yes |
|
|
56
|
+
|
|
57
|
+
## Composition rule
|
|
58
|
+
|
|
59
|
+
For each row at `customer_id`:
|
|
60
|
+
|
|
61
|
+
1. Read `intake_week`, `org_market_id` from `sales.lead_intake` for the customer.
|
|
62
|
+
2. Read `first_lock_week` from `sales.lead_stage_history`: the earliest event with stage = `locked` for the customer; null if no such event.
|
|
63
|
+
3. Read `first_delivery_lesson_week` from `delivery.lesson_event`: the earliest event with `completed_at` set; null if no completed lesson yet.
|
|
64
|
+
4. Count `lessons_completed_to_date` = count of `delivery.lesson_event` rows with `completed_at <= as_of` for the customer.
|
|
65
|
+
5. Sum `revenue_paid_to_date_cents` = sum of `revenue.orders.amount_paid_cents` rows with `paid_at <= as_of` for the customer.
|
|
66
|
+
6. Derive `current_status`:
|
|
67
|
+
- `pre_lock` if `first_lock_week` is null.
|
|
68
|
+
- `active` if `delivery.customer.status = 'active'` and `delivery.customer.last_lesson_at` is within the prior 8 weeks.
|
|
69
|
+
- `dormant` if last lesson is between 8 and 52 weeks ago.
|
|
70
|
+
- `churned` if last lesson is more than 52 weeks ago and customer hasn't booked a future lesson.
|
|
71
|
+
- `lapsed` if no completed lesson since `first_lock_week`.
|
|
72
|
+
|
|
73
|
+
## Cohort slicing
|
|
74
|
+
|
|
75
|
+
The audit endpoint accepts an optional `intake_cohort_week` or `intake_cohort_week_range` filter that slices by `intake_week`. The slice is a filter on the existing row shape, not a parallel row. The cohort-spot-check use case (give me all customers in cohort X) is served by the same endpoint with the filter applied; no parallel cohort-row endpoint exists.
|
|
76
|
+
|
|
77
|
+
## Rate-limit and access posture
|
|
78
|
+
|
|
79
|
+
The endpoint is rate-limited per Finance's `/admin/mart` access configuration. Audit-log every read, including the requesting consumer identity, the query filter (`customer_id` or cohort), and the row count returned. The endpoint is not callable from non-audit access tags.
|
|
80
|
+
|
|
81
|
+
## Reconciliation tolerance
|
|
82
|
+
|
|
83
|
+
- `revenue_paid_to_date_cents` reconciles to `revenue.orders` with tolerance `0` cents.
|
|
84
|
+
- `lessons_completed_to_date` reconciles to `delivery.lesson_event` with tolerance `0`.
|
|
85
|
+
- `first_lock_week`, `first_delivery_lesson_week` are minimums; tolerance `0` weeks.
|
|
86
|
+
- `current_status` is derived; no external reconciliation target.
|
|
87
|
+
|
|
88
|
+
## Known gaps
|
|
89
|
+
|
|
90
|
+
- **The endpoint shape and rate-limit machinery** require Finance-side implementation alongside Platform's `lib/mart/sections/finance.ts` consolidation work. The row-shape is settled in `2026-05-19-finance-customer-audit-consolidation-cleanness`; the endpoint wiring follows.
|
|
91
|
+
- **`current_status` enum definition** is Finance's call and lands here. If Delivery's `delivery.customer.status` enum diverges from `pre_lock`/`active`/`dormant`/`churned`/`lapsed`, Finance maps from Delivery's enum into the Finance enum at composition time.
|
|
92
|
+
- All upstream silver is live; the gates are the endpoint wiring and the rate-limit configuration.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Finance Mart LTV Manifest
|
|
2
|
+
|
|
3
|
+
**Status:** authoritative for finance-mart v2.0.x
|
|
4
|
+
**Date:** 2026-05-19
|
|
5
|
+
**Owner:** finance
|
|
6
|
+
**Sub-section:** ltv
|
|
7
|
+
**Related:** finance-mart README §4.8, 2026-05-19-finance-100-percent-deployment-decisions (Decision 3: LTV decomposition), cac-payback.md (matching CAC decomposition for the LTV:CAC ratio)
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This manifest defines the `ltv` sub-section in the finance gold section. The sub-section answers: what is the lifetime value of a cohort's customers, and what is the LTV:CAC ratio?
|
|
12
|
+
|
|
13
|
+
LTV composes per-customer cumulative gross margin against cohort size. The decomposition is gross-margin LTV per Finance's `_ltv_decomposition: gross_margin` decision, which matches the CAC decomposition (`marketing_plus_sales_sdr`) so the LTV:CAC ratio composes coherently. Cohort grain: `(org_market_id, intake_cohort_week)`.
|
|
14
|
+
|
|
15
|
+
## Served sub-section
|
|
16
|
+
|
|
17
|
+
`GET /api/mart/sections/finance` exposes this sub-section at `groupings.ltv`. Two metrics: `ltv_per_first_lock_cohort` and `ltv_to_cac_ratio`.
|
|
18
|
+
|
|
19
|
+
## Served columns
|
|
20
|
+
|
|
21
|
+
### `ltv_per_first_lock_cohort`
|
|
22
|
+
|
|
23
|
+
Grain: one row per `(org_market_id, intake_cohort_week)`.
|
|
24
|
+
|
|
25
|
+
| Column | Type | Required | Source | Notes |
|
|
26
|
+
|---|---|---:|---|---|
|
|
27
|
+
| `org_market_id` | string | yes | spine | |
|
|
28
|
+
| `intake_cohort_week` | string | yes | sales journey | |
|
|
29
|
+
| `cohort_size` | integer | yes | sales.lead_stage_history | Distinct customers in the cohort with first-lock. |
|
|
30
|
+
| `cumulative_gross_margin_cents` | integer or null | yes | finance composition | Sum across all cohort customers of cumulative gross margin since intake. |
|
|
31
|
+
| `ltv_cents` | integer or null | yes | finance composition | `cumulative_gross_margin_cents / cohort_size`. |
|
|
32
|
+
| `observation_window_weeks` | integer | yes | finance composition | Weeks elapsed since `intake_cohort_week`. |
|
|
33
|
+
| `_ltv_decomposition` | string | yes | finance composition | `gross_margin`. |
|
|
34
|
+
| `null_reason` | string or null | yes | finance composition | `null:upstream_unavailable` until cumulative gross margin per cohort customer is composable; `null:insufficient_data` below cohort floor of 5. |
|
|
35
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
|
|
36
|
+
|
|
37
|
+
Policy: `_ltv_decomposition: gross_margin`. Rollup rule: `weighted_mean_by_cohort_size`.
|
|
38
|
+
|
|
39
|
+
### `ltv_to_cac_ratio`
|
|
40
|
+
|
|
41
|
+
Grain: one row per `(org_market_id, intake_cohort_week)`. Derived from matching `ltv_per_first_lock_cohort` and `cac` (or `cac_per_first_lock` aggregate) at the same cohort grain.
|
|
42
|
+
|
|
43
|
+
| Column | Type | Required | Source | Notes |
|
|
44
|
+
|---|---|---:|---|---|
|
|
45
|
+
| `org_market_id` | string | yes | spine | |
|
|
46
|
+
| `intake_cohort_week` | string | yes | sales journey | |
|
|
47
|
+
| `ltv_cents` | integer or null | yes | finance composition | From `ltv_per_first_lock_cohort` at same grain. |
|
|
48
|
+
| `cac_cents` | integer or null | yes | finance composition | From `cac` (aggregate across source classes) at same grain. |
|
|
49
|
+
| `ltv_to_cac_ratio_bps` | integer or null | yes | finance composition | `round(ltv_cents / cac_cents × 10000)` as basis points. |
|
|
50
|
+
| `null_reason` | string or null | yes | finance composition | `null:upstream_unavailable` when either component is null; `null:not_applicable` when CAC is zero (free-acquisition cohort). |
|
|
51
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
|
|
52
|
+
|
|
53
|
+
Policy: `_ltv_decomposition: gross_margin` and `_cac_cost_decomposition: marketing_plus_sales_sdr` paired. Rollup rule: `weighted_mean_by_cohort_size`.
|
|
54
|
+
|
|
55
|
+
## Source silver columns
|
|
56
|
+
|
|
57
|
+
| Silver object | Target column | Used for | Required |
|
|
58
|
+
|---|---|---|---:|
|
|
59
|
+
| `sales.lead_stage_history` | first-lock events per customer | cohort identity and size | yes |
|
|
60
|
+
| `revenue.revenue_recognition` | per-customer cumulative recognized revenue since intake | LTV revenue side | yes |
|
|
61
|
+
| `coaching.coach.lesson_rate`, `coaching.coach.travel_comp` | per-coach cost rates | cost-of-service per customer | yes |
|
|
62
|
+
| `delivery.lesson_event` | `coach_id`, `duration`, `customer_id`, `org_market_id`, `completed_at` | per-customer cost-of-service composition | yes |
|
|
63
|
+
| spine | `org_market_id`, `person_id` | grain and join keys | yes |
|
|
64
|
+
| cac-payback.md `cac_cents` figure | matching cohort grain | `ltv_to_cac_ratio` denominator | yes (Finance composition, not silver) |
|
|
65
|
+
|
|
66
|
+
## Composition rule
|
|
67
|
+
|
|
68
|
+
`ltv_per_first_lock_cohort`:
|
|
69
|
+
|
|
70
|
+
1. Identify the cohort: distinct customers with their first-lock in `intake_cohort_week` at `org_market_id`, from `sales.lead_stage_history`.
|
|
71
|
+
2. For each cohort customer, compose cumulative gross margin since intake: sum of (per-lesson recognized revenue − per-lesson cost-of-service) over all lessons from `intake_cohort_week` to current observation date. Per-lesson cost-of-service uses the same Coaching + Delivery join as the `margin` sub-section.
|
|
72
|
+
3. Sum across the cohort to `cumulative_gross_margin_cents`.
|
|
73
|
+
4. Read `cohort_size` from step 1.
|
|
74
|
+
5. Return `cumulative_gross_margin_cents / cohort_size` as `ltv_cents`.
|
|
75
|
+
|
|
76
|
+
`ltv_to_cac_ratio_bps`:
|
|
77
|
+
|
|
78
|
+
1. Read `ltv_cents` from `ltv_per_first_lock_cohort` at the row's `(org_market_id, intake_cohort_week)`.
|
|
79
|
+
2. Read `cac_cents` from `cac` (aggregate, not split by source_class) at the same cohort grain.
|
|
80
|
+
3. Return `round(ltv_cents / cac_cents × 10000)` as basis points. Emit `null:not_applicable` when `cac_cents = 0`.
|
|
81
|
+
|
|
82
|
+
## Reconciliation tolerance
|
|
83
|
+
|
|
84
|
+
- `cumulative_gross_margin_cents` reconciles to the same Revenue + Coaching + Delivery composition the `margin` sub-section uses; tolerance `0` cents.
|
|
85
|
+
- `cohort_size` reconciles to `sales.lead_stage_history` with tolerance `0`.
|
|
86
|
+
- `ltv_to_cac_ratio_bps` is derived from two Finance metrics; its reconciliation surface is the component figures.
|
|
87
|
+
|
|
88
|
+
## Known gaps
|
|
89
|
+
|
|
90
|
+
- **`ltv_per_first_lock_cohort`** is currently `null:upstream_unavailable` until per-customer cumulative cost-of-service composition is wired in `lib/mart/sections/finance.ts`. The upstream silver (Revenue recognition + Coaching cost + Delivery lesson_event) is all live; the gate is the Finance compute wiring per Platform's continuous-deployment cadence.
|
|
91
|
+
- **`ltv_to_cac_ratio`** is gated on both `ltv_per_first_lock_cohort` and `cac` reaching beta. CAC is at beta in marketing-only form via Round 6's `cac_per_first_lock`; the full marketing+sales-SDR form gates on Sales SDR allocation silver (see cac-payback.md known gaps).
|
|
92
|
+
- No silver-side gaps for this sub-section; the gaps are Finance composition wiring.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Finance Mart Margin Manifest
|
|
2
|
+
|
|
3
|
+
**Status:** authoritative for finance-mart v1.0.0
|
|
4
|
+
**Date:** 2026-05-18
|
|
5
|
+
**Owner:** finance
|
|
6
|
+
**Grouping:** margin
|
|
7
|
+
**Related:** finance-mart README section 4.4, warehouse-silver v1.1.0, 2026-05-17-revenue-finance-mart-contract-reply
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This manifest defines the `margin` grouping in the finance gold section. The grouping answers: what margin did a reporting period earn, and where is margin concentrated by Organization and Market?
|
|
12
|
+
|
|
13
|
+
Margin pairs recognized revenue with source-backed direct cost facts. Finance will not synthesize cost. If the current silver faces do not carry a cost fact, the served row must expose that gap rather than producing a false full-margin number.
|
|
14
|
+
|
|
15
|
+
## Served grouping
|
|
16
|
+
|
|
17
|
+
`GET /api/mart/sections/finance` exposes this grouping at `groupings.margin`.
|
|
18
|
+
|
|
19
|
+
The grouping grain is one row per:
|
|
20
|
+
|
|
21
|
+
- `reporting_period`
|
|
22
|
+
- `organization_id`
|
|
23
|
+
- `market_id`
|
|
24
|
+
|
|
25
|
+
`org_market_id` is the stable mart coordination key for the Organization and Market pair. `service_area_id` MAY appear only when a source-backed silver face column exists and the implementation explicitly scopes a Service Area cut.
|
|
26
|
+
|
|
27
|
+
## Served columns
|
|
28
|
+
|
|
29
|
+
| Column | Type | Required | Source | Notes |
|
|
30
|
+
|---|---|---:|---|---|
|
|
31
|
+
| `reporting_period` | string | yes | `revenue.recognition.recognition_period` | Calendar period key. |
|
|
32
|
+
| `reporting_period_grain` | string enum | yes | finance composition | `month` in the first implementation. |
|
|
33
|
+
| `organization_id` | string | yes | spine | Platform Organization ID for the discipline. |
|
|
34
|
+
| `organization_slug` | string | yes | spine | Stable display slug for the Organization. |
|
|
35
|
+
| `market_id` | string | yes | spine | Platform Market ID. |
|
|
36
|
+
| `market_slug` | string | yes | spine | Stable display slug for the Market. |
|
|
37
|
+
| `org_market_id` | string | yes | `OrgMarket.slug` | Stable mart key for the Organization and Market pair. |
|
|
38
|
+
| `recognized_revenue_cents` | integer | yes | revenue-recognition composition | Net recognized revenue, using the revenue-recognition manifest rule. |
|
|
39
|
+
| `direct_cost_cents` | integer or null | yes | delivery or coaching silver cost columns | Null until source-backed cost facts exist. |
|
|
40
|
+
| `gross_margin_cents` | integer or null | yes | finance composition | `recognized_revenue_cents - direct_cost_cents`; null when cost is unavailable. |
|
|
41
|
+
| `gross_margin_rate_bps` | integer or null | yes | finance composition | Basis points, null when revenue is zero or cost is unavailable. |
|
|
42
|
+
| `cost_basis_status` | string enum | yes | finance composition | One of `complete`, `partial`, `missing_cost_source`. |
|
|
43
|
+
| `cost_gap_reason` | string or null | yes | finance composition | Required when `cost_basis_status` is not `complete`. |
|
|
44
|
+
| `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | Refresh point of the materialized grouping. |
|
|
45
|
+
|
|
46
|
+
## Source silver columns
|
|
47
|
+
|
|
48
|
+
The revenue side is sourced from `revenue.recognition` using the revenue-recognition manifest.
|
|
49
|
+
|
|
50
|
+
The direct-cost side targets these source-backed cost columns if and only if the live silver faces carry them:
|
|
51
|
+
|
|
52
|
+
| Silver object | Target column | Required for complete margin | Use |
|
|
53
|
+
|---|---|---:|---|
|
|
54
|
+
| `delivery.lesson` | `lesson_id` | yes | Lesson-level cost grain and join key. |
|
|
55
|
+
| `delivery.lesson` | `person_id` | yes where lesson is person-related | Join to the spine. |
|
|
56
|
+
| `delivery.lesson` | `completed_at` | yes | Period alignment for completed lesson cost. |
|
|
57
|
+
| `delivery.lesson` | `organization_id` | yes when source-backed | Organization attribution when present on the face. |
|
|
58
|
+
| `delivery.lesson` | `market_id` | yes when source-backed | Market attribution when present on the face. |
|
|
59
|
+
| `delivery.lesson` | `service_area_id` | optional | Service Area cut, if source-backed. |
|
|
60
|
+
| `delivery.lesson` | `coach_pay_cents` | yes for complete lesson-cost margin | Direct coach cost for delivered lessons. |
|
|
61
|
+
| `delivery.lesson` | `facility_cost_cents` | optional | Direct facility cost, if the source carries it. |
|
|
62
|
+
| `coaching.lesson_booking` | `lesson_booking_id` | optional | Booking-level cost grain when coaching carries the cost fact. |
|
|
63
|
+
| `coaching.lesson_booking` | `coach_cost_cents` | optional | Direct coaching cost, if the source carries it. |
|
|
64
|
+
|
|
65
|
+
If the live silver surface does not carry `coach_pay_cents` or an equivalent source-backed direct cost column, full margin is not implementable yet. In that case the grouping may still expose revenue-side rows with `direct_cost_cents = null`, `gross_margin_cents = null`, `gross_margin_rate_bps = null`, `cost_basis_status = missing_cost_source`, and `cost_gap_reason = direct_cost_fact_absent_from_silver`.
|
|
66
|
+
|
|
67
|
+
## Composition rule
|
|
68
|
+
|
|
69
|
+
For each grouping row:
|
|
70
|
+
|
|
71
|
+
`recognized_revenue_cents` follows the revenue-recognition manifest.
|
|
72
|
+
|
|
73
|
+
`direct_cost_cents = sum(source-backed direct cost columns aligned to the same reporting period, Organization, and Market)`
|
|
74
|
+
|
|
75
|
+
`gross_margin_cents = recognized_revenue_cents - direct_cost_cents`
|
|
76
|
+
|
|
77
|
+
`gross_margin_rate_bps = round(gross_margin_cents / recognized_revenue_cents * 10000)`
|
|
78
|
+
|
|
79
|
+
When direct cost is partially available, the row must set `cost_basis_status = partial` and name the missing cost class in `cost_gap_reason`. Consumers must not treat partial margin as full gross margin.
|
|
80
|
+
|
|
81
|
+
## Reconciliation tolerance
|
|
82
|
+
|
|
83
|
+
The revenue side reconciles to Revenue with a tolerance of `0` cents. The cost side reconciles to the producing silver face with a tolerance of `0` cents once source-backed cost columns exist. Until then, the margin grouping is explicitly not fully reconcilable and the `reconciliation` grouping must report `not_reconcilable_missing_source` for margin-cost figures.
|
|
84
|
+
|
|
85
|
+
## Known gaps
|
|
86
|
+
|
|
87
|
+
Revenue confirmed it does not hold cost data. The expected gap is direct cost coverage on Delivery or Coaching silver. Platform should treat the direct-cost target columns in this manifest as a source-availability check, not as permission to synthesize cost in silver.
|