from datetime import datetime, timezone from pathlib import Path import pytest from app.action_catalog import ACTION_CODES, FLOW_V2_BUSINESS_ACTION_CODES, TRIAGE_ACTION_CODES from app.blif_flow_v2_projection_service import _projection_value, rebuild_blif_flow_v2_projection def derived_row(**overrides): row = { "opportunity_id": "11111111-1111-1111-1111-111111111111", "material_process_key": "odoo_sale_name:s00001", "canonical_process_id": "11111111-1111-1111-1111-111111111111", "raw_v2": { "business_state": "PROFORMA_REQUIRED", "business_next_action": "CREATE_PROFORMA", "diagnostic_status": "clear", "confidence": "high", "precedence": "business_transition", "reason": "Current order intent requires a proforma.", }, "evidence": { "latest_relevant_inbound": {"id": "m1", "at": "2026-08-15T00:00:00+00:00"}, "latest_relevant_outbound": None, "proforma": [], "invoice": [], "payment": [], "odoo": [], "reconciliation": [], }, } row.update(overrides) return row def test_migration_011_is_additive_reversible_and_does_not_touch_stage(): up = Path("migrations/011_blif_flow_v2_persistence.sql").read_text() down = Path("migrations/011_blif_flow_v2_persistence_down.sql").read_text() assert "CREATE TABLE IF NOT EXISTS opportunity_flow_state_v2" in up assert "CREATE TABLE IF NOT EXISTS opportunity_flow_transitions" in up for column in ("resolution_code", "resolved_at", "resolved_by_event_id", "superseded_by_task_id"): assert f"ADD COLUMN IF NOT EXISTS {column}" in up assert f"DROP COLUMN IF EXISTS {column}" in down assert "DROP TABLE IF EXISTS opportunity_flow_state_v2" in down assert "UPDATE opportunities" not in up assert "stage =" not in up def test_flow_v2_actions_are_separate_from_existing_triage_and_runtime_actions(): assert FLOW_V2_BUSINESS_ACTION_CODES == { "CREATE_PROFORMA", "CREATE_INVOICE", "VALIDATE_ODOO_ORDER", "COMPLETE_OPPORTUNITY", } assert "CREATE_JASMIN_QUOTE" not in FLOW_V2_BUSINESS_ACTION_CODES assert FLOW_V2_BUSINESS_ACTION_CODES.isdisjoint(TRIAGE_ACTION_CODES) assert FLOW_V2_BUSINESS_ACTION_CODES.isdisjoint(ACTION_CODES) def test_projection_fingerprint_is_stable_and_excludes_derived_time(): first = _projection_value(derived_row(), datetime(2026, 8, 15, tzinfo=timezone.utc)) second = _projection_value(derived_row(), datetime(2026, 8, 16, tzinfo=timezone.utc)) assert first["source_fingerprint"] == second["source_fingerprint"] assert first["business_next_action"] == "CREATE_PROFORMA" assert first["evidence_refs"] == [{ "source": "message", "role": "latest_relevant_inbound", "id": "m1", "at": "2026-08-15T00:00:00+00:00", }] def test_duplicate_projection_persists_canonical_material_identity(): row = derived_row( opportunity_id="22222222-2222-2222-2222-222222222222", canonical_process_id="11111111-1111-1111-1111-111111111111", ) value = _projection_value(row, datetime.now(timezone.utc)) assert value["is_duplicate_representation"] is True assert value["canonical_opportunity_id"] == "11111111-1111-1111-1111-111111111111" assert value["material_process_key"] == "odoo_sale_name:s00001" def test_off_mode_is_a_noop_without_deriving_or_connecting(): assert rebuild_blif_flow_v2_projection(mode="off") == { "mode": "off", "projection_count": 0, "transitions_written": 0, "disabled": True, } @pytest.mark.parametrize("mode", ["compare", "authoritative", "invalid"]) def test_unimplemented_modes_fail_closed(mode): with pytest.raises(RuntimeError, match="not implemented"): rebuild_blif_flow_v2_projection(mode=mode, derived_rows=[])