@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.
Files changed (96) hide show
  1. package/README.md +4 -1
  2. package/contracts/README.md +30 -0
  3. package/contracts/coach-availability/README.md +355 -0
  4. package/contracts/coach-availability/README.v2.md +263 -0
  5. package/contracts/coach-availability/schema/payloads/coach.assigned-v1.json +91 -0
  6. package/contracts/coach-availability/validation/delivery-assignment.md +89 -0
  7. package/contracts/coach-availability/validation/sales-offer-construction.md +76 -0
  8. package/contracts/coaching-confirmation/README.md +96 -0
  9. package/contracts/coaching-confirmation/schema/payloads/coaching.lesson.confirmation_decided-v1.json +142 -0
  10. package/contracts/coaching-confirmation/schema/payloads/lead.coach.confirmation.requested-v1.json +124 -0
  11. package/contracts/credit-reservation-funding-state/README.md +147 -0
  12. package/contracts/credit-reservation-lock/README.md +433 -0
  13. package/contracts/credit-reservation-lock/delivery-state-vocabulary.md +73 -0
  14. package/contracts/credit-reservation-lock/reservation-create-api.md +191 -0
  15. package/contracts/credit-reservation-lock/reservation-release-api.md +171 -0
  16. package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +1 -1
  17. package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +2 -3
  18. package/contracts/credit-reservation-lock/validation/lesson-lifecycle.md +318 -0
  19. package/contracts/event-envelope/README.md +205 -0
  20. package/contracts/event-envelope/schema/envelope-v1.json +2 -2
  21. package/contracts/event-envelope/validation/event-vocabulary.md +270 -0
  22. package/contracts/event-types-registry.json +337 -24
  23. package/contracts/external-actions/README.md +338 -0
  24. package/contracts/finance-mart/README.md +238 -0
  25. package/contracts/finance-mart/cac-payback.md +113 -0
  26. package/contracts/finance-mart/cash-position.md +98 -0
  27. package/contracts/finance-mart/cohort-summary.md +72 -0
  28. package/contracts/finance-mart/customer-journey-audit.md +92 -0
  29. package/contracts/finance-mart/ltv.md +92 -0
  30. package/contracts/finance-mart/margin.md +87 -0
  31. package/contracts/finance-mart/pnl.md +83 -0
  32. package/contracts/finance-mart/reconciliation.md +98 -0
  33. package/contracts/finance-mart/revenue-recognition-rollup.md +87 -0
  34. package/contracts/finance-mart/unit-economics.md +94 -0
  35. package/contracts/growth-warehouse-api/README.md +162 -0
  36. package/contracts/identity/README.md +184 -0
  37. package/contracts/identity/person-canonical-fields.md +120 -0
  38. package/contracts/identity/person-externals.md +267 -0
  39. package/contracts/identity/person-resolution-semantics.md +144 -0
  40. package/contracts/identity/person-role-taxonomy.md +120 -0
  41. package/contracts/identity/schema/payloads/intake.captured-v2.json +60 -0
  42. package/contracts/identity/schema/payloads/intake.matched-v2.json +123 -0
  43. package/contracts/identity/schema/payloads/person.updated-v1.json +8 -2
  44. package/contracts/identity/schema/payloads/role.assigned-v1.json +50 -0
  45. package/contracts/identity/schema/payloads/role.retired-v1.json +54 -0
  46. package/contracts/identity/validation/client-table.md +131 -0
  47. package/contracts/identity/validation/coach-handling.md +100 -0
  48. package/contracts/identity/validation/person-graph.md +140 -0
  49. package/contracts/lead-lifecycle/README.md +187 -0
  50. package/contracts/lead-lifecycle/schema/payloads/lead.handoff.context.recorded-v1.json +108 -0
  51. package/contracts/lead-lifecycle/schema/payloads/lead.qualified-v1.json +54 -0
  52. package/contracts/lead-lifecycle/schema/payloads/sales.lead.onboarded-v1.json +120 -0
  53. package/contracts/lesson-lifecycle/README.md +118 -0
  54. package/contracts/lesson-lifecycle/schema/payloads/lesson.cancelled-v1.json +30 -0
  55. package/contracts/lesson-lifecycle/schema/payloads/lesson.delivered-v1.json +29 -0
  56. package/contracts/lesson-lifecycle/schema/payloads/lesson.rescheduled-v1.json +157 -0
  57. package/contracts/lesson-lifecycle/schema/payloads/lesson.scheduled-v1.json +107 -0
  58. package/contracts/lesson-lifecycle/validation/README.md +5 -0
  59. package/contracts/mart-consumer-api/README.md +108 -0
  60. package/contracts/order-flow/README.md +106 -0
  61. package/contracts/order-flow/schema/payloads/order.created-v1.json +58 -0
  62. package/contracts/order-flow/schema/payloads/order.updated-v1.json +63 -0
  63. package/contracts/payment-flow/README.md +157 -0
  64. package/contracts/platform-comms/README.md +84 -0
  65. package/contracts/platform-comms/schema/payloads/platform.comms.inbound-v1.json +83 -0
  66. package/contracts/platform-geography-snapshot/README.md +205 -0
  67. package/contracts/platform-geography-snapshot/schema/payloads/geography.market.archived-v1.json +36 -0
  68. package/contracts/platform-geography-snapshot/schema/payloads/geography.market.upserted-v1.json +59 -0
  69. package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.archived-v1.json +36 -0
  70. package/contracts/platform-geography-snapshot/schema/payloads/geography.service-area.upserted-v1.json +65 -0
  71. package/contracts/portfolio-mart/README.md +133 -0
  72. package/contracts/portfolio-mart/cohort-funnel-panel.md +76 -0
  73. package/contracts/portfolio-mart/cross-discipline-performance.md +91 -0
  74. package/contracts/portfolio-mart/cross-market-performance.md +84 -0
  75. package/contracts/portfolio-mart/health-composites.md +88 -0
  76. package/contracts/portfolio-mart/org-topology.md +70 -0
  77. package/contracts/portfolio-mart/portfolio-level-funnel-health.md +92 -0
  78. package/contracts/portfolio-mart/validation/consumer-isolation.md +33 -0
  79. package/contracts/portfolio-mart/validation/decoupling-discipline.md +34 -0
  80. package/contracts/refund-flow/README.md +136 -0
  81. package/contracts/refund-flow/sales-callable-refund-initiation-api.md +218 -0
  82. package/contracts/sales-scheduling-surface/README.md +532 -0
  83. package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.cancelled-v1.json +42 -0
  84. package/contracts/sales-scheduling-surface/schema/payloads/delivery.lesson-hold.created-v1.json +115 -0
  85. package/contracts/sales-scheduling-surface/validation/composite-hold-create.md +97 -0
  86. package/contracts/sales-scheduling-surface/validation/lock-state-machine-conformance.md +84 -0
  87. package/contracts/sales-scheduling-surface/validation/sales-close-orchestration.md +77 -0
  88. package/contracts/warehouse-silver/README.md +118 -0
  89. package/contracts/warehouse-silver/coaching-utilization-columns.md +105 -0
  90. package/dist/events.d.ts +63 -0
  91. package/dist/events.js +293 -0
  92. package/dist/index.d.ts +2 -0
  93. package/dist/index.js +7 -1
  94. package/dist/postgres-consumer.js +2 -1
  95. package/dist/validator.js +1 -0
  96. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ # Finance Mart PnL Manifest
