partner-thresholds.md
Language Markdown9.4 KB176 lines
---type: automation-configscope: 07-Automationtags: [claude, outreach, automation, config]created: 2026-07-22last_modified: 2026-08-12---# Partner Client Thresholds: Weekly Partner Client Pipeline configThis is the **single source of truth** for how the weekly pipeline decides who is apartner client and at what tier. Edit it in VS Code and re-upload. The driver(`weekly_partner_pipeline.py`) parses the first ` ```yaml ` block below. Everythingoutside that block is notes for you, not read by the code.> **The arming switch.** Any `TBD` in the active mode, or in `min_orders_to_qualify`,> leaves the pipeline read-only: classify still exports a spreadsheet, but the stub, XML> and workbook writes refuse to run. `min_orders_to_qualify` was the last `TBD`; setting> it was step 3 of the [[Activation-Runbook]].## Config```yaml# ─── classification ────────────────────────────────────────────────────────────# mode: how a customer is turned into a partner client tier.# ltv_bands -> assign a tier by lifetime spend falling inside a band (default;# mirrors the Partner Program)# multi_factor -> score several signals, then band the score# simple_gate -> single cutoff, partner client vs. not, no tiersmode: ltv_bands# ─── ltv_bands mode ──────────────────────────────────────────────────────────# Lower bound is inclusive, upper is exclusive. Anyone below the lowest band's# min is "Not a partner client". Dollars, no symbols/commas. Leave upper of the top# band as null for "and up".ltv_bands:- tier: "Palladium Partner"min_ltv: 2500max_ltv: 8000- tier: "Platinum Partner"min_ltv: 8000max_ltv: 25000- tier: "Iridium Partner"min_ltv: 25000max_ltv: 75000- tier: "Rhodium Partner"min_ltv: 75000max_ltv: null # and up# ─── multi_factor mode (only read when mode: multi_factor) ────────────────────# Score = sum(weight * normalized_signal). Set a partner_cutoff and per-tier# score bands. Weights and cutoffs are placeholders.multi_factor:weights:ltv: 1.0partner_cutoff: TBD # min score to count as a partner client at allscore_bands: # optional; same shape as ltv_bands but on score- tier: "Palladium Partner"min_score: TBDmax_score: TBD# ─── simple_gate mode (only read when mode: simple_gate) ──────────────────────simple_gate:signal: ltv # ltv | repeat_revenue | order_countmin_value: 2500 # at/above this -> "Partner Client"; below -> "Not a partner client"# ─── tier overrides ──────────────────────────────────────────────────────────# Accounts whose tier is set by the RELATIONSHIP, not by per-mailbox spend.# Applied before the bands and before min_orders_to_qualify, so an override wins# outright: an overridden mailbox is that tier even at $0 lifetime spend.## Why this exists: on 2026-08-12 Matt re-tiered the Harlow Ridge Homes mailboxes to# Rhodium by hand (Harlow Ridge is a directly-partnered builder Hazel speaks with# every other day; the tier belongs to the company, not the individual mailbox). The# rule lived only in MEMORY/DECISIONS, so the very next weekly run demoted most of# them straight back to their computed bands. Encoding it here stops that.## SCOPE IS DELIBERATELY PER-EMAIL, NOT PER-DOMAIN. Plinth carries more harlowridge# mailboxes than the ones re-tiered, and most of the rest classify as "Not a partner# client" on spend. A domain-wide override would promote every one of them to Rhodium# and mint a stub for each, which is not what the 08-12 decision did. Adding a mailbox# here is Matt's call, never inferred from data. To go domain-wide instead, that is a# DECISIONS entry first.## Keyed by exact lowercase email -> tier string (must match an ltv_bands tier name).tier_overrides:emails:maren.achterberg@harlowridge.example: Rhodium Partnersoren.blackwood@harlowridge.example: Rhodium Partnerlena.coldwell@harlowridge.example: Rhodium Partnercallum.dunmore@harlowridge.example: Rhodium Partner# ─── what counts as revenue ───────────────────────────────────────────────────# amount_field: which order dollar figure feeds LTV.# grand_total -> grand total incl. shipping + tax (what the customer paid)# subtotal -> product subtotal onlyamount_field: grand_total# counted_order_statuses: include-list. Only these status_code values are summed.# Empty list = all statuses (minus the excluded list below).counted_order_statuses: []# excluded_order_statuses: these codes are NEVER summed, regardless of the include# list. 27 = Order Cancelled, 28 = Order Refunded. This matches how the roster# workbook counts: verified 2026-07-23 against one long-standing account, whose# workbook "Total Spent (Orders)" equals the excluding-cancelled order sum, so the# weekly delta must use the same basis or baseline+delta drifts. amount_field stays# grand_total (shipping in), also verified as the workbook's basis. PENDING NORA: if# the leadership call picks different rules (e.g. subtotal, or counting refunds),# change these two keys and note it in DECISIONS.excluded_order_statuses: [27, 28]# ─── incremental history (v2, replaces the old ltv_window pull) ──────────────# Lifetime value is computed from the LOCAL history store# (07-Automation/data/order_history.json): the roster baseline + every order# ingested since. The API is never asked for an all-time total. Plinth orders.fetch# silently truncates big result sets (~70 orders/response, verified 2026-07-22; see# DECISIONS.md). baseline as_of is set at seed time.# pull_window_days: how far back the weekly delta pull reaches. 8 = one day of# overlap with the previous Monday run; the ingest dedupe makes overlap free.pull_window_days: 8# suspect_row_count: a single pull window returning this many rows is treated as# possibly truncated. The runner keeps paging (offset + dedupe) until a page adds# zero new order refs, and/or narrows the date window.suspect_row_count: 50# repeat_revenue = spend on every order after the customer's earliest counted order.# (With a baseline: baseline repeat + all post-baseline counted orders.)# ─── reconciliation ──────────────────────────────────────────────────────────# reconcile_with_store_group: if true, also record Plinth customer_group next to our# computed tier so you can spot mismatches (store says Palladium Partner, our spend# math says something else). Does not change the decision.reconcile_with_store_group: true# ─── scope / performance ─────────────────────────────────────────────────────# page_size: Plinth page size for paginated pulls. HARD CAP 100: the API rejects# larger values (validated 2026-07-22). Plinth also caps at 10 req / 10 sec and# throttles rather than fails, so pulls just slow down.page_size: 100# min_orders_to_qualify: ignore customers with fewer than this many counted orders# (0 = keep everyone). Set with Ines: one qualifying order is enough.min_orders_to_qualify: 1# ─── run behavior ────────────────────────────────────────────────────────────# autonomy: how far an unattended (scheduled) run may go on its own.# spreadsheet_only -> pull + classify + export xlsx, then STOP# through_vault -> also create/upgrade stubs + refresh XML, hold Bitrix (current)# full -> entire chain incl. Bitrix pushautonomy: through_vault # ENABLED 2026-07-23: auto stubs + XML; Bitrix / enrich / outreach-routing held```## Activation checklist (done 2026-07-23; kept for a rebuild)1. **Seed the baseline** (one time). `--as-of` is the date the roster's totals runthrough ("LTV up until Monday", so that Monday's date). Dry-run first:```python3 07-Automation/weekly_partner_pipeline.py seed-baseline \--sheet <path-to-roster> --as-of <monday> --dry-run```Check the detected column mapping, fix any miss with `--map "field=Exact Header"`,then run again without `--dry-run`.2. **Catch up the gap.** Pull orders from `as_of` to today (day-windowed, offset+dedupeuntil `added=0`) and `ingest` them, so the history is current before the firstscheduled run.3. **Arm it:** set `min_orders_to_qualify` (the last `TBD`, the arming switch).4. **Dry run:** `classify` then `verify`. Eyeball the spreadsheet, especially the`has_baseline=False` rows (customers seen in the delta with no baseline row).5. **Enable the schedule** per [[scheduled-task]].**Program bands (lifetime spend):** Palladium $2.5K to $8K · Platinum $8K to $25K ·Iridium $25K to $75K · Rhodium $75K+. "Once in the program, always in the program":lifetime, never trailing.