from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models.attachment import AttachmentCategory def _seed_categories(db: Session) -> list[int]: cats = [ AttachmentCategory(name="TEST_CERT", sort_order=1, is_active=True), AttachmentCategory(name="TEST_EXTRA", sort_order=2, is_active=True), AttachmentCategory(name="TEST_OFF", sort_order=3, is_active=False), ] for c in cats: db.add(c) db.commit() for c in cats: db.refresh(c) return [c.id for c in cats] def test_categories_lists_active_only(client: TestClient, db: Session): _seed_categories(db) resp = client.get("/CargoTrace/attachment/categories") assert resp.status_code == 200 names = [c["name"] for c in resp.json()] assert "TEST_CERT" in names assert "TEST_EXTRA" in names assert "TEST_OFF" not in names # sorted by sort_order then name assert names.index("TEST_CERT") < names.index("TEST_EXTRA") def test_status_unknown_zongpai_is_undetermined(client: TestClient): resp = client.get( "/CargoTrace/attachment/status", params={"zongpai_no": "26BW0011"} ) assert resp.status_code == 200 data = resp.json() assert data["zongpai_no"] == "26BW0011" assert data["determination"] == "undetermined" assert data["items"] == [] assert data["all_complete"] is False def test_status_invalid_zongpai(client: TestClient): resp = client.get( "/CargoTrace/attachment/status", params={"zongpai_no": "BAD"} ) assert resp.status_code == 400 assert resp.json()["error_code"] == "INVALID_ZONGPAI" def test_status_reflects_config_and_boxed(client: TestClient, db: Session): cat_ids = _seed_categories(db) cert_id = cat_ids[0] # Plan: 检验证书 expected=80 client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [{"category_id": cert_id, "expected_qty": 80}], }, ) # Box 30 of them client.post( "/CargoTrace/attachment/box", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "box_no": 921, "quantity": 30, }, ) resp = client.get( "/CargoTrace/attachment/status", params={"zongpai_no": "26BW0011"} ) assert resp.status_code == 200 data = resp.json() assert data["determination"] == "has" assert data["all_complete"] is False item = data["items"][0] assert item["category_id"] == cert_id assert item["expected_qty"] == 80 assert item["boxed_qty"] == 30 assert item["complete"] is False def test_config_sets_none(client: TestClient): resp = client.put( "/CargoTrace/attachment/config", json={"zongpai_no": "26BW0011", "determination": "none", "items": []}, ) assert resp.status_code == 200 assert resp.json()["determination"] == "none" assert resp.json()["items"] == [] def test_config_creates_and_updates_items(client: TestClient, db: Session): cert_id, extra_id, _ = _seed_categories(db) client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [ {"category_id": cert_id, "expected_qty": 80}, {"category_id": extra_id, "expected_qty": 5}, ], }, ) # Update: change extra expected, drop nothing resp = client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [ {"category_id": cert_id, "expected_qty": 80}, {"category_id": extra_id, "expected_qty": 8}, ], }, ) assert resp.status_code == 200 extra = next(i for i in resp.json()["items"] if i["category_id"] == extra_id) assert extra["expected_qty"] == 8 def test_config_rejects_removing_type_with_boxes(client: TestClient, db: Session): cert_id, _, _ = _seed_categories(db) client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [{"category_id": cert_id, "expected_qty": 80}], }, ) client.post( "/CargoTrace/attachment/box", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "box_no": 922, "quantity": 10, }, ) # Try to drop the type that now has boxes resp = client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [], }, ) assert resp.status_code == 409 assert resp.json()["error_code"] == "CATEGORY_HAS_BOXES" def test_config_allows_removing_type_with_zero_boxes(client: TestClient, db: Session): cert_id, extra_id, _ = _seed_categories(db) client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [ {"category_id": cert_id, "expected_qty": 80}, {"category_id": extra_id, "expected_qty": 5}, ], }, ) resp = client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [{"category_id": cert_id, "expected_qty": 80}], }, ) assert resp.status_code == 200 assert {i["category_id"] for i in resp.json()["items"]} == {cert_id} def test_config_rejects_unknown_category(client: TestClient): resp = client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [{"category_id": 9999999, "expected_qty": 1}], }, ) assert resp.status_code == 404 assert resp.json()["error_code"] == "CATEGORY_NOT_FOUND" def _config_one_type(client: TestClient, cert_id: int): client.put( "/CargoTrace/attachment/config", json={ "zongpai_no": "26BW0011", "determination": "has", "items": [{"category_id": cert_id, "expected_qty": 80}], }, ) def test_attachment_location_create_and_duplicate(client: TestClient, db: Session): cert_id = _seed_categories(db)[0] _config_one_type(client, cert_id) created = client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "location_code": "A01-01-01", }, ) assert created.status_code == 200 assert created.json()["location_code"] == "A01-01-01" assert created.json()["previous_location"] is None dup = client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "location_code": "A02-02-02", }, ) assert dup.status_code == 409 assert dup.json()["error_code"] == "DUPLICATE_LOCATION" def test_attachment_location_normal_to_temp_then_transit(client: TestClient, db: Session): cert_id = _seed_categories(db)[0] _config_one_type(client, cert_id) client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "location_code": "A01-01-01", }, ) # normal -> temp storage (B prefix) allowed to_temp = client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "location_code": "B01-01-01", }, ) assert to_temp.status_code == 200 assert to_temp.json()["previous_location"] == "A01-01-01" # -> transit (terminal) to_transit = client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "location_code": "TRANS-001", }, ) assert to_transit.status_code == 200 # already transit -> any op blocked again = client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": cert_id, "location_code": "A03-03-03", }, ) assert again.status_code == 409 assert again.json()["error_code"] == "ALREADY_OFF_SHELF" def test_attachment_location_rejects_unknown_category(client: TestClient): resp = client.post( "/CargoTrace/attachment/location", json={ "zongpai_no": "26BW0011", "category_id": 9999999, "location_code": "A01-01-01", }, ) assert resp.status_code == 404 assert resp.json()["error_code"] == "CATEGORY_NOT_FOUND"