122 lines
3.0 KiB
Python
122 lines
3.0 KiB
Python
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"
|