backend store_catalog updates

This commit is contained in:
sriram
2026-08-31 11:03:56 +05:30
parent 998df898db
commit 8394316907
19 changed files with 892 additions and 8 deletions

View File

@@ -0,0 +1,268 @@
"""Tests for unbranded-commodity detection and the own-products bucket.
There was no test anywhere that fed `infer_brand` a name with no brand in it,
which is exactly why "Salt 1kg" had been filing itself under Colgate-Palmolive
and "Red Chilli Powder" under Brooke Bond - complete with Brooke Bond's real
FSSAI licence number. The first two sections below are the regression wall for
that, and the third is the property that keeps this from over-reaching.
"""
from __future__ import annotations
import io
import pytest
from app.core import store_catalog_pipeline as pipeline
from app.services import price_estimator
from app.services.category_registry import detect_category_from_text
from app.services.generic_products import (
OWN_PRODUCTS_BRAND,
canonical_category,
is_unbranded,
)
openpyxl = pytest.importorskip("openpyxl")
def _sheet(names) -> bytes:
wb = openpyxl.Workbook()
ws = wb.active
ws.append(["Item Name"])
for name in names:
ws.append([name])
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
@pytest.fixture(autouse=True)
def _isolate_sku_counter(tmp_path, monkeypatch):
from app.services import sku_service
monkeypatch.setattr(sku_service, "_data_dir", tmp_path / "sku_sequences")
@pytest.fixture
def store(monkeypatch):
"""Captures (brand, row) pairs so a test can assert where a row landed."""
written: list = []
def fake_upsert(brand, rows, cleanup=False):
assert cleanup is False, "cleanup=True would delete the brand's existing catalog"
for row in rows:
written.append((brand, row))
return len(rows)
monkeypatch.setattr(pipeline, "upsert_brand_products", fake_upsert)
monkeypatch.setattr(pipeline, "get_products_by_brand", lambda b, **kw: [])
monkeypatch.setattr(pipeline, "embed_texts", lambda texts: [[0.0] * 384 for _ in texts])
return written
def _brand_of(names, store):
"""Run one sheet and return the brand the (single) input row landed under.
Matches on the ORIGINAL name rather than the stored `product_name`: stage 4
explodes a row into one per pack size and appends that size to the name, so
"Idhayam Sesame Oil" is stored as "Idhayam Sesame Oil 500ml".
"""
pipeline.run_pipeline("store.xlsx", _sheet(names), use_llm=False, fetch_images=False)
brands = {brand for brand, _row in store}
assert len(brands) == 1, f"expected one destination brand, got {brands}"
return brands.pop()
# ---------------------------------------------------------------------------
# The misroutes this exists to stop
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("name", [
"Salt 1kg", # matched alias "colgate active salt"
"Milk 1L", # matched alias "cadbury dairy milk"
"Butter 500g", # matched alias "nestle butter"
"Ghee 1L",
"Paneer 200g",
"Cheese Slices",
"Tea Powder 250g",
"Red Chilli Powder 100g", # matched alias "brooke bond red label"
"Oil 1L",
])
def test_a_commodity_is_not_filed_under_a_real_brand(name, store):
"""Each of these used to be written into a live brand's catalog because one
commodity noun matched a whole word inside a multi-word alias."""
assert _brand_of([name], store) == OWN_PRODUCTS_BRAND
def test_a_commodity_never_carries_someone_elses_fssai_licence(store):
"""The compliance half of the bug: the misresolved parent's real licence
number was copied onto the row."""
pipeline.run_pipeline("s.xlsx", _sheet(["Red Chilli Powder 100g", "Milk 1L", "Butter 500g"]), use_llm=False, fetch_images=False)
assert [row["fssai_license"] for _brand, row in store] == [None, None, None]
@pytest.mark.parametrize("name", [
"Toor Dhal 1kg", "Sugar 1kg", "Black Pepper 100g", "Rice 5kg",
"Urad Dal 500g", "Wheat Flour 5kg", "Jaggery 500g", "Groundnut Oil 1L",
])
def test_a_commodity_does_not_spawn_its_own_brand_table(name, store):
"""These used to become brand_toor, brand_sugar, brand_black, brand_rice -
one junk table per leading word."""
assert _brand_of([name], store) == OWN_PRODUCTS_BRAND
# ---------------------------------------------------------------------------
# The property that keeps this from over-reaching
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("name,expected", [
("Amul Butter 500g", "amul"),
("Cadbury Dairy Milk Silk", "cadbury"),
("Britannia Good Day Cashew Cookies 200g", "britannia"),
("Tata Salt 1kg", "hindustan unilever"), # a real alias, not a misroute
("Colgate Active Salt 200g", "colgate-palmolive"),
])
def test_a_branded_product_keeps_its_own_brand(name, expected, store):
"""One token that is not a commodity means "this is a brand". Without this,
the fix would be worse than the bug."""
assert _brand_of([name], store) == expected
@pytest.mark.parametrize("name", [
"Bikaji Aloo Bhujia 200g", "Aachi Sambar Powder", "Idhayam Sesame Oil",
"Lion Dates 450g", "Naga Maida 2kg",
])
def test_a_brand_absent_from_the_alias_map_is_not_swallowed(name, store):
"""BRAND_ALIASES holds only 231 mostly-large FMCG names. These are real
brands missing from it, and "unknown" must never mean "unbranded"."""
assert _brand_of([name], store) != OWN_PRODUCTS_BRAND
def test_qualifiers_do_not_make_a_commodity_look_branded():
assert is_unbranded("Organic Toor Dal Whole 1kg")
assert is_unbranded("Premium Sortex Basmati Rice 5kg")
assert is_unbranded("Rock Salt 1kg")
def test_a_supplied_brand_always_wins():
assert not is_unbranded("Sugar 1kg", sheet_brand="Madhur")
def test_a_blank_brand_column_is_an_explicit_statement():
"""The sheet troubled itself to include the column and left it empty. This
was previously the one case rejected outright."""
assert is_unbranded("Anything At All", brand_column_supplied=True)
# ---------------------------------------------------------------------------
# The rest of the row: category and price
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("name,category", [
("Toor Dhal 1kg", "Pulses, Grains & Spices"),
("Sugar 1kg", "Sugar & Jaggery"),
("Salt 1kg", "Salt & Staples"),
("Black Pepper 100g", "Spices & Masalas"),
("Rice 5kg", "Atta & Staples"),
])
def test_a_commodity_resolves_to_a_real_category(name, category):
"""These all fell through to "General" before, which disabled HSN lookup and
the pack-size unit rulebook along with it."""
assert detect_category_from_text(name) == category
assert canonical_category(name) is not None
def test_the_category_carries_an_hsn_code(store):
"""The names were chosen to match HSN_GST_TABLE keys, so tax enrichment
works without a second mapping."""
_brand_of(["Toor Dhal 1kg"], store)
assert store[0][1]["hsn_code"] == "0713"
@pytest.mark.parametrize("name,size,ceiling", [
("Sugar 1kg", "1kg", 100), # was ₹120-280 under the "general" band
("Salt 1kg", "1kg", 60), # was ₹120-280
("Turmeric Powder 100g", "100g", 80),
])
def test_a_staple_is_no_longer_priced_like_branded_fmcg(name, size, ceiling):
lo, _hi = price_estimator.estimate_price_range_for_size(
size, name, OWN_PRODUCTS_BRAND, detect_category_from_text(name) or ""
)
assert lo < ceiling, f"{name} still prices at ₹{lo}, above the {ceiling} sanity ceiling"
def test_pepper_is_not_priced_like_a_cheap_staple():
"""The opposite failure: spices are genuinely expensive by weight and used
to come out at ₹20-45 per 100g."""
lo, _hi = price_estimator.estimate_price_range_for_size(
"100g", "Black Pepper 100g", OWN_PRODUCTS_BRAND, "Spices & Masalas"
)
assert lo > 50
def test_staples_get_staple_pack_sizes():
"""100g/250g/500g is not how dal, rice or salt is sold."""
assert price_estimator.default_size_variants("Pulses, Grains & Spices", "Toor Dal") == ["500g", "1kg", "5kg"]
assert price_estimator.default_size_variants("Salt & Staples", "Salt") == ["1kg"]
# ---------------------------------------------------------------------------
# Table naming and visibility
# ---------------------------------------------------------------------------
def test_the_bucket_round_trips_to_its_table_and_back():
"""The frontend passes the display name straight back to
/api/brands/{brand}/products, so this has to be an identity."""
from app.services.brand_registry import resolve_parent_brand
from app.services.vector_store import _sanitize_name, display_name_for_suffix
suffix = _sanitize_name(resolve_parent_brand(OWN_PRODUCTS_BRAND))
assert suffix == "own_products"
assert display_name_for_suffix(suffix) == OWN_PRODUCTS_BRAND
def test_the_bucket_survives_an_active_brands_whitelist(monkeypatch):
"""conftest blanks ACTIVE_BRANDS for the whole suite, so nothing else here
runs under the value production actually ships. ACTIVE_BRANDS is a
whitelist: without the bucket in it, the table is invisible everywhere."""
from app.services import active_brands
monkeypatch.setattr(active_brands, "_RAW_ACTIVE_BRANDS", "Amul,Cadbury,Own Products")
active_brands.invalidate()
try:
assert active_brands.is_active_suffix("own_products")
assert not active_brands.is_active_suffix("toor")
finally:
active_brands.invalidate()
# ---------------------------------------------------------------------------
# The migration for rows already misfiled
# ---------------------------------------------------------------------------
def test_the_migration_selects_only_unbranded_rows():
"""The safety property. It runs over the REAL brand catalogs looking for
misfiled commodities, so it must never pick up a genuine product."""
from scripts.migrate_own_products import is_unbranded as predicate
# These are the rows sitting in brand_nestle / brand_amul / brand_cadbury.
assert predicate("Butter 500g")
assert predicate("Milk 1L")
assert predicate("Salt 1kg")
# These are their legitimate neighbours in the same tables.
assert not predicate("Amul Butter 500g")
assert not predicate("Nestle Munch Chocolate")
assert not predicate("Cadbury Dairy Milk Silk 150g")
def test_a_migrated_row_is_rebuilt_for_its_new_home():
from scripts.migrate_own_products import TARGET_SUFFIX, _rebuild_row
rebuilt = _rebuild_row({
"product_name": "Red Chilli Powder 100g",
"size_variants": ["100g"],
"category": "General",
"image_id": "brooke_bond_red_chilli_powder_100g",
"image_url": "https://cdn.example.com/brooke_bond/x.jpg",
"image_urls": ["https://cdn.example.com/brooke_bond/x.jpg"],
"fssai_license": "10013022001897", # Brooke Bond's, not this product's
})
assert rebuilt["fssai_license"] is None, "another company's licence must not travel"
assert rebuilt["image_id"].startswith(TARGET_SUFFIX)
assert rebuilt["image_urls"] == [] and rebuilt["image_url"] is None
assert rebuilt["category"] == "Spices & Masalas"
assert rebuilt["price_range"].startswith("₹")