| """Tests for robust item resolution / dedup (dukaan.resolve + ops.add_inventory). |
| |
| Covers: |
| - Exact-name match returns status=="matched" and score==100.0 |
| - Upper-case variant of an existing item restocks (no duplicate row) |
| - Totally made-up item creates new_item and resolve returns "none" |
| - candidates_item returns a list; normalize_item_name parses brand/base |
| """ |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from dukaan import db, ops |
| import dukaan.resolve as resolve |
|
|
|
|
| |
| def _an_item() -> dict: |
| """Return a real seeded inventory row with a non-trivial name.""" |
| rows = db.qx( |
| "SELECT * FROM inv.inventory WHERE length(name) > 8 ORDER BY item_id LIMIT 1" |
| ) |
| assert rows, "seeded DB must have at least one item" |
| return rows[0] |
|
|
|
|
| |
| def test_exact_name_matched(seeded_db): |
| """An exact seeded name must resolve to 'matched' with score==100.0.""" |
| item = _an_item() |
| result = resolve.resolve_item(item["name"]) |
| assert result["status"] == "matched", ( |
| f"Expected 'matched' for '{item['name']}', got '{result['status']}'" |
| ) |
| assert result["score"] == 100.0, ( |
| f"Expected score 100.0, got {result['score']}" |
| ) |
|
|
|
|
| def test_case_and_variant_merges(seeded_db): |
| """Adding an existing item name uppercased must restock (merge), not create a duplicate row.""" |
| item = _an_item() |
| original_name = item["name"] |
| original_qty = int(item["qty"]) |
|
|
| |
| result = ops.add_inventory(original_name.upper(), 5) |
| assert result["ok"] is True, f"add_inventory failed: {result}" |
| assert result["action"] == "restock", ( |
| f"Expected 'restock' for upper-case variant of '{original_name}', " |
| f"got action='{result['action']}'" |
| ) |
|
|
| |
| updated = ops.find_item(original_name) |
| assert updated is not None, "Item disappeared after restock" |
| assert updated["qty"] == original_qty + 5, ( |
| f"Expected qty={original_qty + 5}, got {updated['qty']}" |
| ) |
|
|
| |
| count_rows = db.qx( |
| "SELECT COUNT(*) AS n FROM inv.inventory WHERE lower(name) = lower(?)", |
| (original_name,), |
| ) |
| assert count_rows[0]["n"] == 1, ( |
| f"Expected exactly 1 row for '{original_name}', " |
| f"got {count_rows[0]['n']} (duplicate created!)" |
| ) |
|
|
|
|
| def test_new_item_not_matched(seeded_db): |
| """A completely invented name must resolve to 'none' against the seeded catalog, |
| then create a new_item when added via add_inventory.""" |
| weird_name = "Qwzz9 Madeup Brand 500ml 1kg pack" |
|
|
| |
| resolve_before = resolve.resolve_item(weird_name) |
| assert resolve_before["status"] == "none", ( |
| f"Expected 'none' before insertion for '{weird_name}', " |
| f"got '{resolve_before['status']}'" |
| ) |
|
|
| |
| result = ops.add_inventory(weird_name, 7, mrp=40) |
| assert result["ok"] is True, f"add_inventory failed: {result}" |
| assert result["action"] == "new_item", ( |
| f"Expected 'new_item', got '{result['action']}'" |
| ) |
|
|
|
|
| def test_candidates_is_list(seeded_db): |
| """candidates_item('dal', 5) returns a list; normalize_item_name parses brand/base.""" |
| cands = ops.candidates_item("dal", 5) |
| assert isinstance(cands, list), ( |
| f"Expected list from candidates_item, got {type(cands)}" |
| ) |
| |
| |
| |
| parsed = resolve.normalize_item_name("TATA SALT 1KG") |
| assert isinstance(parsed, dict), "normalize_item_name must return a dict" |
| assert "base" in parsed, "Result must contain 'base' key" |
| |
| assert "salt" in parsed["base"].lower(), ( |
| f"Expected 'salt' in base, got base='{parsed['base']}'" |
| ) |
|
|