2
+
3
+ **Status:** authoritative for finance-mart v2.0.x
4
+ **Date:** 2026-05-19
5
+ **Owner:** finance
6
+ **Sub-section:** pnl
7
+ **Related:** finance-mart README §4.4, revenue-recognition-rollup.md (carries `revenue_recognition_summary` under the same sub-section), 2026-05-19-platform-mart-round-status-and-next-silver-asks (Round 6 wiring of pnl_summary v0), 2026-05-19-finance-100-percent-deployment-decisions (OPEX allocation rule)
8
+
9
+ ## Purpose
10
+
11
+ This manifest defines the `pnl_summary` metric in the finance gold section's `pnl` sub-section. The metric answers: what is the P&L for a given period at the business reporting grain, composing recognized revenue, cost-of-service, and operating expense?
12
+
13
+ The companion `revenue_recognition_summary` metric in the same sub-section is documented in `revenue-recognition-rollup.md` (carry-forward from v1.0.0 §4.3). This manifest covers `pnl_summary` only.
14
+
15
+ ## Served sub-section
16
+
17
+ `GET /api/mart/sections/finance` exposes this metric under `groupings.pnl.pnl_summary` (or the equivalent typed grouping per Platform's `PnlSummaryRow` shape in `lib/mart/sections/finance.ts`). Three grains per Round 6: `per-org × per-month`, `per-portfolio × per-month` rollup with sum, and `per-org-market × per-month`.
18
+
19
+ ## Served columns
20
+
21
+ Grain: one row per `(geography_key, reporting_period)` where `geography_key` is one of (org_market, organization, portfolio).
22
+
23
+ | Column | Type | Required | Source | Notes |
24
+ |---|---|---:|---|---|
25
+ | `geography_key` | string | yes | spine | One of `org_market_id`, `organization_id`, `portfolio_id`. |
26
+ | `geography_grain` | string enum | yes | finance composition | `org_market`, `organization`, `portfolio`. |
27
+ | `reporting_period` | string | yes | finance composition | Month key in v2.0.x first implementation. |
28
+ | `recognized_revenue_cents` | integer | yes | revenue silver | GAAP earned revenue from `revenue_recognition_summary`. |
29
+ | `cost_of_service_cents` | integer or null | yes | coaching + delivery silver | Coach compensation joined to lesson-event; matches Coaching cost-trio plus Delivery `lesson_event`. |
30
+ | `attributable_ad_spend_cents` | integer or null | yes | growth silver via revenue ad-spend-reconciled | S&M cost attributable to the reporting period; from Revenue's `ad-spend-reconciled` silver endpoint. |
31
+ | `operating_expense_cents` | integer or null | yes | finance operator-recorded substrate + composition | Allocated per `_opex_allocation_rule`; null until OPEX feed lands. |
32
+ | `contribution_margin_cents` | integer or null | yes | finance composition | `recognized_revenue_cents − cost_of_service_cents − attributable_ad_spend_cents`. |
33
+ | `net_income_cents` | integer or null | yes | finance composition | `contribution_margin_cents − operating_expense_cents`; null when OPEX is `null:upstream_unavailable`. |
34
+ | `opex_kind` | string enum or null | yes | finance composition | One of `native_grain`, `cross_grain_revenue_share_allocated`, `structurally_unallocated_held_at_portfolio`; null at coarsest grain when all OPEX is native-grain. |
35
+ | `opex_allocation_rule_applied` | string or null | yes | finance composition | `native_grain_plus_revenue_share_for_cross_grain` when OPEX is included; null otherwise. |
36
+ | `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
37
+
38
+ Policy: `_opex_allocation_rule: native_grain_plus_revenue_share_for_cross_grain`. Rollup rule: `sum`.
39
+
40
+ ## Source silver columns
41
+
42
+ | Silver object | Target column | Used for | Required |
43
+ |---|---|---|---:|
44
+ | `revenue.revenue_recognition` | `recognized_amount_cents`, `recognition_period`, `org_market_id` | numerator | yes |
45
+ | `coaching.coach` | `lesson_rate`, `travel_comp`, `coach_id` | cost-of-service per coach | yes |
46
+ | `delivery.lesson_event` | `coach_id`, `duration`, `org_market_id`, `completed_at` | cost-of-service per lesson | yes |
47
+ | Revenue `/api/v1/silver/ad-spend-reconciled` | `ad_spend_cents`, `source_class`, `week`, `org_market_id` | S&M cost attribution | yes |
48
+ | Finance `cash_balance_entries` (Finance-owned table) | OPEX-tagged entries per period per Organization | operating_expense_cents native-grain | yes (when scoped) |
49
+ | spine | `org_market_id`, `organization_id`, geography rollups | grain key resolution | yes |
50
+
51
+ ## Composition rule
52
+
53
+ For each row:
54
+
55
+ 1. `recognized_revenue_cents` = sum from `revenue_recognition_summary` at the row's `(geography_key, reporting_period)`.
56
+ 2. `cost_of_service_cents` = sum of `(coaching.coach.lesson_rate + coaching.coach.travel_comp) × delivery.lesson_event.duration_hours` for lessons completed in the reporting period at the row's `geography_key`. Travel comp threshold per Coaching's 2026-05-19 cost-trio declaration (5-minute back-to-back).
57
+ 3. `attributable_ad_spend_cents` = sum of `ad_spend_cents` from the Revenue `ad-spend-reconciled` endpoint, filtered to paid-source-class slice (`paid_social`, `paid_search`; `direct` excluded as non-acquisition; `other` flagged) for weeks falling within `reporting_period`, at the row's `geography_key`.
58
+ 4. `operating_expense_cents` = native-grain OPEX entries from `cash_balance_entries` table tagged at the row's `geography_key`, plus revenue-share-allocated OPEX flowing down from coarser grains per the `_opex_allocation_rule`.
59
+ 5. `contribution_margin_cents` = `recognized_revenue_cents − cost_of_service_cents − attributable_ad_spend_cents`.
60
+ 6. `net_income_cents` = `contribution_margin_cents − operating_expense_cents`.
61
+ 7. `opex_kind` tags how the OPEX in row 4 was composed (`native_grain`, `cross_grain_revenue_share_allocated`, or the row sums structurally-unallocated portfolio-grain OPEX excluded from finer-grain rows).
62
+
63
+ ## OPEX allocation rule
64
+
65
+ Per `_opex_allocation_rule: native_grain_plus_revenue_share_for_cross_grain`:
66
+
67
+ - **Native-grain OPEX**: costs incurred at the row's `geography_grain` stay at that grain. A Market-grain OPEX entry sums into Market-grain rows; an Organization-grain entry sums into Organization-grain rows; a Portfolio-grain entry sums into the Portfolio-grain row.
68
+ - **Cross-grain OPEX flowing down**: when a row at a finer grain needs to include OPEX incurred at a coarser grain, allocation is by revenue share. An Organization-grain OPEX of $X allocates to each Market in proportion to that Market's share of Organization revenue for the reporting period; an Organization gets a portion of Portfolio-grain OPEX in proportion to its share of Portfolio revenue.
69
+ - **Structurally-unallocated OPEX held at portfolio**: capital, founder draws, one-time legal, and other genuinely portfolio-level non-allocatable entries stay at the Portfolio grain only. Finer-grain rows do not include this OPEX and surface it via `opex_kind = structurally_unallocated_held_at_portfolio` on the Portfolio-grain row only.
70
+
71
+ ## Reconciliation tolerance
72
+
73
+ - `recognized_revenue_cents` reconciles to Revenue's ledger with tolerance `0` cents.
74
+ - `cost_of_service_cents` reconciles to the Coaching + Delivery silver join with tolerance `0` cents.
75
+ - `attributable_ad_spend_cents` reconciles to the Revenue `ad-spend-reconciled` silver with tolerance `0` cents.
76
+ - `operating_expense_cents` reconciles to the `cash_balance_entries` table with tolerance `0` cents once the table is in place.
77
+ - `contribution_margin_cents` and `net_income_cents` are derived; their reconciliation surface is their component figures.
78
+
79
+ ## Known gaps
80
+
81
+ - **`operating_expense_cents` is `null:upstream_unavailable`** until the Finance-owned `cash_balance_entries` table is scoped and shipped (P3/S commitment in `2026-05-19-finance-three-asks-decisions`). Once the table is live with operator-recorded OPEX entries, `operating_expense_cents` populates and `net_income_cents` follows.
82
+ - **`pnl_summary` v0 ships today with `operating_expense_cents` and `net_income_cents` null and `contribution_margin_cents` populated.** v0 corresponds to the Round 6 Platform wiring.
83
+ - No outstanding silver gaps for the revenue, cost-of-service, or ad-spend slices.
@@ -0,0 +1,98 @@
1
+ # Finance Mart Reconciliation Manifest
2
+
3
+ **Status:** authoritative for finance-mart v1.0.0
4
+ **Date:** 2026-05-18
5
+ **Owner:** finance
6
+ **Grouping:** reconciliation
7
+ **Related:** finance-mart README section 4.6, warehouse-silver v1.1.0, 2026-05-17-revenue-finance-mart-contract-reply
8
+
9
+ ## Purpose
10
+
11
+ This manifest defines the `reconciliation` grouping in the finance gold section. The grouping answers: does each business-grain finance figure tie back to Revenue's transaction-grain ledger within tolerance, and where is the variance?
12
+
13
+ The reconciliation grouping is the safety surface. Finance must surface variance here rather than hiding it in the revenue recognition, margin, or cash position rows.
14
+
15
+ ## Served grouping
16
+
17
+ `GET /api/mart/sections/finance` exposes this grouping at `groupings.reconciliation`.
18
+
19
+ The grouping grain is one row per:
20
+
21
+ - `reporting_period`
22
+ - `organization_id`
23
+ - `market_id`
24
+ - `figure_name`
25
+
26
+ `org_market_id` is the stable mart coordination key for the Organization and Market pair.
27
+
28
+ ## Served columns
29
+
30
+ | Column | Type | Required | Source | Notes |
31
+ |---|---|---:|---|---|
32
+ | `reporting_period` | string | yes | finance composition | Calendar period being reconciled. |
33
+ | `reporting_period_grain` | string enum | yes | finance composition | `month` in the first implementation. |
34
+ | `organization_id` | string | yes | spine | Platform Organization ID for the discipline. |
35
+ | `organization_slug` | string | yes | spine | Stable display slug for the Organization. |
36
+ | `market_id` | string | yes | spine | Platform Market ID. |
37
+ | `market_slug` | string | yes | spine | Stable display slug for the Market. |
38
+ | `org_market_id` | string | yes | `OrgMarket.slug` | Stable mart key for the Organization and Market pair. |
39
+ | `figure_name` | string enum | yes | finance composition | One of the figures listed below. |
40
+ | `reported_amount_cents` | integer or null | yes | finance grouping output | Amount reported by finance-mart. |
41
+ | `source_amount_cents` | integer or null | yes | source silver columns | Amount independently summed from authoritative source facts. |
42
+ | `variance_cents` | integer or null | yes | finance composition | `reported_amount_cents - source_amount_cents`. |
43
+ | `tolerance_cents` | integer | yes | this manifest | Allowed absolute variance. |
44
+ | `reconciliation_status` | string enum | yes | finance composition | `reconciled`, `variance`, `not_reconcilable_missing_source`, or `not_applicable`. |
45
+ | `source_row_count` | integer | yes | source silver columns | Count of source rows behind the reconciliation target. |
46
+ | `exception_count` | integer | yes | source silver columns | Count of exception or anomaly rows included in the target. |
47
+ | `variance_reason` | string or null | yes | finance composition | Required when status is not `reconciled`. |
48
+ | `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | Refresh point of the materialized grouping. |
49
+
50
+ ## Figures and source targets
51
+
52
+ | `figure_name` | Reported source | Authoritative source target | Tolerance | Notes |
53
+ |---|---|---|---:|---|
54
+ | `recognized_revenue` | `revenue_recognition.recognized_revenue_cents` | Sum of `revenue.recognition.recognized_amount_cents`, non-contra less contra, tied through `source_credit_ledger_entry_id` to `revenue.credit_ledger` | 0 cents | Primary revenue-recognition tie-back. |
55
+ | `gross_recognized_revenue` | `revenue_recognition.gross_recognized_revenue_cents` | Sum of non-contra `revenue.recognition.recognized_amount_cents` | 0 cents | Gross before contra rows. |
56
+ | `contra_revenue` | `revenue_recognition.contra_revenue_cents` | Sum of contra `revenue.recognition.recognized_amount_cents` | 0 cents | Refund and reversal recognition impact. |
57
+ | `cash_received` | `cash_position.cash_received_cents` | Sum of confirmed paid `revenue.order.amount_paid_cents` by `paid_at` | 0 cents | Cash receipt tie-back. |
58
+ | `cash_refunded` | `cash_position.cash_refunded_cents` | Sum of confirmed refunded `revenue.refund_item.amount_cents` by `refunded_at` | 0 cents | Cash-out tie-back. |
59
+ | `net_cash` | `cash_position.net_cash_cents` | Confirmed cash received less confirmed cash refunded | 0 cents | Derived from the two source-backed cash figures. |
60
+ | `reserved_credit_liability` | `cash_position.reserved_credit_liability_cents` | Sum of funded open `revenue.credit_reservation` obligations, cross-checked against `revenue.credit_ledger` movements | 0 cents where money amount exists | If money amount is absent, status is `not_reconcilable_missing_source`. |
61
+ | `unearned_credit_balance` | `cash_position.unearned_credit_balance_cents` | Money-denominated purchased credits not yet earned, refunded, forfeited, or adjusted away in `revenue.credit_ledger` | 0 cents where money amount exists | If credit quantity cannot be converted from source-backed money amount, status is `not_reconcilable_missing_source`. |
62
+ | `margin_revenue` | `margin.recognized_revenue_cents` | Same target as `recognized_revenue` | 0 cents | Revenue side of margin. |
63
+ | `margin_cost` | `margin.direct_cost_cents` | Delivery or Coaching source-backed direct cost columns named in `margin.md` | 0 cents where source exists | Expected initial gap until direct cost facts are present. |
64
+ | `gross_margin` | `margin.gross_margin_cents` | `margin_revenue - margin_cost` | 0 cents where both sides exist | Status is `not_reconcilable_missing_source` when cost source is missing. |
65
+
66
+ ## Source silver columns
67
+
68
+ The source columns are the union of the columns named by:
69
+
70
+ - `revenue-recognition-rollup.md`
71
+ - `cash-position.md`
72
+ - `margin.md`
73
+
74
+ In addition, reconciliation uses Revenue's ledger tie-back columns:
75
+
76
+ | Silver object | Silver column | Required | Use |
77
+ |---|---|---:|---|
78
+ | `revenue.credit_ledger` | `credit_ledger_entry_id` | yes | Ledger row identity. |
79
+ | `revenue.credit_ledger` | `type` | yes | Movement classification, including `PURCHASE_CREDIT`, `LESSON_DEBIT`, `REFUND_DEBIT`, `RESERVATION_LOCK_DEBIT`, `CREDIT_FORFEIT`, and `ADJUSTMENT` or equivalent conformed values. |
80
+ | `revenue.credit_ledger` | `delta_credits` | yes | Credit quantity movement. |
81
+ | `revenue.credit_ledger` | `amount_cents` | yes when source-backed | Money movement for money-denominated reconciliation. |
82
+ | `revenue.credit_ledger` | `occurred_at` | yes | Ledger movement timestamp. |
83
+ | `revenue.credit_ledger` | `order_item_id` | yes when present in source | Tie-back to commercial order item. |
84
+ | `revenue.credit_ledger` | `credit_reservation_id` | yes when present in source | Tie-back to reservation and lock state. |
85
+
86
+ ## Status rules
87
+
88
+ `reconciled` means `abs(variance_cents) <= tolerance_cents`.
89
+
90
+ `variance` means the source exists, the reported figure exists, and the absolute variance exceeds tolerance.
91
+
92
+ `not_reconcilable_missing_source` means the reported figure depends on a source fact that is absent from silver or absent from the producing source. This status is expected for full margin-cost reconciliation until source-backed direct cost facts exist.
93
+
94
+ `not_applicable` means the figure does not apply to the row's grain, for example a grouping with no source activity in that period and dimension.
95
+
96
+ ## Known gaps
97
+
98
+ The margin-cost and gross-margin figures are not fully reconcilable until direct cost facts exist in Delivery or Coaching silver. Finance must not mark those figures reconciled based only on Revenue data.
@@ -0,0 +1,87 @@
1
+ # Finance Mart Revenue Recognition Rollup Manifest
2
+
3
+ **Status:** authoritative for finance-mart v1.0.0
4
+ **Date:** 2026-05-18
5
+ **Owner:** finance
6
+ **Grouping:** revenue_recognition
7
+ **Related:** finance-mart README section 4.3, warehouse-silver v1.1.0, 2026-05-17-revenue-finance-mart-contract-reply
8
+
9
+ ## Purpose
10
+
11
+ This manifest defines the `revenue_recognition` grouping in the finance gold section. The grouping answers: what recognized revenue did a reporting period earn, and how does that revenue roll up by Organization and Market?
12
+
13
+ The grouping is business-grain reporting over Revenue's transaction-grain truth. Finance composes it from the Revenue silver face and the identity and geography spine. Finance does not read Revenue bronze and does not restate Revenue's ledger.
14
+
15
+ ## Served grouping
16
+
17
+ `GET /api/mart/sections/finance` exposes this grouping at `groupings.revenue_recognition`.
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. Service Area is not assumed from the spine.
26
+
27
+ ## Served columns
28
+
29
+ | Column | Type | Required | Source | Notes |
30
+ |---|---|---:|---|---|
31
+ | `reporting_period` | string | yes | `revenue.recognition.recognition_period` | Calendar period key. The first implementation uses month grain unless the route adds an explicit `reporting_period_grain`. |
32
+ | `reporting_period_grain` | string enum | yes | finance composition | `month` in the first implementation. Future grains are additive if existing rows keep their meaning. |
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 | finance composition over `revenue.recognition` | Net recognized revenue, gross recognitions less contra recognitions. |
39
+ | `gross_recognized_revenue_cents` | integer | yes | `revenue.recognition.recognized_amount_cents` | Sum of non-contra recognition rows. |
40
+ | `contra_revenue_cents` | integer | yes | `revenue.recognition.recognized_amount_cents`, `is_contra` | Sum of contra rows as a positive contra amount. |
41
+ | `recognition_count` | integer | yes | `revenue.recognition.recognition_id` | Count of recognition rows in the grouping. |
42
+ | `exception_recognition_count` | integer | yes | `revenue.recognition.has_exception` | Count of recognition rows marked with an exception. |
43
+ | `exception_revenue_cents` | integer | yes | `revenue.recognition.has_exception`, `recognized_amount_cents` | Net recognized revenue from exception rows. |
44
+ | `has_exception` | boolean | yes | finance composition | True when `exception_recognition_count` is greater than zero. |
45
+ | `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | Refresh point of the materialized grouping. |
46
+
47
+ ## Source silver columns
48
+
49
+ The implementation reads the Revenue silver face, expected object `revenue.recognition`, with these source-backed conformed columns:
50
+
51
+ | Silver column | Required | Use |
52
+ |---|---:|---|
53
+ | `recognition_id` | yes | Stable recognition-row identifier and row count. |
54
+ | `person_id` | yes where source row is person-related | Join to the spine for Organization and Market. |
55
+ | `recognition_period` | yes | Reporting period. |
56
+ | `recognized_amount_cents` | yes | Amount in integer cents. |
57
+ | `is_contra` | yes | Contra classification for refunds and reversals. |
58
+ | `created_via` | yes | Variance analysis by recognition origin. Expected values include `LESSON_COMPLETION_JOB`, `REFUND_JOB`, `MANUAL`, and `BACKFILL`. |
59
+ | `source_credit_ledger_entry_id` | yes | Tie-back into Revenue's credit ledger for reconciliation. |
60
+ | `order_item_id` | yes when present in source | Commercial tie-back. |
61
+ | `credit_reservation_id` | yes when present in source | Reservation and obligation tie-back. |
62
+ | `occurred_at` | yes | Event timestamp. |
63
+ | `created_at` | yes | Row creation timestamp, useful for freshness checks. |
64
+ | `has_exception` | yes | Exception inclusion flag. |
65
+ | `exception_reason` | yes when `has_exception` is true | Exception explanation. |
66
+
67
+ The implementation joins the spine through `person_id` for `organization_id`, `organization_slug`, `market_id`, `market_slug`, and `org_market_id`.
68
+
69
+ ## Composition rule
70
+
71
+ For each grouping row:
72
+
73
+ `gross_recognized_revenue_cents = sum(recognized_amount_cents where is_contra = false)`
74
+
75
+ `contra_revenue_cents = sum(recognized_amount_cents where is_contra = true)`
76
+
77
+ `recognized_revenue_cents = gross_recognized_revenue_cents - contra_revenue_cents`
78
+
79
+ Exception rows are included at face value. They are not filtered out. Their count and net amount are surfaced through `exception_recognition_count`, `exception_revenue_cents`, and `has_exception` so reporting consumers can see variance risk without Finance silently dropping Revenue facts.
80
+
81
+ ## Reconciliation tolerance
82
+
83
+ This grouping reconciles to Revenue's `revenue.recognition` and `revenue.credit_ledger` surfaces with a tolerance of `0` cents. Money is carried as integer minor units, so rounding variance is not expected. Any non-zero variance belongs in the `reconciliation` grouping.
84
+
85
+ ## Known gaps
86
+
87
+ None for the revenue-recognition figure set named here, assuming the live Revenue silver face carries the source-backed columns listed above. If the face uses different names for the same source-backed facts, Platform should conform them to the names in this manifest or return the equivalent mapping before implementation.
@@ -0,0 +1,94 @@
1
+ # Finance Mart Unit Economics Manifest
2
+
3
+ **Status:** authoritative for finance-mart v2.0.x
4
+ **Date:** 2026-05-19
5
+ **Owner:** finance
6
+ **Sub-section:** unit_economics
7
+ **Related:** finance-mart README §4.3, warehouse-silver v1.1.0, 2026-05-19-finance-revenue-per-active-student-absorption, 2026-05-19-platform-mart-round-status-and-next-silver-asks (Round 5 wiring of revenue_concentration)
8
+
9
+ ## Purpose
10
+
11
+ This manifest defines the `unit_economics` sub-section in the finance gold section. The sub-section answers: what is the per-active-student revenue intensity at the operational unit level, and how concentrated is revenue across the customer base?
12
+
13
+ Unit economics composes recognized revenue against operational-scale denominators. The sub-section is keyed at the business reporting grain (`reporting_period × geography_key`) for the per-period view, with `revenue_per_active_student` reading Delivery's snapshot-week-end active-student count and `revenue_concentration` reading per-customer revenue aggregates.
14
+
15
+ ## Served sub-section
16
+
17
+ `GET /api/mart/sections/finance` exposes this sub-section at `groupings.unit_economics` (or the equivalent typed grouping per Platform's `FinanceComputeResult` shape). Two metrics, one row shape per metric, both at the business reporting grain.
18
+
19
+ ## Served columns
20
+
21
+ ### `revenue_per_active_student`
22
+
23
+ Grain: one row per `(org_market_id, week)`.
24
+
25
+ | Column | Type | Required | Source | Notes |
26
+ |---|---|---:|---|---|
27
+ | `org_market_id` | string | yes | spine | Stable mart key for the Organization and Market pair. |
28
+ | `week` | string | yes | finance composition | ISO week (YYYY-Www, UTC), keyed to lesson-week for the numerator. |
29
+ | `recognized_revenue_cents` | integer | yes | revenue silver | Numerator: GAAP earned revenue summed by `org_market × lesson_week`. |
30
+ | `active_student_count` | integer or null | yes | delivery silver | Denominator: Delivery's `active_student_count` snapshot-week-end value. |
31
+ | `revenue_per_active_student_cents` | integer or null | yes | finance composition | `recognized_revenue_cents / active_student_count`; null when denominator is below cohort floor of 5. |
32
+ | `null_reason` | string or null | yes | finance composition | `null:insufficient_data` when `active_student_count < 5`; null otherwise. |
33
+ | `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | Refresh point of the materialized row. |
34
+
35
+ Policy: `_axis_reconciliation: revenue_lesson_week_div_delivery_snapshot_week_end`. Rollup rule `weighted_mean_by_revenue` per spec. Cohort floor `active_student_count < 5`.
36
+
37
+ ### `revenue_concentration`
38
+
39
+ Grain: one row per `(org_market_id, reporting_period)` per Revenue's Round 5 decision (`_concentration_n: 10`, `_concentration_window_weeks: 52`).
40
+
41
+ | Column | Type | Required | Source | Notes |
42
+ |---|---|---:|---|---|
43
+ | `org_market_id` | string | yes | spine | |
44
+ | `reporting_period` | string | yes | finance composition | Calendar period key. |
45
+ | `top_n_revenue_cents` | integer | yes | revenue silver | Sum of recognized revenue from the top-10 customers by GAAP earned revenue in the trailing 52-week window ending at `reporting_period`. |
46
+ | `total_revenue_cents` | integer | yes | revenue silver | Sum of all customers' recognized revenue in the same window. |
47
+ | `concentration_ratio_bps` | integer | yes | finance composition | `round(top_n_revenue_cents / total_revenue_cents × 10000)`; basis points. |
48
+ | `_concentration_n` | integer | yes | finance composition | 10 (Revenue's Round 5 decision). |
49
+ | `_concentration_window_weeks` | integer | yes | finance composition | 52 (trailing year). |
50
+ | `as_of` | UTC ISO 8601 timestamp | yes | finance materialization | |
51
+
52
+ Policy: `_concentration_n: 10`, `_concentration_window_weeks: 52`. Rollup rule `weighted_mean_by_revenue`.
53
+
54
+ ## Source silver columns
55
+
56
+ | Silver object | Target column | Used for | Required |
57
+ |---|---|---|---:|
58
+ | `revenue.revenue_recognition` | `recognized_amount_cents` | both metrics, numerator | yes |
59
+ | `revenue.revenue_recognition` | `recognition_period` | both metrics, period alignment | yes |
60
+ | `revenue.revenue_recognition` | `person_id` | `revenue_concentration` per-customer aggregation | yes |
61
+ | `revenue.revenue_recognition` | `org_market_id` | spine attribution | yes |
62
+ | `delivery.customer` | `status = 'active'` filter | `revenue_per_active_student` denominator | yes |
63
+ | `delivery.customer` | `org_market_id` | spine attribution | yes |
64
+ | `delivery.customer` | `snapshot_week` | snapshot-week-end alignment | yes |
65
+ | spine | `org_market_id`, `organization_id`, `market_id` | geography keys | yes |
66
+
67
+ ## Composition rule
68
+
69
+ `revenue_per_active_student_cents`:
70
+
71
+ 1. Sum `recognized_amount_cents` from `revenue.revenue_recognition` over `recognition_period` falling within `lesson_week`, grouped by `org_market_id`.
72
+ 2. Read `active_student_count` from `delivery.customer` at the snapshot-week-end value matching `week`.
73
+ 3. If `active_student_count >= 5`, return `recognized_revenue_cents / active_student_count`; otherwise emit `null:insufficient_data`.
74
+
75
+ `concentration_ratio_bps`:
76
+
77
+ 1. Aggregate `recognized_amount_cents` per `(org_market_id, person_id)` over the trailing 52-week window ending at `reporting_period`.
78
+ 2. Take the top-10 `person_id` rows by aggregate revenue; sum to `top_n_revenue_cents`.
79
+ 3. Sum all rows to `total_revenue_cents`.
80
+ 4. Return `round(top_n_revenue_cents / total_revenue_cents × 10000)` as basis points.
81
+
82
+ ## Reconciliation tolerance
83
+
84
+ `revenue_per_active_student` numerator reconciles to Revenue's transaction-grain ledger with tolerance `0` cents (the numerator is the sum of recognized-revenue rows; identity).
85
+
86
+ `revenue_concentration` numerator and denominator both reconcile to Revenue's ledger with tolerance `0` cents. The ratio itself is a derived figure with no reconciliation target outside Finance.
87
+
88
+ ## Known gaps
89
+
90
+ `revenue_per_active_student` is wirable today: both upstream surfaces (`revenue.revenue_recognition`, `delivery.customer.status`) are live.
91
+
92
+ `revenue_concentration` is wirable today: per-customer revenue aggregation from `revenue.revenue_recognition.person_id` is the standard cross-domain join. The metric flips to beta with Platform's Round 5 wiring.
93
+
94
+ No outstanding silver gaps for this sub-section.
@@ -0,0 +1,162 @@
1
+ # Growth Warehouse API contract
2
+
3
+ **Status:** active
4
+ **Version:** 1
5
+ **Producer:** growth
6
+ **Date:** 2026-05-12
7
+
8
+ Read-only HTTP surface for forecasting and analytics consumers (primarily Them OS) to pull raw Growth-domain data without managing direct Postgres connections.
9
+
10
+ The same data is also reachable via the dbt source declarations in `platform/warehouse/models/sources/growth.yml` for BI tools that prefer SQL. This API is the lower-friction option for application consumers.
11
+
12
+ ## Auth
13
+
14
+ Bearer token shared with each consumer.
15
+
16
+ - Header: `Authorization: Bearer <THEM_OS_API_KEY>`
17
+ - Token lives in the Growth deploy's environment (`THEM_OS_API_KEY` on Vercel).
18
+ - Consumers store the same value as their outbound credential.
19
+ - Comparison is constant-time (`crypto.timingSafeEqual`) to avoid timing-leak attacks on the token.
20
+
21
+ Tokens are not multi-tenant in v1. If a second consumer needs access, mint a separate token (`<CONSUMER>_API_KEY`) and add the auth check to recognize either; rotating one consumer should not invalidate the other.
22
+
23
+ ## Endpoints
24
+
25
+ ### `GET /api/warehouse/growth/manifest`
26
+
27
+ Returns the catalogue: every exposed table, its primary key, cursor column, optional date filter column, and full column descriptors with types and nullability.
28
+
29
+ Response (abbreviated):
30
+
31
+ ```json
32
+ {
33
+ "generated_at": "2026-05-12T20:00:00.000Z",
34
+ "domain": "growth",
35
+ "api_version": 1,
36
+ "max_page_size": 5000,
37
+ "default_page_size": 1000,
38
+ "tables": [
39
+ {
40
+ "table": "lead_intake",
41
+ "description": "Form submissions ...",
42
+ "primary_key": "id",
43
+ "cursor_column": "id",
44
+ "date_column": "createdAt",
45
+ "columns": [
46
+ { "name": "id", "type": "string", "nullable": false, "description": "fsm_<UUIDv7>" },
47
+ { "name": "createdAt", "type": "datetime", "nullable": false }
48
+ ]
49
+ }
50
+ ]
51
+ }
52
+ ```
53
+
54
+ Consumers SHOULD fetch the manifest on startup to discover available tables, then cache it for the session. The manifest is cheap (~10 KB) but doesn't change between deploys.
55
+
56
+ ### `GET /api/warehouse/growth/<table>?since=&until=&cursor=&limit=`
57
+
58
+ Returns one page of rows from the named table. Keyset pagination on the cursor column.
59
+
60
+ Query params:
61
+
62
+ | Param | Type | Default | Notes |
63
+ |---|---|---|---|
64
+ | `since` | ISO 8601 | none | Filters `<date_column> >= since`. Omit to start from the beginning. |
65
+ | `until` | ISO 8601 | none | Filters `<date_column> < until`. Exclusive upper bound. |
66
+ | `cursor` | string | none | Opaque cursor returned from the previous page's `next_cursor`. |
67
+ | `limit` | integer | 1000 | Page size. Capped at 5000. |
68
+
69
+ Response shape:
70
+
71
+ ```json
72
+ {
73
+ "table": "lead_intake",
74
+ "row_count": 1000,
75
+ "rows": [{ "id": "fsm_...", "firstName": "...", "createdAt": "2026-05-12T..." }],
76
+ "next_cursor": "fsm_...",
77
+ "has_more": true,
78
+ "served_at": "2026-05-12T20:00:01.234Z"
79
+ }
80
+ ```
81
+
82
+ Pagination loop on the consumer:
83
+
84
+ ```python
85
+ cursor = None
86
+ while True:
87
+ response = get(f"/api/warehouse/growth/lead_intake?since={since}&cursor={cursor}&limit=2000")
88
+ yield from response["rows"]
89
+ if not response["has_more"]:
90
+ break
91
+ cursor = response["next_cursor"]
92
+ ```
93
+
94
+ ## Exposed tables
95
+
96
+ The current whitelist (v1; see `growth/src/modules/warehouse-api/schema.ts` for the canonical list and full column descriptors):
97
+
98
+ | Table | Grain | Primary key | Cursor column | Date column |
99
+ |---|---|---|---|---|
100
+ | `lead_intake` | One per form submission | `id` (`fsm_<UUIDv7>`) | `id` | `createdAt` |
101
+ | `lead_attribution` | One per visitor-id-stitched touch | `id` (`lat_<UUIDv7>`) | `id` | `capturedAt` |
102
+ | `campaign` | One per campaign | `id` (`cmp_<UUIDv7>`) | `id` | `createdAt` |
103
+ | `campaign_external_mapping` | One per (platform, ext acct, ext campaign) | `id` (`cem_<UUIDv7>`) | `id` | `createdAt` |
104
+ | `campaign_spend_daily` | One per (campaign, date, source) | `id` (`csd_<UUIDv7>`) | `id` | `date` |
105
+ | `qualified_intake` | One per qualified form_submission | `formSubmissionId` (`fsm_<UUIDv7>`) | `formSubmissionId` | `firstQualifiedAt` |
106
+ | `lead_intake_correlation` | One per Sales Lead | `leadId` (`lead_<UUIDv7>`) | `leadId` | `observedAt` |
107
+ | `subscriber` | One per email signup | `id` (`sub_<UUIDv7>`) | `id` | `createdAt` |
108
+ | `acquisition_suppression` | One per archived person | `person_id` (`per_<UUIDv7>`) | `person_id` | `archived_at` |
109
+ | `intake_event_correlation` | One per intake.captured emit | `eventId` (`evt_<UUIDv7>`) | `eventId` | `emittedAt` |
110
+
111
+ The `growth.touchpoint` warehouse view (the fully-resolved, person-keyed funnel attribution surface) is NOT in this API. It lives in the dbt warehouse and is queryable via SQL only, by design: it depends on Platform's `intake.matched` events and would couple the API to the warehouse DSN. Forecasting consumers that need it should connect to the warehouse Postgres directly.
112
+
113
+ ## Column type encoding
114
+
115
+ The manifest's column `type` values map to JSON values like this:
116
+
117
+ | Manifest type | JSON value | Notes |
118
+ |---|---|---|
119
+ | `string` | string | Trimmed/normalized per Growth's writers. |
120
+ | `bigint_string` | string | int8 / BigInt columns serialized as decimal strings to avoid JS Number precision loss. `spendCents` is the only field that uses this today. |
121
+ | `number` | number | Standard JSON number. |
122
+ | `boolean` | boolean | |
123
+ | `datetime` | string (ISO 8601 UTC) | Postgres `timestamp(3)` → `2026-05-12T20:00:00.000Z`. Date-only columns (e.g., `campaign_spend_daily.date`) also serialize as ISO timestamps with midnight UTC. |
124
+ | `json` | object/array | JSONB columns. |
125
+ | `null_only` | null | Reserved; not currently used. |
126
+
127
+ Null values come through as JSON null regardless of declared type.
128
+
129
+ ## Refresh cadence
130
+
131
+ Reads are against Growth's live Postgres (the same database Growth's app writes to). There is no replication lag. The data is real-time to within transactional commit visibility.
132
+
133
+ If/when this API moves to read from the warehouse Postgres (dbt-compiled views, separate Supabase project), the response will gain a `replication_lag_seconds` field and the warehouse refresh cadence will be documented here. Consumers should treat that future field as optional and degrade gracefully when it is absent.
134
+
135
+ ## Versioning
136
+
137
+ Additive changes (new tables, new columns on existing tables, new optional query params) bump the manifest's column / table list without changing `api_version`.
138
+
139
+ Breaking changes (removing a table, renaming a column, changing a column's type) bump `api_version` to 2 and ship a new path prefix (`/api/warehouse/growth/v2/...`) with a deprecation window of at least two weeks during which both versions are served.
140
+
141
+ `acquisition_suppression`'s column names use snake_case (`person_id`, `archived_at`) where the rest use camelCase. This is because the underlying table maps differently in Prisma; consumers should treat column casing as opaque and rely on the manifest's `columns[].name`.
142
+
143
+ ## Out of scope for v1
144
+
145
+ - Aggregations or derived models. Consumers do their own rollups. If a derived view becomes load-bearing, it lands as a dbt model in the warehouse, not as an API endpoint.
146
+ - Write access. The API is read-only. Mutations to Growth data flow through Growth's existing app routes.
147
+ - Cross-domain joins. The API exposes Growth-domain raw data only. Joins against Person facts (Platform), Lead lifecycle (Sales), Revenue ledger, etc. happen on the consumer side or via the dbt warehouse where source declarations from multiple domains can join.
148
+
149
+ ## Operational
150
+
151
+ - Env var on Growth's deploy: `THEM_OS_API_KEY` (generate strong; rotate annually or on compromise).
152
+ - Same value on Them OS's outbound credential store.
153
+ - Rate limit: not yet implemented; v1 trusts the bearer token. If a second consumer joins, add per-token rate limits before issuing the second token.
154
+ - Observability: every request logs to console (Vercel function logs) with the table name and the row count served. Add per-request latency tracking when usage grows past trivial.
155
+
156
+ ## References
157
+
158
+ - API module source: `growth/src/modules/warehouse-api/`
159
+ - Route handlers: `growth/src/app/api/warehouse/growth/manifest/route.ts` and `growth/src/app/api/warehouse/growth/[table]/route.ts`
160
+ - dbt source declarations (parallel surface for SQL consumers): `platform/warehouse/models/sources/growth.yml`
161
+ - Growth domain doc: `coordination/domains/growth.md`
162
+ - Person archive propagation (where `acquisition_suppression` came from): `coordination/memos/2026/2026-05-11-platform-archive-propagation-sales-growth.md`