274 lines
12 KiB
Python
274 lines
12 KiB
Python
"""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",
|
|
)
|