File size: 9,054 Bytes
ac25a64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# 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)