# Regression tests for the July 2026 field report (recurrences + new findings): # single-source price registry, AI-routed command types, spoken numbers, service # dedupe. See ADR 0018. Run with: python scripts/test_july_report_fixes.py import os, sys os.environ.setdefault("GEMINI_API_KEY", "fake-key-for-tests") os.environ.setdefault("FIREBASE", "{}") os.environ.setdefault("aai_key", "fake") os.environ.setdefault("whatsapp_token", "fake") os.environ.setdefault("phone_number_id", "0000") from unittest import mock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import firebase_admin from firebase_admin import credentials, firestore with mock.patch.object(credentials, "Certificate", return_value=mock.MagicMock()), \ mock.patch.object(firebase_admin, "initialize_app", return_value=mock.MagicMock()), \ mock.patch.object(firestore, "client", return_value=mock.MagicMock()): import main import utility PASS = FAIL = 0 def check(desc, got, want): global PASS, FAIL ok = got == want PASS += ok; FAIL += (not ok) print(f"PASS {desc}" if ok else f"FAIL {desc}\n got={got!r} want={want!r}") def check_true(desc, got): check(desc, bool(got), True) def check_false(desc, got): check(desc, bool(got), False) # ── Minimal fake Firestore (enough for the price/service paths) ──────────────── class _FDoc: def __init__(self, data): self._d = data @property def exists(self): return self._d is not None def to_dict(self): return dict(self._d) if self._d else None class _FDocRef: def __init__(self, store, key): self.store, self.key = store, key def get(self): return _FDoc(self.store.get(self.key)) def set(self, data, merge=False): cur = dict(self.store.get(self.key) or {}) if merge else {} cur.update(data); self.store[self.key] = cur def update(self, data): self.set(data, merge=True) def delete(self): self.store.pop(self.key, None) class _FListRef: def __init__(self, coll, idx): self.coll, self.idx = coll, idx def update(self, data): self.coll.rows[self.idx].update(data) class _FListDoc(_FDoc): def __init__(self, data, coll, idx): super().__init__(data); self.reference = _FListRef(coll, idx) class _FListColl: def __init__(self, rows): self.rows = rows; self._keep = None def where(self, f, op, v): sub = _FListColl(self.rows) sub._keep = [i for i, r in enumerate(self.rows) if r.get(f) == v] return sub def get(self): keep = self._keep if self._keep is not None else range(len(self.rows)) return [_FListDoc(self.rows[i], self, i) for i in keep] def limit(self, n): return self def order_by(self, *a, **k): return self class _FKvColl: def __init__(self, store): self.store = store def document(self, key): return _FDocRef(self.store, key) def get(self): return [_FDoc(v) for v in self.store.values()] def limit(self, n): return self class _FUser: def __init__(self, colls): self.colls = colls def collection(self, name): return self.colls[name] def get(self): return _FDoc({"currency": "USD"}) class _FDb: def __init__(self, colls): self.user = _FUser(colls) def collection(self, name): user = self.user class _U: def document(self, _): return user return _U() def make_db(batches=None, item_prices=None, services=None, overrides=None): return _FDb({ "stock_batches": _FListColl(batches or []), "item_prices": _FKvColl(item_prices if item_prices is not None else {}), "services": _FKvColl(services if services is not None else {}), "price_overrides": _FListColl(overrides or []), "transactions": _FListColl([]), }) print("== July report §3: registry price beats stale batch price ==") ip = {} fdb = make_db( batches=[{"name": "dovi", "quantity_remaining": 0, "price_each": 2.56, "stocked_at": "2026-05-01"}, {"name": "dovi", "quantity_remaining": 270, "price_each": 2.56, "stocked_at": "2026-06-01"}], item_prices=ip) utility.set_item_price(fdb, "m", "Dovi", 4.00, currency="USD", source="test") check(" registry write", ip["dovi"]["price"], 4.0) check(" new price ends promo", ip["dovi"]["on_sale"], False) check(" lookup uses registry", utility._lookup_item_price(fdb, "m", "dovi"), 4.0) print("== July §3: sale resolves the UPDATED price (Chido atenga 50 Dovi) ==") txns = [{"intent": "create", "transaction_type": "sale", "details": {"items": [{"item": "dovi", "quantity": 50, "unit": "each"}], "customer": "Chido"}}] utility.resolve_sale_prices(fdb, "m", txns, raw_text="Chido atenga 50 Dovi") check(" sale ppu = 4.00", txns[0]["details"]["items"][0].get("price_per_unit"), 4.0) check(" sale total = 200", txns[0]["details"].get("amount"), 200.0) print("== July §3 step 4: 'move' discounts from the NEW price ==") msg = utility.mark_item_on_sale(fdb, "m", "dovi") check_true(" 20% off 4.00 = 3.20", "3.20" in msg) check(" registry promo", ip["dovi"]["sale_price"], 3.2) check_true(" registry on_sale", ip["dovi"]["on_sale"]) check(" promo lookup", utility._lookup_promo_price(fdb, "m", "dovi"), 3.2) txns2 = [{"intent": "create", "transaction_type": "sale", "details": {"items": [{"item": "dovi", "quantity": 10, "unit": "each"}]}}] utility.resolve_sale_prices(fdb, "m", txns2, raw_text="sold 10 dovi") check(" sale uses promo", txns2[0]["details"]["items"][0].get("price_per_unit"), 3.2) print("== _sorted_batches: active + newest first ==") sb = utility._sorted_batches([ {"quantity_remaining": 0, "stocked_at": "2026-06-30", "price_each": 9}, {"quantity_remaining": 5, "stocked_at": "2026-05-01", "price_each": 1}, {"quantity_remaining": 7, "stocked_at": "2026-06-01", "price_each": 2}]) check(" first is active+newest", sb[0]["price_each"], 2) check(" depleted last", sb[-1]["price_each"], 9) print("== July §8: service dedupe (no re-onboarding) ==") svc = {"knotless_short": {"name": "knotless short", "price": 25.0}} sdb = make_db(services=svc) r = utility._register_service(sdb, "m", {"service": "Knotless Short", "currency": "USD"}) check_true(" states existing price", "already one of your services at USD25.00" in r[1]) check_false(" no onboarding restart", "What do you usually charge" in r[1]) r2 = utility._register_service(sdb, "m", {"service": "Knotless Short", "amount": 30, "currency": "USD"}) check_true(" price update path", "Updated" in r2[1] and "30.00" in r2[1]) check(" service price updated", svc["knotless_short"]["price"], 30.0) print("== July §2/§8: price reply hits a known service ==") prices_msg = main._apply_price_replies(sdb, "m", {"knotless short": 28.0}) check_true(" service price reply", "(service)" in prices_msg and "28.00" in prices_msg) check(" stored", svc["knotless_short"]["price"], 28.0) print("== July §7: spoken numbers ==") nsn = utility.normalise_spoken_numbers check(" eight oranges", nsn("I sold eight oranges"), "I sold 8 oranges") check(" eight(100)", nsn("I sold eight(100) bananas"), "I sold 100 bananas") check(" twenty five", nsn("stocked twenty five bread"), "stocked 25 bread") check(" ninety-nine", nsn("ninety-nine oranges"), "99 oranges") check(" two hundred", nsn("sold two hundred tomatoes"), "sold 200 tomatoes") check(" a hundred", nsn("a hundred eggs"), "100 eggs") check(" digits untouched", nsn("sold 40 tomatoes to Chipo"), "sold 40 tomatoes to Chipo") check(" item-name strip", utility.normalise_item_name("eight oranges"), "orange") print("== AI-routed command types via process_intent ==") with mock.patch("firebase_admin.firestore.client", return_value=fdb): out = utility.process_intent([{"intent": "create", "transaction_type": "set_price", "details": {"items": [{"item": "dovi", "price_per_unit": 5.0}], "currency": "USD"}}], "m") check_true(" set_price saves", "Prices saved" in str(out) and "5.00" in str(out)) check(" registry updated", ip["dovi"]["price"], 5.0) check(" set_price ends promo", ip["dovi"]["on_sale"], False) out2 = utility.process_intent([{"intent": "create", "transaction_type": "mark_on_sale", "details": {"item": "dovi"}}], "m") check_true(" mark_on_sale runs", "on sale" in str(out2) and "4.00" in str(out2)) check(" promo from new base 5.00", ip["dovi"]["sale_price"], 4.0) print("== debt_query type fills the query ==") with mock.patch.object(utility, "_read_transactions", side_effect=lambda db, m, det: det.get("query")), \ mock.patch("firebase_admin.firestore.client", return_value=fdb): out = utility.process_intent([{"intent": "read", "transaction_type": "debt_query", "details": {"direction": "i_owe"}}], "m") check(" debt_query mapped", out, "who do i owe") print(f"\n{'='*40}\nTOTAL: {PASS} passed, {FAIL} failed") sys.exit(1 if FAIL else 0)