File size: 10,412 Bytes
3ea2835
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 field bug-report fixes of 2026-06-03 (see ADR 0016 and
# ADR 0017). Run with:  python scripts/test_bug_report_fixes.py
# Mocks Firebase/Gemini so main.py imports cleanly; only pure logic is exercised.
import os, sys, json
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")
os.environ.setdefault("WHATSAPP_PROXY_URL", "")
os.environ.setdefault("VERIFY_TOKEN", "fake")
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

_fake_db = mock.MagicMock()
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=_fake_db):
    import main
import utility

PASS = FAIL = 0
def check(desc, got, want):
    global PASS, FAIL
    ok = got == want
    PASS += ok; FAIL += (not ok)
    print(("PASS" if ok else "FAIL") + f"  {desc}\n      got={got!r} want={want!r}" if not ok else f"PASS  {desc}")

def check_true(desc, got):  check(desc, bool(got), True)
def check_false(desc, got): check(desc, bool(got), False)

print("== Bug 2: price-format equivalence (_parse_price_reply) ==")
check("carrot: USD0.15",       main._parse_price_reply("carrot: USD0.15"),      {"carrot": 0.15})
check("carrot: 0.15 dollars",  main._parse_price_reply("carrot: 0.15 dollars"), {"carrot": 0.15})
check("carrot: 0.15 USD",      main._parse_price_reply("carrot: 0.15 USD"),     {"carrot": 0.15})
check("carrot: 15 cents",      main._parse_price_reply("Carrot: 15 cents"),     {"carrot": 0.15})
check("carrot: 50c",           main._parse_price_reply("carrot: 50c"),          {"carrot": 0.5})
check("peanut butter - $3",    main._parse_price_reply("peanut butter - $3"),   {"peanut butter": 3.0})
check("multiline",             main._parse_price_reply("jam: USD2\nbread 1.20 each"), {"jam": 2.0, "bread": 1.2})
check("skip",                  main._parse_price_reply("skip"), {})
check("CRUD not price",        main._parse_price_reply("update transaction abc123 to USD12"), None)
check("question not price",    main._parse_price_reply("what is on sale?"), None)

print("== Bug 2: bare money reply ==")
check("USD0.50",       main._parse_bare_money_reply("USD0.50"), 0.5)
check("0.50 dollars",  main._parse_bare_money_reply("0.50 dollars"), 0.5)
check("50c",           main._parse_bare_money_reply("50c"), 0.5)
check("0.50 each",     main._parse_bare_money_reply("0.50 each"), 0.5)
check("plain 2",       main._parse_bare_money_reply("2"), 2.0)
check("not money",     main._parse_bare_money_reply("2 jam"), None)
check("not money txt", main._parse_bare_money_reply("hello"), None)

print("== Bug 1: move-item extraction ==")
check("move dovi",           main._extract_move_item("move dovi"), "dovi")
check("put dovi on sale",    main._extract_move_item("put dovi on sale"), "dovi")
check("discount tomatoes",   main._extract_move_item("discount tomatoes"), "tomatoes")
check("question guarded",    main._extract_move_item("what is on sale?"), None)

print("== Bugs 4+5: debt query detection ==")
check_true(" who do I owe money?",  utility.is_debt_query("Who do I owe money?"))
check_true(" who do I owe?",        utility.is_debt_query("Who do I owe?"))
check_true(" who owes me money?",   utility.is_debt_query("Who owes me money?"))
check_true(" show my payables",     utility.is_debt_query("show my payables"))
check_true(" my debtors",           utility.is_debt_query("list my debtors"))
check_false(" I owe John 50 (record, not ask)", utility.is_debt_query("I owe John 50"))
check_false(" Chipo owes me 20 for bread",      utility.is_debt_query("Chipo owes me 20 for bread"))
check_false(" sold 2 jam",                      utility.is_debt_query("sold 2 jam"))

print("== Bugs 4+5: canonical report format ==")
custs = [{"name": "Supplier A", "payable": 50.0, "receivable": 0},
         {"name": "Supplier B", "payable": 75.0, "receivable": 0},
         {"name": "Chipo", "payable": 0, "receivable": 20.0}]
rep = utility._handle_customer_credit_query("who do i owe money?", custs, "USD")
print(rep); print("---")
check_true(" has You owe heading", rep.startswith("*You owe:*"))
check_true(" has em-dash lines",   "Supplier A — USD50.00" in rep)
check_true(" has total",           "*Total you owe:* USD125.00" in rep)
rep2 = utility._handle_customer_credit_query("who owes me money?", custs, "USD")
check_true(" receivables heading", rep2.startswith("*Who owes you:*"))
check_true(" receivable line",     "Chipo — USD20.00" in rep2)
check_true(" receivable total",    "*Total owed to you:* USD20.00" in rep2)
rep3 = utility._handle_customer_credit_query("who owes me and who do i owe", custs, "USD")
check_true(" combined both",       ("*Who owes you:*" in rep3) and ("*You owe:*" in rep3))

