瀏覽代碼

feat: add dual-gate storage release pure functions and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 月之前
父節點
當前提交
309b781c0e
共有 2 個文件被更改,包括 167 次插入 和 0 次删除
  1. 74 0
      backend/app/crud/storage_release_review.py
  2. 93 0
      backend/tests/test_storage_release_dual_gate.py

+ 74 - 0
backend/app/crud/storage_release_review.py

@@ -135,6 +135,80 @@ def normalize_review_phase(review_phase: Optional[str]) -> Optional[str]:
     return None
 
 
+def conclusion_blank(value: Optional[str]) -> bool:
+    return not (value or "").strip()
+
+
+def aggregate_review_conclusion(
+    prod: Optional[str], qc: Optional[str]
+) -> Optional[str]:
+    p = (prod or "").strip()
+    q = (qc or "").strip()
+    if p == "btg" or q == "btg":
+        return "btg"
+    if p == "tg" and q == "tg":
+        return "tg"
+    return None
+
+
+def storage_transition(
+    prod: Optional[str], qc: Optional[str], storage_status: Optional[str]
+) -> str:
+    """Return 'store' | 'unstore' | 'noop'."""
+    agg = aggregate_review_conclusion(prod, qc)
+    st = (storage_status or "drk").strip() or "drk"
+    if agg == "tg" and st == "drk":
+        return "store"
+    if agg == "btg" and st == "yrk":
+        return "unstore"
+    return "noop"
+
+
+def normalize_review_side(raw: Optional[str]) -> Optional[str]:
+    s = (raw or "").strip().lower()
+    if s in ("overview", "production", "qc"):
+        return s
+    return None
+
+
+def review_side_required_menu(review_side: Optional[str]) -> Optional[str]:
+    if review_side == "production":
+        return "production:storageReleaseReview:list"
+    if review_side == "qc":
+        return "quality:storageReleaseReview:list"
+    return None
+
+
+def matches_dual_review_phase(
+    review_side: Optional[str],
+    phase: Optional[str],
+    prod: Optional[str],
+    qc: Optional[str],
+) -> bool:
+    """Pure predicate for Tab semantics; SQL filter must match this."""
+    side = normalize_review_side(review_side) or "overview"
+    ph = normalize_review_phase(phase)
+    if not ph:
+        return True
+    prod_blank = conclusion_blank(prod)
+    qc_blank = conclusion_blank(qc)
+    if side == "production":
+        return prod_blank if ph == "pending" else (not prod_blank)
+    if side == "qc":
+        return qc_blank if ph == "pending" else (not qc_blank)
+    both_filled = (not prod_blank) and (not qc_blank)
+    return (not both_filled) if ph == "pending" else both_filled
+
+
+def should_copy_legacy_to_qc(
+    qc_review_conclusion: Optional[str],
+    legacy_review_conclusion: Optional[str],
+) -> bool:
+    return conclusion_blank(qc_review_conclusion) and not conclusion_blank(
+        legacy_review_conclusion
+    )
+
+
 def normalize_qc_work_order_completed(raw) -> Optional[bool]:
     """Return True/False when recognizable; None means no filter."""
     if raw is None:

+ 93 - 0
backend/tests/test_storage_release_dual_gate.py

@@ -0,0 +1,93 @@
+from app.crud.storage_release_review import (
+    aggregate_review_conclusion,
+    conclusion_blank,
+    matches_dual_review_phase,
+    normalize_review_side,
+    review_side_required_menu,
+    should_copy_legacy_to_qc,
+    storage_transition,
+)
+
+
+def test_conclusion_blank():
+    assert conclusion_blank(None) is True
+    assert conclusion_blank("") is True
+    assert conclusion_blank("  ") is True
+    assert conclusion_blank("tg") is False
+    assert conclusion_blank("btg") is False
+
+
+def test_aggregate_both_tg():
+    assert aggregate_review_conclusion("tg", "tg") == "tg"
+
+
+def test_aggregate_any_btg():
+    assert aggregate_review_conclusion("btg", "tg") == "btg"
+    assert aggregate_review_conclusion("tg", "btg") == "btg"
+    assert aggregate_review_conclusion("btg", None) == "btg"
+    assert aggregate_review_conclusion("", "btg") == "btg"
+
+
+def test_aggregate_pending_when_not_aligned():
+    assert aggregate_review_conclusion("tg", None) is None
+    assert aggregate_review_conclusion(None, "tg") is None
+    assert aggregate_review_conclusion(None, None) is None
+    assert aggregate_review_conclusion("", "") is None
+
+
+def test_storage_transition_store_when_both_tg_and_pending():
+    assert storage_transition("tg", "tg", "drk") == "store"
+
+
+def test_storage_transition_unstore_when_btg_and_stored():
+    assert storage_transition("btg", "tg", "yrk") == "unstore"
+    assert storage_transition("tg", "btg", "yrk") == "unstore"
+
+
+def test_storage_transition_noop_otherwise():
+    assert storage_transition("tg", "tg", "yrk") == "noop"
+    assert storage_transition("tg", None, "drk") == "noop"
+    assert storage_transition("btg", "tg", "drk") == "noop"
+    assert storage_transition(None, None, "yrk") == "noop"
+
+
+def test_matches_dual_review_phase_production():
+    assert matches_dual_review_phase("production", "pending", None, "tg") is True
+    assert matches_dual_review_phase("production", "pending", "tg", "tg") is False
+    assert matches_dual_review_phase("production", "reviewed", "btg", None) is True
+    assert matches_dual_review_phase("production", "reviewed", None, "tg") is False
+
+
+def test_matches_dual_review_phase_qc():
+    assert matches_dual_review_phase("qc", "pending", "tg", None) is True
+    assert matches_dual_review_phase("qc", "reviewed", "tg", "tg") is True
+    assert matches_dual_review_phase("qc", "reviewed", "tg", None) is False
+
+
+def test_matches_dual_review_phase_overview():
+    assert matches_dual_review_phase("overview", "pending", None, "tg") is True
+    assert matches_dual_review_phase("overview", "pending", "tg", "tg") is False
+    assert matches_dual_review_phase("overview", "reviewed", "tg", "btg") is True
+    assert matches_dual_review_phase("overview", "reviewed", "tg", None) is False
+
+
+def test_should_copy_legacy_to_qc():
+    assert should_copy_legacy_to_qc(None, "tg") is True
+    assert should_copy_legacy_to_qc("", "btg") is True
+    assert should_copy_legacy_to_qc("tg", "tg") is False
+    assert should_copy_legacy_to_qc(None, None) is False
+    assert should_copy_legacy_to_qc(None, "") is False
+
+
+def test_normalize_review_side():
+    assert normalize_review_side("production") == "production"
+    assert normalize_review_side(" QC ") == "qc"
+    assert normalize_review_side("overview") == "overview"
+    assert normalize_review_side("all") is None
+    assert normalize_review_side(None) is None
+
+
+def test_review_side_required_menu():
+    assert review_side_required_menu("production") == "production:storageReleaseReview:list"
+    assert review_side_required_menu("qc") == "quality:storageReleaseReview:list"
+    assert review_side_required_menu("overview") is None