"""dispatch 모듈 박스 묶기/SKU 합산 순수 로직 테스트. DB/엑셀 없이 store.py 의 규칙만 검증한다. python -m app.modules.dispatch.tests.test_grouping 또는 pytest 로 실행 가능. """ from __future__ import annotations from app.modules.dispatch import store def test_package_id_priority(): """묶음키 우선순위: package_id > tracking_id > order_id.""" rows = [ {"order_id": "O1", "package_id": "P1", "tracking_id": "T1", "seller_sku": "MT-0320", "quantity": "1"}, {"order_id": "O1", "package_id": "P1", "tracking_id": "T1", "seller_sku": "MT-0320", "quantity": "1"}, {"order_id": "O1", "package_id": "P2", "tracking_id": "T2", "seller_sku": "MY-0001", "quantity": "1"}, ] parcels = store.group_parcels(rows) assert len(parcels) == 2, parcels # P1, P2 → 박스 2개 p1 = next(p for p in parcels if p["package_id"] == "P1") # 같은 박스 같은 SKU 수량 합산 → 2 assert p1["items"][0]["quantity"] == 2, p1 def test_tracking_then_order_fallback(): """package_id 없으면 tracking_id, 그것도 없으면 order_id 로 묶는다.""" rows = [ {"order_id": "O9", "package_id": "", "tracking_id": "TRK", "seller_sku": "MT-0550", "quantity": ""}, {"order_id": "O9", "package_id": "", "tracking_id": "TRK", "seller_sku": "MX-0001", "quantity": "3"}, {"order_id": "O8", "package_id": "", "tracking_id": "", "seller_sku": "MT-0750", "quantity": "2"}, ] parcels = store.group_parcels(rows) assert len(parcels) == 2, parcels trk = next(p for p in parcels if p["tracking_id"] == "TRK") assert len(trk["items"]) == 2 # 다른 SKU 2종 # quantity 빈칸 → 1 로 보정 qty_by_sku = {it["seller_sku"]: it["quantity"] for it in trk["items"]} assert qty_by_sku["MT-0550"] == 1 assert qty_by_sku["MX-0001"] == 3 def test_quantity_and_tracking_preserved_as_string(): """수량 비면 1, 송장번호 숫자로 들어와도 문자열 보존.""" assert store.normalize_quantity("") == 1 assert store.normalize_quantity(None) == 1 assert store.normalize_quantity("0") == 1 assert store.normalize_quantity("5") == 5 # 큰 송장번호가 float 로 읽혀도 과학표기/.0 없이 보존 assert store.clean_text(12345678901.0) == "12345678901" assert store.clean_text(float("nan")) == "" def test_sku_summary(): rows = [ {"order_id": "A", "package_id": "PA", "tracking_id": "", "seller_sku": "MT-0320_3", "quantity": "1"}, {"order_id": "B", "package_id": "PB", "tracking_id": "", "seller_sku": "MT-0320_3", "quantity": "1"}, {"order_id": "C", "package_id": "PC", "tracking_id": "", "seller_sku": "MY-0001", "quantity": "5"}, ] parcels = store.group_parcels(rows) summary = store.sku_summary(parcels) totals = {r["seller_sku"]: r["total_qty"] for r in summary} assert totals == {"MT-0320_3": 2, "MY-0001": 5}, totals def test_missing_required_columns(): """필수 컬럼 누락 감지: seller_sku 없거나 묶음키 전무.""" mapping = store.resolve_columns(["Order ID", "Product Name", "Quantity"]) missing = store.missing_required(mapping) assert any("Seller SKU" in m for m in missing), missing # 묶음키(order id) 는 있으므로 그 누락 메시지는 없어야 함 assert not any("최소 1개" in m for m in missing), missing def test_column_alias_strip_and_case(): """헤더 앞뒤 공백/대소문자 무시하고 매핑.""" mapping = store.resolve_columns([" Order ID ", "TRACKING ID", "Seller SKU", "Quantity"]) assert "order_id" in mapping assert "tracking_id" in mapping assert "seller_sku" in mapping def test_shopee_product_info_single(): blob = ("[1] Product Name:[1+1=3] Mira's Kitchen Korean Grade Miracle Food " "Container Launch PROMO; Variation Name:3x 320ml; Price: RM 20.00; " "Quantity: 1; SKU Reference No.: MT-0320_3;") items = store.parse_shopee_product_info(blob) assert len(items) == 1, items it = items[0] assert it["seller_sku"] == "MT-0320_3", it assert it["quantity"] == 1, it assert it["variation"] == "3x 320ml", it assert "Mira's Kitchen" in it["product_name"], it def test_shopee_product_info_multi(): blob = ("[1] Product Name:Item A; Variation Name:S; Quantity: 2; " "SKU Reference No.: MT-0001; " "[2] Product Name:Item B; Variation Name:L; Quantity: 3; " "SKU Reference No.: MX-0002;") items = store.parse_shopee_product_info(blob) skus = {i["seller_sku"]: i["quantity"] for i in items} assert skus == {"MT-0001": 2, "MX-0002": 3}, skus def test_shopee_product_info_empty(): assert store.parse_shopee_product_info("") == [] assert store.parse_shopee_product_info(None) == [] def _run_all(): fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] for fn in fns: fn() print("PASS", fn.__name__) print(f"\n{len(fns)} tests passed.") if __name__ == "__main__": _run_all()