print("== Bug 3: money-mention provenance ==")
check_false(" sold 40 tomatoes to Chipo", utility._has_money_mention("I sold 40 tomatoes to Chipo"))
check_true("  sold 2 jam for 4",          utility._has_money_mention("sold 2 jam for 4"))
check_true("  sold 2 jam at 3 each",      utility._has_money_mention("sold 2 jam at 3 each"))
check_true("  USD4",                      utility._has_money_mention("sold jam USD4"))
check_true("  40 dollars",                utility._has_money_mention("sold tomatoes 40 dollars"))
check_true("  paid 20",                   utility._has_money_mention("sold jam, she paid 20"))

print("== Bug 3: promo price wins over guessed amount (resolve_sale_prices) ==")
class _Doc:
    def __init__(self, d): self._d = d
    def to_dict(self): return self._d
class _Q:
    def __init__(self, docs): self._docs = docs
    def where(self, *a, **k): return self
    def get(self): return self._docs
class _Coll:
    def __init__(self, name_map): self.name_map = name_map
    def document(self, _): return self
    def collection(self, name): return _Q(self.name_map.get(name, []))
promo_db = mock.MagicMock()
promo_db.collection.return_value = _Coll({
    "stock_batches": [_Doc({"name": "tomato", "on_sale": True, "sale_price_each": 0.51,
                            "quantity_remaining": 200, "price_each": 0.51})],
    "price_overrides": [],
})
txns = [{"intent": "create", "transaction_type": "sale",
         "details": {"items": [{"item": "tomato", "quantity": 40, "unit": "each"}],
                     "amount": 40.0, "customer": "Chipo"}}]
utility.resolve_sale_prices(promo_db, "263771", txns, raw_text="I sold 40 tomatoes to Chipo")
d = txns[0]["details"]; it = d["items"][0]
check(" promo ppu", it.get("price_per_unit"), 0.51)
check(" promo total", d.get("amount"), 20.4)
check_true(" promo flag survives _infer_prices", it.get("promo_applied"))

# Explicit user price must NOT be overridden by the promo.
txns2 = [{"intent": "create", "transaction_type": "sale",
          "details": {"items": [{"item": "tomato", "quantity": 40, "unit": "each",
                                 "price_per_unit": 1.0}], "amount": 40.0}}]
utility.resolve_sale_prices(promo_db, "263771", txns2, raw_text="I sold 40 tomatoes for $1 each")
check(" explicit price kept", txns2[0]["details"]["items"][0].get("price_per_unit"), 1.0)
check_true(" explicit flag set", txns2[0]["details"].get("explicit_price"))

print("== Bug 6: stock-in cost prompt + remembered-cost correction ==")
stock_txn = [{"intent": "create", "transaction_type": "stock_in",
              "details": {"items": [{"item": "jam", "quantity": 100, "unit": "each"}]}}]
prompt = main._check_missing_details(stock_txn, currency="USD")
print(prompt); print("---")
check_true(" asks cost for jam", prompt is not None and "pay for the Jam" in prompt)
stock_ok = [{"intent": "create", "transaction_type": "stock_in",
             "details": {"items": [{"item": "orange", "quantity": 100, "unit": "each",
                                    "price_per_unit": 0.15}]}}]
check(" no prompt when cost known", main._check_missing_details(stock_ok, currency="USD"), None)

remembered = [{"intent": "create", "transaction_type": "stock_in",
               "details": {"items": [{"item": "orange", "quantity": 100, "unit": "each",
                                      "price_per_unit": 0.15, "cost_source": "remembered"}],
                           "amount": 15.0}}]
res = main._try_fill_missing_details("0.20 each", remembered, currency="USD", mobile="")
check_true(" correction accepted", res is not None)
if res:
    filled, still = res
    it = filled[0]["details"]["items"][0]
    check(" corrected ppu", it.get("price_per_unit"), 0.2)
    check(" corrected total", filled[0]["details"].get("amount"), 20.0)
    check(" nothing still missing", still, None)

print("== card shows remembered-cost note ==")
card = utility.format_transaction_response([{
    "intent": "create", "transaction_type": "stock_in",
    "details": {"items": [{"item": "orange", "quantity": 100, "unit": "each",
                           "price_per_unit": 0.15, "cost_source": "remembered"}],
                "currency": "USD"}}])
print(card); print("---")
check_true(" last-cost marker", "your last cost" in card)
check_true(" changed-cost hint", "reply with the new cost" in card)
card2 = utility.format_transaction_response([{
    "intent": "create", "transaction_type": "sale",
    "details": {"items": [{"item": "tomato", "quantity": 40, "unit": "each",
                           "price_per_unit": 0.51, "promo_applied": True}],
                "currency": "USD"}}])
print(card2); print("---")
check_true(" on-sale marker", "on sale" in card2)
check_false(" no raw flag leak", "Promo Applied" in card2 or "Explicit Price" in card2)

print("== _money_to_float equivalence ==")
for v, want in [("USD0.15", 0.15), ("0.15 USD", 0.15), ("0.15 dollars", 0.15),
                ("15 cents", 0.15), ("50c", 0.5), ("R50", 50.0), ("4", 4.0)]:
    check(f" {v}", utility._money_to_float(v), want)

print("== misc: normalise, debt-regex safety ==")
check(" carrots→carrot", utility.normalise_item_name("Carrots"), "carrot")
check(" tomatoes→tomato", utility.normalise_item_name("tomatoes"), "tomato")

print(f"\n{'='*40}\nTOTAL: {PASS} passed, {FAIL} failed")
sys.exit(1 if FAIL else 0)