mattrguerra/meridian-outreach-vaultmainPrivate on GitHub, published here in full

partner-thresholds.md

Language Markdown9.4 KB176 lines

07-Automation/partner-thresholds.md

  1. ---
  2. type: automation-config
  3. scope: 07-Automation
  4. tags: [claude, outreach, automation, config]
  5. created: 2026-07-22
  6. last_modified: 2026-08-12
  7. ---
  8. # Partner Client Thresholds: Weekly Partner Client Pipeline config
  9. This is the **single source of truth** for how the weekly pipeline decides who is a
  10. partner client and at what tier. Edit it in VS Code and re-upload. The driver
  11. (`weekly_partner_pipeline.py`) parses the first ` ```yaml ` block below. Everything
  12. outside that block is notes for you, not read by the code.
  13. > **The arming switch.** Any `TBD` in the active mode, or in `min_orders_to_qualify`,
  14. > leaves the pipeline read-only: classify still exports a spreadsheet, but the stub, XML
  15. > and workbook writes refuse to run. `min_orders_to_qualify` was the last `TBD`; setting
  16. > it was step 3 of the [[Activation-Runbook]].
  17. ## Config
  18. ```yaml
  19. # ─── classification ────────────────────────────────────────────────────────────
  20. # mode: how a customer is turned into a partner client tier.
  21. # ltv_bands -> assign a tier by lifetime spend falling inside a band (default;
  22. # mirrors the Partner Program)
  23. # multi_factor -> score several signals, then band the score
  24. # simple_gate -> single cutoff, partner client vs. not, no tiers
  25. mode: ltv_bands
  26. # ─── ltv_bands mode ──────────────────────────────────────────────────────────
  27. # Lower bound is inclusive, upper is exclusive. Anyone below the lowest band's
  28. # min is "Not a partner client". Dollars, no symbols/commas. Leave upper of the top
  29. # band as null for "and up".
  30. ltv_bands:
  31. - tier: "Palladium Partner"
  32. min_ltv: 2500
  33. max_ltv: 8000
  34. - tier: "Platinum Partner"
  35. min_ltv: 8000
  36. max_ltv: 25000
  37. - tier: "Iridium Partner"
  38. min_ltv: 25000
  39. max_ltv: 75000
  40. - tier: "Rhodium Partner"
  41. min_ltv: 75000
  42. max_ltv: null # and up
  43. # ─── multi_factor mode (only read when mode: multi_factor) ────────────────────
  44. # Score = sum(weight * normalized_signal). Set a partner_cutoff and per-tier
  45. # score bands. Weights and cutoffs are placeholders.
  46. multi_factor:
  47. weights:
  48. ltv: 1.0
  49. partner_cutoff: TBD # min score to count as a partner client at all
  50. score_bands: # optional; same shape as ltv_bands but on score
  51. - tier: "Palladium Partner"
  52. min_score: TBD
  53. max_score: TBD
  54. # ─── simple_gate mode (only read when mode: simple_gate) ──────────────────────
  55. simple_gate:
  56. signal: ltv # ltv | repeat_revenue | order_count
  57. min_value: 2500 # at/above this -> "Partner Client"; below -> "Not a partner client"
  58. # ─── tier overrides ──────────────────────────────────────────────────────────
  59. # Accounts whose tier is set by the RELATIONSHIP, not by per-mailbox spend.
  60. # Applied before the bands and before min_orders_to_qualify, so an override wins
  61. # outright: an overridden mailbox is that tier even at $0 lifetime spend.
  62. #
  63. # Why this exists: on 2026-08-12 Matt re-tiered the Harlow Ridge Homes mailboxes to
  64. # Rhodium by hand (Harlow Ridge is a directly-partnered builder Hazel speaks with
  65. # every other day; the tier belongs to the company, not the individual mailbox). The
  66. # rule lived only in MEMORY/DECISIONS, so the very next weekly run demoted most of
  67. # them straight back to their computed bands. Encoding it here stops that.
  68. #
  69. # SCOPE IS DELIBERATELY PER-EMAIL, NOT PER-DOMAIN. Plinth carries more harlowridge
  70. # mailboxes than the ones re-tiered, and most of the rest classify as "Not a partner
  71. # client" on spend. A domain-wide override would promote every one of them to Rhodium
  72. # and mint a stub for each, which is not what the 08-12 decision did. Adding a mailbox
  73. # here is Matt's call, never inferred from data. To go domain-wide instead, that is a
  74. # DECISIONS entry first.
  75. #
  76. # Keyed by exact lowercase email -> tier string (must match an ltv_bands tier name).
  77. tier_overrides:
  78. emails:
  79. maren.achterberg@harlowridge.example: Rhodium Partner
  80. soren.blackwood@harlowridge.example: Rhodium Partner
  81. lena.coldwell@harlowridge.example: Rhodium Partner
  82. callum.dunmore@harlowridge.example: Rhodium Partner
  83. # ─── what counts as revenue ───────────────────────────────────────────────────
  84. # amount_field: which order dollar figure feeds LTV.
  85. # grand_total -> grand total incl. shipping + tax (what the customer paid)
  86. # subtotal -> product subtotal only
  87. amount_field: grand_total
  88. # counted_order_statuses: include-list. Only these status_code values are summed.
  89. # Empty list = all statuses (minus the excluded list below).
  90. counted_order_statuses: []
  91. # excluded_order_statuses: these codes are NEVER summed, regardless of the include
  92. # list. 27 = Order Cancelled, 28 = Order Refunded. This matches how the roster
  93. # workbook counts: verified 2026-07-23 against one long-standing account, whose
  94. # workbook "Total Spent (Orders)" equals the excluding-cancelled order sum, so the
  95. # weekly delta must use the same basis or baseline+delta drifts. amount_field stays
  96. # grand_total (shipping in), also verified as the workbook's basis. PENDING NORA: if
  97. # the leadership call picks different rules (e.g. subtotal, or counting refunds),
  98. # change these two keys and note it in DECISIONS.
  99. excluded_order_statuses: [27, 28]
  100. # ─── incremental history (v2, replaces the old ltv_window pull) ──────────────
  101. # Lifetime value is computed from the LOCAL history store
  102. # (07-Automation/data/order_history.json): the roster baseline + every order
  103. # ingested since. The API is never asked for an all-time total. Plinth orders.fetch
  104. # silently truncates big result sets (~70 orders/response, verified 2026-07-22; see
  105. # DECISIONS.md). baseline as_of is set at seed time.
  106. # pull_window_days: how far back the weekly delta pull reaches. 8 = one day of
  107. # overlap with the previous Monday run; the ingest dedupe makes overlap free.
  108. pull_window_days: 8
  109. # suspect_row_count: a single pull window returning this many rows is treated as
  110. # possibly truncated. The runner keeps paging (offset + dedupe) until a page adds
  111. # zero new order refs, and/or narrows the date window.
  112. suspect_row_count: 50
  113. # repeat_revenue = spend on every order after the customer's earliest counted order.
  114. # (With a baseline: baseline repeat + all post-baseline counted orders.)
  115. # ─── reconciliation ──────────────────────────────────────────────────────────
  116. # reconcile_with_store_group: if true, also record Plinth customer_group next to our
  117. # computed tier so you can spot mismatches (store says Palladium Partner, our spend
  118. # math says something else). Does not change the decision.
  119. reconcile_with_store_group: true
  120. # ─── scope / performance ─────────────────────────────────────────────────────
  121. # page_size: Plinth page size for paginated pulls. HARD CAP 100: the API rejects
  122. # larger values (validated 2026-07-22). Plinth also caps at 10 req / 10 sec and
  123. # throttles rather than fails, so pulls just slow down.
  124. page_size: 100
  125. # min_orders_to_qualify: ignore customers with fewer than this many counted orders
  126. # (0 = keep everyone). Set with Ines: one qualifying order is enough.
  127. min_orders_to_qualify: 1
  128. # ─── run behavior ────────────────────────────────────────────────────────────
  129. # autonomy: how far an unattended (scheduled) run may go on its own.
  130. # spreadsheet_only -> pull + classify + export xlsx, then STOP
  131. # through_vault -> also create/upgrade stubs + refresh XML, hold Bitrix (current)
  132. # full -> entire chain incl. Bitrix push
  133. autonomy: through_vault # ENABLED 2026-07-23: auto stubs + XML; Bitrix / enrich / outreach-routing held
  134. ```
  135. ## Activation checklist (done 2026-07-23; kept for a rebuild)
  136. 1. **Seed the baseline** (one time). `--as-of` is the date the roster's totals run
  137. through ("LTV up until Monday", so that Monday's date). Dry-run first:
  138. ```
  139. python3 07-Automation/weekly_partner_pipeline.py seed-baseline \
  140. --sheet <path-to-roster> --as-of <monday> --dry-run
  141. ```
  142. Check the detected column mapping, fix any miss with `--map "field=Exact Header"`,
  143. then run again without `--dry-run`.
  144. 2. **Catch up the gap.** Pull orders from `as_of` to today (day-windowed, offset+dedupe
  145. until `added=0`) and `ingest` them, so the history is current before the first
  146. scheduled run.
  147. 3. **Arm it:** set `min_orders_to_qualify` (the last `TBD`, the arming switch).
  148. 4. **Dry run:** `classify` then `verify`. Eyeball the spreadsheet, especially the
  149. `has_baseline=False` rows (customers seen in the delta with no baseline row).
  150. 5. **Enable the schedule** per [[scheduled-task]].
  151. **Program bands (lifetime spend):** Palladium $2.5K to $8K · Platinum $8K to $25K ·
  152. Iridium $25K to $75K · Rhodium $75K+. "Once in the program, always in the program":
  153. lifetime, never trailing.