from __future__ import annotations from dataclasses import dataclass, field from datetime import date from rules import ( REASON_EXPIRY, REASON_EXPIRY_UNPARSEABLE, REASON_MARGIN, REASON_NOT_SELF, Allocation, Candidate, PurchaseLine, Rejected, allocate, validate_purchase, ) KIND_SUCCESS = "SUCCESS" KIND_BUSINESS_FAIL = "BUSINESS_FAIL" KIND_SYSTEM_FAIL = "SYSTEM_FAIL" @dataclass class MallOutcome: mall_name: str candidates: list[Candidate] | None = None failed_reason: str | None = None @dataclass class SelectionResult: kind: str reason: str allocations: list[Allocation] rejected: list[Rejected] malls: list[str] mall_errors: dict[str, str] = field(default_factory=dict) def run_selection( purchase: PurchaseLine, mall_outcomes: list[MallOutcome], today: date | None = None, ) -> SelectionResult: malls = [item.mall_name for item in mall_outcomes] mall_errors = { item.mall_name: item.failed_reason for item in mall_outcomes if item.failed_reason } row_reason = validate_purchase(purchase) if row_reason: return SelectionResult( kind=KIND_BUSINESS_FAIL, reason=row_reason, allocations=[], rejected=[], malls=malls, mall_errors=mall_errors, ) merged: list[Candidate] = [] had_hit = False for item in mall_outcomes: if item.failed_reason: continue rows = item.candidates or [] if rows: had_hit = True merged.extend(rows) allocation = allocate(merged, purchase, today=today) if allocation.allocations: return SelectionResult( kind=KIND_SUCCESS, reason="SELECTED", allocations=allocation.allocations, rejected=allocation.rejected, malls=malls, mall_errors=mall_errors, ) if mall_errors: return SelectionResult( kind=KIND_SYSTEM_FAIL, reason="SYSTEM_FAULT", allocations=[], rejected=allocation.rejected, malls=malls, mall_errors=mall_errors, ) return SelectionResult( kind=KIND_BUSINESS_FAIL, reason=_business_reason(allocation.passed_before_fold, allocation.rejected, had_hit), allocations=[], rejected=allocation.rejected, malls=malls, mall_errors={}, ) def _business_reason( passed_before_fold: list[Candidate], rejected: list[Rejected], had_hit: bool, ) -> str: if passed_before_fold: return "STOCK_INSUFFICIENT" reasons = [item.reason for item in rejected] if REASON_MARGIN in reasons: return REASON_MARGIN if REASON_EXPIRY in reasons or REASON_EXPIRY_UNPARSEABLE in reasons: return REASON_EXPIRY if REASON_NOT_SELF in reasons: return REASON_NOT_SELF if not had_hit: return "NO_HIT" return "NO_CANDIDATE_AFTER_RULES"