generate_sample.py
Language Python37.9 KB679 lines
#!/usr/bin/env python3"""generate_sample.py: build the invented client book this vault ships with.Meridian Display Co. is fictional, and so is every client in this vault. Thisscript invents them: 381 Plinth accounts on the roster (plus one new customerwho first orders during the sample weeks), two weeks of Plinth `orders.fetch`page dumps, and two weekly Bitrix deal enumerations with their stage histories.It is seeded, so every run produces the same world.`run_sample.py` then replays two Monday runs of the weekly pipeline againstthese inputs, and everything under 03-Clients/, 05-Outputs/partner-runs/ and07-Automation/data/ is the pipeline's own output, not hand-written.The world is built to exercise the parts of the driver that matter:* a Plinth day that truncates silently, paged with overlapping offsets, sothe ingest convergence loop has to keep going until a page adds nothing;* cancelled (27) and refunded (28) orders that must not count;* clients who cross a tier band mid-week, and a brand-new customer whosefirst order qualifies them (has_baseline=False);* an approved merge (one person, two Plinth accounts);* a directly-partnered account whose mailboxes are Rhodium by override andnever cold-emailed, plus a staff mailbox on the company domain;* an identity twin (same email, two accounts) where only one twin carriesthe Not Interested closure;* a shared company mailbox used by two different people;* a role-named account and a messy shipping-label name;* retired Bitrix stages, same-minute workflow re-entries, and a cart thatmoved into production funnels after converting.Usage:python3 07-Automation/sample/generate_sample.py # writes sample/inputs/python3 07-Automation/sample/generate_sample.py --out DIRNeeds openpyxl (the roster workbook is an .xlsx, like the real one)."""from __future__ import annotationsimport argparseimport datetime as dtimport jsonimport randomfrom pathlib import PathSEED = 20260921HERE = Path(__file__).resolve().parent# Two Monday runs. The roster workbook's totals run through BASELINE_AS_OF.BASELINE_AS_OF = dt.date(2026, 9, 7)RUN_DATES = [dt.date(2026, 9, 14), dt.date(2026, 9, 21)]PULL_WINDOW_DAYS = 8 # matches partner-thresholds.md (one day of overlap)TRUNCATE_AT = 50 # Plinth's silent cap on this sample's busy dayBUSY_DAY = dt.date(2026, 9, 17)# Id ranges. Everything is invented; the ranges only keep the kinds apart.ACCOUNT_BASE = 40000ORDER_BASE = 61000DEAL_BASE = 400100CONTACT_BASE = 600000TEST_DEALS = ["400017", "400023"] # must match TEST_DEALS in partner_pipeline/stages.pyBANDS = [("Palladium Partner", 2500, 8000), ("Platinum Partner", 8000, 25000),("Iridium Partner", 25000, 75000), ("Rhodium Partner", 75000, 160000)]FIRST = """Ada Aiden Alma Amara Anika Ansel Arlo Isla Audra Avery Beatrix Bennett BramBriony Callum Calla Cassius Celia Cyrus Dalia Dario Delphine Denver Edie Elio ElowenEmeric Esme Ezra Fallon Felix Fenna Florian Gemma Greer Hale Harriet Idris IlseImogen Isaak Jonah Juniper Kai Kasimir Keira Lark Leander Lena Linus Lucia MabelMagnus Maren Matteo Mira Nadia Nell Niko Noor Oona Orrin Otto Paloma Perrin PetraQuill Remy Rhea Rowan Saoirse Selma Silas Soren Sunniva Tamsin Tobiah Uma VesperWendell Wren Xavi Yara Yusuf Zadie Zev""".split()LAST = """Abernethy Achterberg Alcott Ambrose Ashdown Barrowclough Beaumont BirchallBlackwood Bramley Castellanos Cavendish Clemons Coldwell Crane Dalgleish DelacroixDunmore Eastwood Ellery Fairbairn Farrow Fenwick Gallardo Garroway Greaves HadleyHalloran Hartigan Hawthorne Holloway Ingleby Iverson Jardine Kettering KingsleyLachance Larkspur Lindgren Lockhart Maddox Marchetti Merriweather Montague NorthcottOakes Ormsby Pemberton Penhaligon Quimby Radcliffe Ravensworth Redfern RushworthSaltonstall Sandoval Seaton Sheffield Stanhope Sutcliffe Tamsworth Thornbury TrevelyanUnderhill Vance Vandermeer Wainwright Westbrook Whitcombe Winterbourne YardleyZamora""".split()CO_A = """Amber Anchor Arbor Aster Basin Beacon Bluff Cedar Cinder Cobalt CopperlineCrescent Driftwood Ember Fernhill Fieldstone Foxglove Granite Harbor Heron HollowJuniper Keystone Kestrel Lantern Larch Meadowlark Millrace Northwind Oakmoss OrchardPinecrest Prairie Quarry Redwing Ridgeback Saltmarsh Sandpiper Silverleaf SkylarkStonebridge Sundial Tidewater Timberline Valley Willowbank""".split()CO_B = ["Events", "Outfitters", "Athletics", "Realty", "Brewing Co.", "Clinic Group","Foundation", "Expo Services", "Coffee Roasters", "Academy", "Dental","Motorsports", "Credit Union", "Marketing", "Builders", "Film Festival","Youth Soccer", "Wellness", "Distillery", "Church", "Robotics", "Farms"]INDUSTRIES = ["events services", "sports & recreation", "real estate", "food & beverages","health care", "nonprofit", "education", "automotive", "financial services","marketing & advertising", "construction", "religious institutions","entertainment", "retail", "manufacturing"]TITLES = ["Marketing Manager", "Events Coordinator", "Owner", "Operations Director","Brand Manager", "Office Manager", "Community Relations Lead", "Executive Director","Trade Show Manager", "Facilities Coordinator", ""]OWNERS = ["Matt Guerra", "Hazel Okafor", "Nora Lindqvist"]def iso(d: dt.date) -> str:return d.isoformat()class World:def __init__(self, seed: int = SEED):self.r = random.Random(seed)self.accounts: list[dict] = []self.orders: list[dict] = [] # post-baseline orders onlyself.next_account = ACCOUNT_BASEself.next_order = ORDER_BASEself.used_names: set[tuple[str, str]] = set()self.used_companies: set[str] = set()self.overrides: list[str] = []self.merges: list[dict] = []self.suppression = {"names": [], "emails": [], "domains": []}self.cases: dict[str, dict] = {}# ── people and companies ──────────────────────────────────────────────def person(self) -> tuple[str, str]:while True:n = (self.r.choice(FIRST), self.r.choice(LAST))if n not in self.used_names:self.used_names.add(n)return ndef company(self) -> tuple[str, str]:while True:a, b = self.r.choice(CO_A), self.r.choice(CO_B)name = f"{a} {b}"if name not in self.used_companies:self.used_companies.add(name)slug = (a + b.split()[0]).lower().replace(".", "")return name, f"{slug}.example"def phone(self) -> str:return f"({self.r.randint(201, 989)}) 555-01{self.r.randint(0, 99):02d}"def add_account(self, *, ltv: float, last_order: dt.date, orders: int | None = None,first=None, last=None, company=None, domain=None, email=None,group: str = "", extra_email: str = "", title=None, industry=None) -> dict:if first is None:first, last = self.person()if company is None:company, domain = self.company()if email is None:email = f"{first.lower()}.{last.lower()}@{domain}".replace(" ", "")n = orders if orders is not None else max(1, int(ltv // self.r.uniform(900, 2600)) + 1)span = self.r.randint(120, 1600)if n == 0:last_order = Nonefirst_order = (last_order - dt.timedelta(days=span if n > 1 else 0)) if last_order else Nonerepeat = 0.0 if n == 1 else round(ltv * self.r.uniform(0.35, 0.8), 2)acc = {"account_ref": self.next_account, "first_name": first, "last_name": last,"email": email.lower(), "extra_email": extra_email, "company": company,"phone": self.phone(), "title": self.r.choice(TITLES) if title is None else title,"website": f"https://www.{domain}" if domain else "","industry": industry or self.r.choice(INDUSTRIES),"registered": (first_order or BASELINE_AS_OF) - dt.timedelta(days=self.r.randint(0, 400)),"group": group,"baseline": {"ltv": round(ltv, 2), "orders": n, "repeat": repeat,"first": first_order, "last": last_order},}self.next_account += 1self.accounts.append(acc)return accdef add_order(self, acc: dict, day: dt.date, amount: float, status: int = 5) -> dict:ship = round(amount * 0.08, 2)o = {"order_ref": self.next_order, "account": acc, "day": day,"grand_total": round(amount + ship, 2), "subtotal": round(amount, 2),"shipping_total": ship, "status_code": status,"hour": self.r.randint(8, 18), "minute": self.r.randint(0, 59)}self.next_order += 1self.orders.append(o)return o# ── the book ──────────────────────────────────────────────────────────def build(self):r = self.rrun = RUN_DATES[-1]dormant_cut = run - dt.timedelta(days=365)def last_date(dormant: bool) -> dt.date:if dormant:return dormant_cut - dt.timedelta(days=r.randint(20, 900))return BASELINE_AS_OF - dt.timedelta(days=r.randint(3, 330))# Partner book by band, then Retail accounts under the lowest band.for count, bi in [(80, 0), (26, 1), (7, 2), (3, 3)]:tier, lo, hi = BANDS[bi]for _ in range(count):ltv = round(r.uniform(lo, hi if bi < 3 else lo * 1.9), 2)dormant = r.random() < 0.55grp = tier if r.random() < 0.25 else ""if grp and bi > 0 and r.random() < 0.3:grp = BANDS[bi - 1][0] # stale store group: a real mismatchself.add_account(ltv=ltv, last_order=last_date(dormant), group=grp)for _ in range(250):ltv = round(r.uniform(60, 2450), 2)self.add_account(ltv=ltv, last_order=last_date(r.random() < 0.6),group="Retail" if r.random() < 0.3 else "")# Special cases, each named in self.cases for run_sample.py and the docs.# 1. Directly-partnered account: Rhodium by override, never cold-emailed.hr = []for first, last, ltv, n in [("Maren", "Achterberg", 4210.50, 3), ("Soren", "Blackwood", 880.00, 1),("Lena", "Coldwell", 0.0, 0), ("Callum", "Dunmore", 12650.25, 6),("Rhea", "Farrow", 1540.00, 2), ("Otto", "Garroway", 610.00, 1)]:self.used_names.add((first, last))a = self.add_account(ltv=ltv, orders=n, last_order=last_date(False), first=first,last=last, company="Harlow Ridge Homes", domain="harlowridge.example",industry="real estate", group="")hr.append(a)self.overrides = [a["email"] for a in hr[:4]] # per-email, not per-domainself.suppression["domains"] = ["harlowridge.example", "meridiandisplay.example"]self.cases["partnered_account"] = {"overridden": self.overrides,"not_overridden": [a["email"] for a in hr[4:]]}# 2. Staff mailbox on the company domain with real spend (event orders).self.used_names.add(("Theo", "Bassett"))staff = self.add_account(ltv=31480.00, orders=14, last_order=dormant_cut - dt.timedelta(days=40),first="Theo", last="Bassett", company="Meridian Display Co.",domain="meridiandisplay.example", industry="printing", title="Operations")self.cases["staff_mailbox"] = staff["email"]# 3. Approved merge: one person, two Plinth accounts after a rebrand.self.used_names.add(("Imogen", "Radcliffe"))surv = self.add_account(ltv=6120.40, orders=5, last_order=last_date(False), first="Imogen",last="Radcliffe", company="Tidewater Foundation", domain="tidewaterfdn.example")absd = self.add_account(ltv=2310.00, orders=2, last_order=dt.date(2024, 11, 2), first="Imogen",last="Radcliffe", company="Tidewater Trust", domain="tidewatertrust.example")self.merges = [{"survivor": str(surv["account_ref"]), "absorbed": [str(absd["account_ref"])],"unit": "person", "name": "Imogen Radcliffe","reason": "Same person across the Tidewater Trust -> Tidewater Foundation rename: "f"{surv['email']} (live) + {absd['email']} (old domain). Combined ""lifetime crosses into Platinum.","approved_by": "Matt", "approved": "2026-08-06"}]self.cases["merge"] = {"survivor": surv["account_ref"], "absorbed": absd["account_ref"]}# 4. Identity twin: same person re-registered under a married name, same email.self.used_names.update({("Delphine", "Hartigan"), ("Delphine", "Hartigan-Vance")})tw_a = self.add_account(ltv=5340.00, orders=4, last_order=last_date(False), first="Delphine",last="Hartigan", company="Lantern Film Festival", domain="lanternfilm.example")tw_b = self.add_account(ltv=3120.00, orders=2, last_order=last_date(False), first="Delphine",last="Hartigan-Vance", company="Lantern Film Festival",domain="lanternfilm.example", email=tw_a["email"])self.cases["twin"] = {"with_closure": tw_a["account_ref"], "clean_twin": tw_b["account_ref"],"email": tw_a["email"]}# 5. Shared company mailbox, two different people.for first, last in [("Nell", "Ormsby"), ("Idris", "Seaton")]:self.used_names.add((first, last))self.add_account(ltv=r.uniform(2800, 7000), last_order=last_date(False), first=first,last=last, company="Lakeshore Arts Council", domain="lakeshorearts.example",email="events@lakeshorearts.example")self.cases["shared_mailbox"] = "events@lakeshorearts.example"# 6. Role-named account (dormant, so Winback holds it for review).role = self.add_account(ltv=9870.00, orders=7, last_order=dormant_cut - dt.timedelta(days=200),first="Purchasing", last="Dept", company="Northwind Academy",domain="northwindacademy.example", email="purchasing@northwindacademy.example",title="")self.cases["role_named"] = role["email"]# 7. Messy shipping-label name (the stub gets flagged on creation).messy = self.add_account(ltv=3360.00, orders=2, last_order=last_date(False), first="Booth 214",last="c/o Riverside Expo", company="Riverside Expo Center",domain="riversideexpo.example", email="booth214@riversideexpo.example",title="")self.cases["messy_name"] = messy["email"]# ── post-baseline orders (the weekly pulls) ──partners = [a for a in self.accounts if a["baseline"]["ltv"] >= 2500]# Small accounts well under the Palladium line, so routine orders don't# push them over it (the one planned crossing is added below).retail = [a for a in self.accounts if a["baseline"]["ltv"] < 1600 and a["company"] != "Harlow Ridge Homes"]# Routine orders go to clients already buying this year, and stay clear of# a band edge, so the week's tier movement is only the planned crossings.tops = [lo for _, lo, _ in BANDS]buying = [a for a in partners if a["baseline"]["last"] >= dormant_cutand a["email"] != self.cases["role_named"]]added: dict[int, float] = {}def headroom(a):now = a["baseline"]["ltv"] + added.get(a["account_ref"], 0.0)return min([top - now for top in tops if top > now] or [1e9])def pick():while True:a, amount = ((r.choice(buying), round(r.uniform(150, 1400), 2)) if r.random() < 0.7else (r.choice(retail), round(r.uniform(60, 350), 2)))if amount * 1.08 < headroom(a) - 50:added[a["account_ref"]] = added.get(a["account_ref"], 0.0) + amount * 1.08return a, amountdays = [BASELINE_AS_OF + dt.timedelta(days=i) for i in range(0, 15)]for d in days:if d.weekday() >= 5:continuefor _ in range(r.randint(3, 7)):acc, amount = pick()self.add_order(acc, d, amount)# The busy day: enough orders that Plinth truncates the response.for _ in range(64):acc, amount = pick()self.add_order(acc, BUSY_DAY, amount)# Cancelled and refunded orders that must not count toward lifetime value.self.add_order(r.choice(partners), dt.date(2026, 9, 10), 2200.00, status=27)self.add_order(r.choice(partners), dt.date(2026, 9, 16), 1450.00, status=28)# Tier crossings mid-week.near = [a for a in self.accounts if 2000 < a["baseline"]["ltv"] < 2450and a["company"] != "Harlow Ridge Homes"]up_retail = max(near, key=lambda a: a["baseline"]["ltv"])self.add_order(up_retail, dt.date(2026, 9, 16), 640.00)pal = [a for a in partners if 7300 < a["baseline"]["ltv"] < 8000]up_pal = pal[0] if pal else partners[0]self.add_order(up_pal, dt.date(2026, 9, 18), 1120.00)# A brand-new customer whose first order qualifies them.self.used_names.add(("Wendell", "Pemberton"))newbie = {"account_ref": self.next_account, "first_name": "Wendell", "last_name": "Pemberton","email": "wendell@pembertonrobotics.example", "extra_email": "","company": "Pemberton Robotics", "phone": self.phone(), "title": "Founder","website": "https://www.pembertonrobotics.example", "industry": "manufacturing","registered": dt.date(2026, 9, 11), "group": "", "baseline": None}self.next_account += 1self.accounts.append(newbie)self.add_order(newbie, dt.date(2026, 9, 19), 3875.00)self.cases["crossings"] = {"retail_to_palladium": up_retail["account_ref"],"palladium_to_platinum": up_pal["account_ref"],"new_customer": newbie["account_ref"]}return self# ── writers ──────────────────────────────────────────────────────────────────def plinth_order(o: dict) -> dict:a = o["account"]ts = f"{iso(o['day'])} {o['hour']:02d}:{o['minute']:02d}:00"# Plinth sends some numbers as strings and some as numbers; so does the sample.gt = o["grand_total"] if o["order_ref"] % 3 else f"{o['grand_total']:.2f}"return {"order_ref": o["order_ref"], "account_ref": a["account_ref"], "email": a["email"],"first_name": a["first_name"], "last_name": a["last_name"], "company_name": a["company"],"phone": a["phone"], "customer_group": a["group"], "grand_total": gt,"subtotal": o["subtotal"], "shipping_total": o["shipping_total"], "tax_total": 0,"status_code": o["status_code"],"status_label": {27: "Order Cancelled", 28: "Order Refunded"}.get(o["status_code"], "Completed"),"closed_on": ts, "shop": 1, "lines": {"1": {"line_ref": o["order_ref"] * 10 + 1}},"shipments": [],}def write_pages(w: World, out: Path):"""One file per (day, offset) API response, as Claude would save them.Days with more orders than TRUNCATE_AT come back cut short, and the nextoffset overlaps the previous page instead of continuing after it."""pdir = out / "plinth"pdir.mkdir(parents=True, exist_ok=True)by_day: dict[dt.date, list] = {}for o in w.orders:by_day.setdefault(o["day"], []).append(o)first = RUN_DATES[0] - dt.timedelta(days=PULL_WINDOW_DAYS - 1)d = firstwhile d <= RUN_DATES[-1]:rows = sorted(by_day.get(d, []), key=lambda o: o["order_ref"])if len(rows) <= TRUNCATE_AT:pages = [rows]else:pages = [rows[:TRUNCATE_AT], rows[20:20 + TRUNCATE_AT], rows[30:]]for i, page in enumerate(pages):body = ({"order_map": {f"ord_{o['order_ref']}": plinth_order(o) for o in page}}if page else {"notice": "No match"})(pdir / f"{iso(d)}_p{i}.json").write_text(json.dumps(body, indent=1), encoding="utf-8")d += dt.timedelta(days=1)def write_roster(w: World, out: Path):"""The partner roster workbook, same tab and column contract as the real one."""from openpyxl import Workbookfrom openpyxl.styles import Font, PatternFillwb = Workbook()work = wb.activework.title = "2026 Partner Working List"part = wb.create_sheet("Complete Partner Clients List")allacc = wb.create_sheet("All Accounts")date_us = BASELINE_AS_OF.strftime("%m/%d/%y")ph = ["Id", "First Name", "Last Name", "Email", "Additional Email", "Company", "Phone","Job Title", "Website", "User Group", "Correct Group", "Updated Group Plinth?","Date Updated Plinth", f"Plinth LTV {date_us}", "Total Spent (Orders)", "Last Order","Industry", "Registered", "# Orders", "Repeat Revenue (Plinth)", "First Order","Pipeline(s)", "Comments", "Contacted (Pipeline Outreach)", "Most Recent Contact Date","First Outreach Date", "Times Contacted (Pipeline Touches)", "Total Pipeline Entries","Current Pipeline Stage(s)", "Pipeline Owner/Rep"]wh = ["Completed"] + ph[:9] + ["User Type"] + ph[9:13] + ["Social Media", "Total Spent (Orders)","Industry", "Registered", "# Orders", "Repeat Revenue (Plinth)", "First Order", "Last Order","Pipeline(s)", "Comments", "Contacted (Pipeline Outreach)", "Most Recent Contact Date","First Outreach Date", "Times Contacted (Pipeline Touches)", "Total Pipeline Entries","Current Pipeline Stage(s)", "Pipeline Owner/Rep", "LTV (Plinth)"]head = Font(name="Arial", bold=True, color="FFFFFF")fill = PatternFill("solid", fgColor="1F3864")for ws, hdr in ((part, ph), (work, wh), (allacc, ["ID", "First Name", "Last Name", "Email", "LTV", "RR"])):ws.append(hdr)for c in ws[1]:c.font, c.fill = head, filldef tier_of(ltv):for t, lo, hi in BANDS:if ltv >= lo and (ltv < hi or t == "Rhodium Partner"):return treturn ""money, day = "$#,##0.00", "mm/dd/yyyy"body = Font(name="Arial", size=10)for a in w.accounts:b = a["baseline"]if b is None:continueallacc.append([a["account_ref"], a["first_name"], a["last_name"], a["email"], b["ltv"], b["repeat"]])tier = tier_of(b["ltv"])if a["email"] in w.overrides:tier = "Rhodium Partner"if not tier:continuepart.append([a["account_ref"], a["first_name"], a["last_name"], a["email"], a["extra_email"],a["company"], a["phone"], a["title"], a["website"], a["group"], tier, "", None,b["ltv"], b["ltv"], b["last"], a["industry"], a["registered"], b["orders"],b["repeat"], b["first"], "", "", "", None, None, None, None, "", ""])for ws in (part, allacc):for row in ws.iter_rows(min_row=2):for c in row:c.font = bodyh = ws.cell(row=1, column=c.column).value or ""if h.startswith("Plinth LTV") or h in ("Total Spent (Orders)", "Repeat Revenue (Plinth)", "LTV", "RR"):c.number_format = moneyif h in ("Last Order", "First Order", "Registered", "Date Updated Plinth"):c.number_format = day# The curated working tab: a hand-kept subset with human columns filled in.r = random.Random(SEED + 1)notes = ["Prefers a call before quoting.", "Orders ahead of the spring show circuit.","Asked about storage for booth kits.", "New marketing lead since spring.","Uses our design studio for every order.", ""]for a in [x for x in w.accounts if x["baseline"] and x["baseline"]["ltv"] >= 8000][:24]:b = a["baseline"]work.append(["", a["account_ref"], a["first_name"], a["last_name"], a["email"], "",a["company"], a["phone"], a["title"], a["website"], "Customer", a["group"],tier_of(b["ltv"]), "", None, "", b["ltv"], a["industry"], a["registered"],b["orders"], b["repeat"], b["first"], b["last"], "", r.choice(notes),"", None, None, None, None, "", r.choice(OWNERS), b["ltv"]])out.mkdir(parents=True, exist_ok=True)path = out / f"Partner-Clients-Roster_{iso(BASELINE_AS_OF)}.xlsx"wb.save(path)return path# ── Bitrix: two weekly enumerations and the stage histories behind them ─────STAGES = {"24": {"research": "UC_2YNXWE", "tier_email": "UC_6H9C8U", "artwork": "UC_5DTN59","mockup": "PREPAYMENT_INVOIC", "gift_sent": "FINAL_INVOICE", "won": "WON","lose": "LOSE", "retired_send": "PREPARATION", "retired_2nd": "UC_CEAGAV","retired_bulk": "UC_N4JCMW"},"26": {"client": "UC_JUUNES", "activated": "UC_L2JMHY", "first": "PREPARATION","noreply1": "UC_K9M6LT", "follow": "PREPAYMENT_INVOIC", "precall": "FINAL_INVOICE","won": "WON", "lose": "LOSE", "survey": "UC_PMLDJW"},"36": {"active": "NEW", "dormant": "UC_LXCMAV", "first": "PREPAYMENT_INVOIC","second": "EXECUTING", "noreply": "UC_7XQR9H", "won": "WON", "notint": "1"},"16": {"new": "NEW", "first": "PREPARATION", "follow": "PREPAYMENT_INVOIC","noreply2": "EXECUTING", "won": "WON", "lose": "LOSE"},}def build_deals(w: World):"""Returns (week1_deals, week2_deals, histories, contacts). Deal JSON shape iswhat the SOP's enumeration step saves: deal_id, cat, stage, stage_date, title,owner, created, value. Histories and contacts are what get_stage_history andget_record would return, attached by run_sample.py only for the deals thepull list asks for."""r = random.Random(SEED + 2)run = RUN_DATES[-1]cut = run - dt.timedelta(days=365)by_ref = {a["account_ref"]: a for a in w.accounts}partners = [a for a in w.accounts if a["baseline"] and a["baseline"]["ltv"] >= 2500and a["company"] not in ("Harlow Ridge Homes", "Meridian Display Co.")and a["email"] not in (w.cases["shared_mailbox"], w.cases["role_named"], w.cases["messy_name"])]dormant = [a for a in partners if a["baseline"]["last"] < cut]active = [a for a in partners if a["baseline"]["last"] >= cut]twin_a = by_ref[w.cases["twin"]["with_closure"]]twin_b = by_ref[w.cases["twin"]["clean_twin"]]dormant = [a for a in dormant if a not in (twin_a, twin_b)]active = [a for a in active if a not in (twin_a, twin_b)]r.shuffle(dormant)r.shuffle(active)deals, hist, contacts = [], {}, {}next_id = [DEAL_BASE]def at(d: dt.date, h=None, m=None):return f"{iso(d)} {h if h is not None else r.randint(8, 17):02d}:{m if m is not None else r.randint(0, 59):02d}"def deal(cat, a, path, created, *, title=None, value="", owner=None, deal_id=None):"""path: list of (stage_key, date). The deal sits in the last stage."""did = deal_id or str(next_id[0])if not deal_id:next_id[0] += r.randint(3, 40)st = STAGES[cat]h = [{"stage": f"C{cat}:{st[k] if k in st else k}", "at": at(d)} for k, d in path]last_key, last_day = path[-1]stage = f"C{cat}:{st[last_key] if last_key in st else last_key}"deals.append({"deal_id": did, "cat": cat, "stage": stage, "stage_date": at(last_day),"title": title or f"{a['first_name']} {a['last_name']}","owner": owner or r.choice(OWNERS), "created": at(created, 9, 0), "value": value})hist[did] = hif a is not None:contacts[did] = {"name": f"{a['first_name']} {a['last_name']}", "email": a["email"],"phone": a["phone"]}return didd = dt.date# Winback (26): most sit untouched in the Client bucket; Nora's stage moves are the sends.for a in dormant[:12]:deal("26", a, [("client", d(2026, 8, 6))], d(2026, 8, 6), owner="Nora Lindqvist")for a in dormant[12:15]:deal("26", a, [("client", d(2026, 8, 6)), ("activated", d(2026, 8, 11))], d(2026, 8, 6),owner="Nora Lindqvist")for a in dormant[15:18]:deal("26", a, [("client", d(2026, 8, 6)), ("activated", d(2026, 8, 11)), ("first", d(2026, 8, 13)),("noreply1", d(2026, 8, 20)), ("follow", d(2026, 8, 24))], d(2026, 8, 6),owner="Nora Lindqvist")a = dormant[18]did = deal("26", a, [("client", d(2026, 8, 6)), ("activated", d(2026, 8, 11)), ("first", d(2026, 8, 12)),("follow", d(2026, 8, 19)), ("precall", d(2026, 8, 27))], d(2026, 8, 6),owner="Nora Lindqvist")# Same-minute workflow re-entries: one send, not three.hist[did] += [{"stage": "C26:PREPAYMENT_INVOIC", "at": "2026-08-19 10:14"},{"stage": "C26:PREPAYMENT_INVOIC", "at": "2026-08-19 10:15"}]for a in dormant[19:21]:deal("26", a, [("first", d(2026, 2, 10)), ("survey", d(2026, 2, 12)), ("won", d(2026, 3, 30))],d(2026, 2, 9), owner="Hazel Okafor")deal("26", dormant[21], [("first", d(2026, 2, 10)), ("follow", d(2026, 2, 17)), ("lose", d(2026, 4, 1))],d(2026, 2, 9), owner="Hazel Okafor")# Winback already done this dormancy spell: excluded from routing, not re-touched.deal("26", dormant[22], [("client", d(2026, 6, 2)), ("first", d(2026, 6, 4)), ("lose", d(2026, 7, 15))],d(2026, 6, 2), owner="Nora Lindqvist")# Partner Tier Upgrade (24): first touches and tier crossings on active clients.for a in active[:4]:deal("24", a, [("research", d(2026, 7, 20)), ("tier_email", d(2026, 7, 21))], d(2026, 7, 20),owner="Hazel Okafor")deal("24", active[4], [("tier_email", d(2026, 7, 21)), ("artwork", d(2026, 7, 29))], d(2026, 7, 20))deal("24", active[5], [("tier_email", d(2026, 6, 24)), ("mockup", d(2026, 7, 2)),("gift_sent", d(2026, 7, 14))], d(2026, 6, 23))for a in active[6:8]:# Older deals that moved through stages since deleted from the funnel.deal("24", a, [("retired_send", d(2026, 3, 3)), ("retired_2nd", d(2026, 3, 17)),("retired_bulk", d(2026, 4, 1)), ("won", d(2026, 4, 20))], d(2026, 3, 2),owner="Hazel Okafor")deal("24", active[8], [("research", d(2026, 8, 12))], d(2026, 8, 12))# The twin with the Not Interested closure (an opt-out for the whole identity).deal("24", twin_a, [("tier_email", d(2026, 6, 24)), ("lose", d(2026, 7, 8))], d(2026, 6, 23),owner="Hazel Okafor")# Quarterly Check-In (36).for a in active[9:13]:deal("36", a, [("active", d(2026, 8, 6))], d(2026, 8, 6), owner="Matt Guerra")# Dual-active: an open tier-upgrade deal parks the check-in in Dormant.for a in active[:2]:deal("36", a, [("active", d(2026, 5, 12)), ("dormant", d(2026, 8, 12))], d(2026, 5, 12),owner="Matt Guerra")for a in active[13:15]:deal("36", a, [("active", d(2026, 7, 27)), ("first", d(2026, 7, 29))], d(2026, 7, 27),owner="Matt Guerra")deal("36", active[15], [("first", d(2026, 7, 29)), ("noreply", d(2026, 8, 5)), ("second", d(2026, 8, 11))],d(2026, 7, 27), owner="Matt Guerra")deal("36", active[16], [("first", d(2026, 4, 14)), ("won", d(2026, 5, 2))], d(2026, 4, 13),owner="Hazel Okafor")deal("36", active[17], [("first", d(2026, 7, 29)), ("notint", d(2026, 8, 3))], d(2026, 7, 27),owner="Matt Guerra")deal("36", active[18], [("first", d(2026, 4, 14)), ("second", d(2026, 4, 28)), ("won", d(2026, 5, 20))],d(2026, 4, 13), owner="Matt Guerra")# Abandoned Carts (16): titles are cart numbers, so contacts come from get_record.retail = [a for a in w.accounts if a["baseline"] and a["baseline"]["ltv"] < 2500and a["company"] != "Harlow Ridge Homes"]for i, a in enumerate(retail[:5]):path = [("new", d(2026, 9, 2 + i))]if i % 2:path.append(("first", d(2026, 9, 3 + i)))deal("16", a, path, d(2026, 9, 2 + i), title=f"Cart - {70100 + i * 7}",value=round(r.uniform(90, 900), 2), owner="Hazel Okafor")# A guest checkout that is not a Plinth account at all.guest = {"first_name": "Selma", "last_name": "Iverson", "email": "selma.iverson@quarrybrewing.example","phone": "(512) 555-0147"}deal("16", guest, [("new", d(2026, 9, 9))], d(2026, 9, 9), title="Cart - 70177", value=412.50,owner="Hazel Okafor")# A cart that converted and moved on into production funnels (C18/C20 entries# must not count as cart emails even though PREPARATION is a cat-16 send token).conv = retail[6]did = deal("16", conv, [("new", d(2026, 8, 25)), ("first", d(2026, 8, 26)), ("won", d(2026, 8, 28))],d(2026, 8, 25), title="Cart - 70093", value=655.00, owner="Hazel Okafor")hist[did] += [{"stage": "C18:PREPARATION", "at": "2026-08-29 09:12"},{"stage": "C20:PREPARATION", "at": "2026-09-01 11:40"}]for tid in TEST_DEALS:deal("16", None, [("new", d(2026, 8, 20))], d(2026, 8, 20), title=f"TEST cart {tid}",value=1.00, owner="Matt Guerra", deal_id=tid)# Week 1 is the same book one week earlier: deals that moved in week 2 sat one stage back.week2 = [dict(x) for x in deals]week1 = []moved = {}for x in deals:h = sorted(hist[x["deal_id"]], key=lambda e: e["at"])before = [e for e in h if e["at"][:10] <= iso(RUN_DATES[0])]if not before:continue # created in week 2y = dict(x)if before[-1]["stage"] != x["stage"]:y["stage"], y["stage_date"] = before[-1]["stage"], before[-1]["at"]moved[x["deal_id"]] = Trueweek1.append(y)# Two week-2 moves, so the cache has something real to decide about.for x in week2:if x["cat"] == "36" and x["stage"] == "C36:NEW":x["stage"], x["stage_date"] = "C36:PREPAYMENT_INVOIC", "2026-09-16 10:20"hist[x["deal_id"]].append({"stage": "C36:PREPAYMENT_INVOIC", "at": "2026-09-16 10:20"})breakfor x in week2:if x["cat"] == "26" and x["stage"] == "C26:UC_L2JMHY":x["stage"], x["stage_date"] = "C26:PREPARATION", "2026-09-17 14:05"hist[x["deal_id"]].append({"stage": "C26:PREPARATION", "at": "2026-09-17 14:05"})breakreturn week1, week2, hist, contactsdef main():ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])ap.add_argument("--out", default=str(HERE / "inputs"))a = ap.parse_args()out = Path(a.out)w = World().build()write_pages(w, out)roster = write_roster(w, out)week1, week2, hist, contacts = build_deals(w)bdir = out / "bitrix"bdir.mkdir(parents=True, exist_ok=True)for day, deals in zip(RUN_DATES, (week1, week2)):(bdir / f"bitrix-deals_{iso(day)}.json").write_text(json.dumps({"deals": deals}, indent=1), encoding="utf-8")(bdir / "stage-histories.json").write_text(json.dumps(hist, indent=1, sort_keys=True), encoding="utf-8")(bdir / "contacts.json").write_text(json.dumps(contacts, indent=1, sort_keys=True), encoding="utf-8")(out / "merge_registry.json").write_text(json.dumps({"_readme": "Approved Plinth account merges. The weekly pipeline folds each absorbed account ""into its survivor at classify time (lifetime metrics), drops absorbed pl-<id> ""chunks from the XML census, and never re-creates them. Keys are ""order_history.json customer keys (the Plinth account id as a string). Add ""entries ONLY with Matt's explicit approval, one LEDGER entry per addition.","version": 1, "updated": "2026-08-06", "merges": w.merges}, indent=1), encoding="utf-8")(out / "suppression_list.json").write_text(json.dumps({"_readme": "Explicit email opt-outs and never-contact accounts. A client here, or with any ""Not-Interested funnel closure, never routes into any outreach pipeline, ""permanently, until Matt personally reverses it (DECISIONS 2026-08-06). Names ""match on normalized full name; emails on exact lowercase; domains match the ""part after '@', lowercase.","_domains_note": "domains: whole companies that are never cold-outreached because the ""relationship is handled directly (DECISIONS 2026-08-12). ""harlowridge.example = Harlow Ridge Homes, a directly-partnered builder; ""meridiandisplay.example = our own staff.",**w.suppression}, indent=1), encoding="utf-8")(out / "cases.json").write_text(json.dumps({**w.cases, "tier_overrides": w.overrides},indent=1, default=str), encoding="utf-8")n_orders = len(w.orders)print(f"accounts {len(w.accounts)}, post-baseline orders {n_orders}, "f"deals week1 {len(week1)} / week2 {len(week2)} -> {out}")print(f"roster: {roster.name}")if __name__ == "__main__":main()