"""Smoke test — UOM conversions are one-directional, and the API says so. `UomConverter` looks up exactly one shape, `FromUom -> ToUom = item.BaseUomId`, and never inverts a factor. `UpdateUomConversionsAsync` used to accept *any* pair, so saving the more natural-reading `base -> BOX` produced a row that returned 200, appeared in the item detail response, and was then silently invisible to every consumer — surfacing much later as "no UOM conversion" 422 at GRN confirm or stage start, on an item that visibly had one. * base -> other is rejected with 422 (the direction that used to save and then not work) * other -> base is accepted * a self-conversion and a base-as-source row are rejected * a zero/negative factor is rejected (UomConverter divides unit cost by it) * changing an item's base UOM while conversions exist is refused rather than orphaning them python Backend/smoke/uom_direction.py """ from __future__ import annotations import sys from smoke_common import bootstrap def main(): c, chk, args = bootstrap(__doc__) print(f"API {args.api}") item = c.get("/items?pageSize=1&status=Active").body["items"] if not item: sys.exit("FATAL: no active items.") item = item[0] item_id, base_uom = item["itemId"], item["baseUomId"] uoms = c.get("/uoms?pageSize=50").body["items"] other = next((u["uomId"] for u in uoms if u["uomId"] != base_uom), None) if other is None: sys.exit("FATAL: need at least 2 UOMs.") print(f"item={item_id} baseUom={base_uom} otherUom={other}") def put(conversions): return c.put(f"/items/{item_id}/uom-conversions", {"conversions": conversions}) chk.section("1. The correct direction is accepted") ok = put([{"fromUom": other, "toUom": base_uom, "factor": 12}]) chk.status("other -> base", ok, 200) if ok.status == 200: chk.check("stored with the base UOM as target", ok.body["conversions"][0]["toUom"], base_uom) chk.section("2. The reverse direction is rejected, not silently stored") chk.status("base -> other", put([{"fromUom": base_uom, "toUom": other, "factor": 12}]), 422) chk.section("3. Degenerate rows are rejected") chk.status("self-conversion (other -> other)", put([{"fromUom": other, "toUom": other, "factor": 2}]), 422) chk.status("zero factor", put([{"fromUom": other, "toUom": base_uom, "factor": 0}]), 422) chk.status("negative factor", put([{"fromUom": other, "toUom": base_uom, "factor": -3}]), 422) chk.section("4. Base UOM cannot be repointed while conversions exist") # Restore a valid conversion first, so the guard has something to protect. put([{"fromUom": other, "toUom": base_uom, "factor": 12}]) head = c.get(f"/items/{item_id}") if head.status == 200: body = head.body moved = c.put(f"/items/{item_id}", { "sku": body["sku"], "name": body["name"], "description": body.get("description"), "categoryId": body["categoryId"], "subCategoryId": body.get("subCategoryId"), "brandId": body.get("brandId"), "baseUomId": other, # <- the repoint being guarded "defaultVendorId": body.get("defaultVendorId"), "stockNature": body["stockNature"], "trackingMode": body["trackingMode"], "taxClass": body.get("taxClass"), "salePrice": body.get("salePrice"), }, if_match=head.etag) chk.status("change base UOM with conversions defined", moved, 422) else: chk.check("could read the item for the repoint test", head.status, 200) return chk.finish("UOM-DIRECTION") if __name__ == "__main__": sys.exit(main())