"""Pure, shadow-only BLIF Flow v2 business-state projection. This module has no database or V1 dependencies. In particular, task fields are kept only for audit output and never establish document or payment facts. """ from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime from typing import Any @dataclass(frozen=True) class BusinessFacts: opportunity_id: str = "" terminal: bool = False explicitly_lost: bool = False exception: bool = False review_required: bool = False fiscal_blocked: bool = False document_reconciliation_required: bool = False customer_request: bool = False request_kind: str = "info" # info | quote latest_relevant_inbound_at: datetime | None = None latest_relevant_outbound_at: datetime | None = None info_or_offer_sent: bool = False order_intent: bool = False order_intent_at: datetime | None = None fiscal_identity_evidence: bool = False proforma_exists: bool = False proforma_sent: bool = False proforma_created_at: datetime | None = None proforma_sent_at: datetime | None = None potential_payment_evidence: bool = False payment_confirmed: bool = False payment_confirmed_at: datetime | None = None invoice_exists: bool = False invoice_created_at: datetime | None = None odoo_order_exists: bool = False odoo_order_validated: bool = False fulfillment_complete: bool = False material_order_change: bool = False material_order_change_at: datetime | None = None later_customer_inbound_satisfies_followup: bool = False blockers: tuple[str, ...] = () audit_task_codes: tuple[str, ...] = field(default=(), compare=False) @dataclass(frozen=True) class FlowV2Decision: business_state: str next_action: str | None operational_queue: str reason: str confidence: str = "high" def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass(frozen=True) class EffectiveOperationalDecision: business_state: str business_next_action: str | None effective_operational_action: str | None effective_operational_queue: str reason: str confidence: str precedence: str diagnostic_status: str = "clear" legacy_preserved_action: bool = False def to_dict(self) -> dict[str, Any]: return asdict(self) def derive_business_facts(**evidence: Any) -> BusinessFacts: """Normalize factual adapter output without inferring facts from tasks.""" allowed = BusinessFacts.__dataclass_fields__ values = {key: value for key, value in evidence.items() if key in allowed} for key in ("blockers", "audit_task_codes"): if key in values and not isinstance(values[key], tuple): values[key] = tuple(values[key] or ()) return BusinessFacts(**values) def _after(left: datetime | None, right: datetime | None) -> bool: return bool(left and right and left > right) def derive_business_state(facts: BusinessFacts) -> str: """Derive the current state from strongest present-tense facts.""" if facts.exception: return "EXCEPTION" if facts.explicitly_lost: return "LOST" if facts.review_required: return "REVIEW_REQUIRED" if facts.fiscal_blocked: return "FISCAL_BLOCKED" if facts.document_reconciliation_required: return "DOCUMENT_RECONCILIATION_REQUIRED" change_after_payment = facts.material_order_change and ( facts.payment_confirmed or facts.invoice_exists or _after(facts.material_order_change_at, facts.payment_confirmed_at) ) if change_after_payment: return "REVIEW_REQUIRED" # An invoice without confirmed payment contradicts BLIF's normal protected # sequence. Do not silently skip payment or invent a correction flow. if facts.invoice_exists and not facts.payment_confirmed: return "REVIEW_REQUIRED" if facts.odoo_order_exists and (not facts.payment_confirmed or not facts.invoice_exists): return "REVIEW_REQUIRED" if facts.terminal and facts.payment_confirmed and facts.invoice_exists and facts.odoo_order_validated: return "COMPLETED" if facts.fulfillment_complete and facts.payment_confirmed and facts.invoice_exists: return "COMPLETED" if facts.odoo_order_validated: return "ODOO_ORDER_VALIDATED" if facts.odoo_order_exists: return "ODOO_ORDER_CREATED" if facts.invoice_exists: return "INVOICE_CREATED" if facts.payment_confirmed: return "INVOICE_REQUIRED" change_invalidates_proforma = facts.material_order_change and ( not facts.material_order_change_at or not facts.proforma_created_at or _after(facts.material_order_change_at, facts.proforma_created_at) ) if facts.order_intent and (not facts.proforma_exists or change_invalidates_proforma): return "PROFORMA_REQUIRED" if facts.proforma_exists: return "PROFORMA_SENT" if facts.proforma_sent else "PROFORMA_CREATED" if facts.order_intent: return "ORDER_INTENT" if facts.info_or_offer_sent and not _after( facts.latest_relevant_inbound_at, facts.latest_relevant_outbound_at ): return "AWAITING_CUSTOMER" return "INQUIRY" def derive_next_action(facts: BusinessFacts, state: str | None = None) -> FlowV2Decision: state = state or derive_business_state(facts) if state in {"REVIEW_REQUIRED", "FISCAL_BLOCKED", "DOCUMENT_RECONCILIATION_REQUIRED", "EXCEPTION"}: action = { "REVIEW_REQUIRED": "REVIEW_REQUIRED", "FISCAL_BLOCKED": "VALIDATE_FISCAL_CUSTOMER", "DOCUMENT_RECONCILIATION_REQUIRED": "RECONCILE_DOCUMENTS", "EXCEPTION": "REVIEW_EXCEPTION", }[state] return FlowV2Decision(state, action, "review" if state != "EXCEPTION" else "exception", "; ".join(facts.blockers) or f"{state} requires operator review.", "medium") if state in {"LOST", "NO_INTEREST", "COMPLETED"}: return FlowV2Decision(state, None, "not_current", "The factual process is terminal.") if state == "INQUIRY": if not facts.customer_request: return FlowV2Decision(state, None, "not_current", "No current unanswered customer request is evidenced.", "low") action = "SEND_QUOTE" if facts.request_kind == "quote" else "SEND_INFO" return FlowV2Decision(state, action, "do_now", "Customer request has no later relevant outbound response.", "medium") if state == "AWAITING_CUSTOMER": return FlowV2Decision(state, None, "waiting", "Information or offer was sent; awaiting a later customer decision.") if state in {"ORDER_INTENT", "PROFORMA_REQUIRED"}: return FlowV2Decision("PROFORMA_REQUIRED", "CREATE_PROFORMA", "do_now", "Customer order intent exists and no current structured proforma exists.") if state == "PROFORMA_CREATED": return FlowV2Decision(state, "SEND_PROFORMA", "do_now", "A current structured proforma exists but has no factual sent evidence.") if state == "PROFORMA_SENT": if facts.potential_payment_evidence: return FlowV2Decision("AWAITING_PAYMENT", "CONFIRM_PAYMENT", "do_now", "Potential payment evidence requires operator confirmation.", "medium") return FlowV2Decision("AWAITING_PAYMENT", None, "waiting", "The current proforma was sent and payment is not confirmed.") if state in {"PAYMENT_CONFIRMED", "INVOICE_REQUIRED"}: return FlowV2Decision("INVOICE_REQUIRED", "CREATE_INVOICE", "do_now", "Payment is confirmed and no structured invoice exists.") if state == "INVOICE_CREATED": return FlowV2Decision("ODOO_ORDER_REQUIRED", "PREPARE_ORDER", "do_now", "Invoice exists and no Odoo sale order exists.") if state == "ODOO_ORDER_CREATED": return FlowV2Decision(state, "VALIDATE_ODOO_ORDER", "do_now", "Odoo sale order exists but is not validated.") if state == "ODOO_ORDER_VALIDATED": return FlowV2Decision(state, "COMPLETE_OPPORTUNITY", "do_now", "The validated Odoo order is ready for opportunity completion.") return FlowV2Decision("REVIEW_REQUIRED", "REVIEW_REQUIRED", "review", f"No safe Flow v2 rule for {state}.", "low") def derive_v2_operational_queue(facts: BusinessFacts) -> FlowV2Decision: return derive_next_action(facts, derive_business_state(facts)) def derive_effective_operational_action( business: FlowV2Decision, *, integration_exception: bool = False, scheduled_call_current: bool = False, due_followup_action: str | None = None, future_followup_action: str | None = None, fiscal_complete: bool = True, fiscal_required: bool = False, reconciliation_blocking: bool = False, diagnostic_status: str = "clear", ) -> EffectiveOperationalDecision: """Apply operational prerequisites without changing the business state.""" action, queue, reason, precedence = ( business.next_action, business.operational_queue, business.reason, "business_transition" ) if integration_exception: action, queue, reason, precedence = "REVIEW_EXCEPTION", "exception", "An integration failure blocks current work.", "integration_exception" elif scheduled_call_current: action, queue, reason, precedence = "CALL_CUSTOMER", "do_now", "An explicit scheduled call is currently due.", "scheduled_call" elif reconciliation_blocking: action, queue, reason, precedence = "RECONCILE_DOCUMENTS", "review", "A real formal document requires current association/reconciliation.", "document_prerequisite" elif fiscal_required and not fiscal_complete: action, queue, reason, precedence = "VALIDATE_FISCAL_CUSTOMER", "do_now", "Fiscal identity is required before the current formal-document transition.", "fiscal_prerequisite" elif due_followup_action and business.operational_queue == "waiting": action, queue, reason, precedence = due_followup_action, "do_now", "A scheduled external follow-up is due and remains unsatisfied.", "due_followup" elif future_followup_action and business.operational_queue == "waiting": action, queue, reason, precedence = future_followup_action, "waiting", "A scheduled external follow-up is not due yet.", "future_followup" return EffectiveOperationalDecision( business.business_state, business.next_action, action, queue, reason, business.confidence, precedence, diagnostic_status, ) def derive_safe_operational_action( raw: EffectiveOperationalDecision, *, v1_action: str | None, v1_queue: str | None, strong_current_evidence: bool, ) -> EffectiveOperationalDecision: """Conservatively preserve current V1 work when RAW evidence is uncertain.""" current = v1_queue in {"do_now", "review", "exception"} if strong_current_evidence and ( raw.confidence == "high" or raw.business_state in {"REVIEW_REQUIRED", "FISCAL_BLOCKED", "EXCEPTION"} ): return raw if current and v1_action: return EffectiveOperationalDecision( raw.business_state, raw.business_next_action, v1_action, v1_queue or "review", "SAFE V2 preserves the current V1 obligation because RAW evidence is not strong enough to replace it.", raw.confidence, "safe_preserve_v1", raw.diagnostic_status, v1_action == "CREATE_JASMIN_QUOTE", ) if v1_queue in {"backlog", "waiting"} and not strong_current_evidence: return EffectiveOperationalDecision( raw.business_state, raw.business_next_action, v1_action, v1_queue, "SAFE V2 preserves the non-current V1 queue because no stronger current obligation is proven.", raw.confidence, "safe_preserve_noncurrent", raw.diagnostic_status, ) if raw.effective_operational_queue in {"do_now", "review", "exception"}: return EffectiveOperationalDecision( raw.business_state, raw.business_next_action, None, "not_current", "Ambiguous or incomplete history is diagnostic only; it does not create current work.", raw.confidence, "safe_diagnostic_only", raw.diagnostic_status, ) return raw def suppress_duplicate_representation( projection: EffectiveOperationalDecision, *, canonical_process_id: str, ) -> EffectiveOperationalDecision: """Suppress a duplicate local card while retaining its diagnostic trace.""" return EffectiveOperationalDecision( projection.business_state, projection.business_next_action, None, "not_current", f"Duplicate representation of canonical material process {canonical_process_id}.", "high", "duplicate_representation", "duplicate_representation", )