"""Typed data contracts for the pricing pipeline. These mirror the master-tracker columns the Pricing Analyst owns (playbook §11) and the Definition of Done (playbook §13). Keeping them as pydantic models means every hop in the LangGraph state machine is validated. """ from __future__ import annotations from enum import Enum from pydantic import BaseModel, Field class Decision(str, Enum): APPROVED = "APPROVED" # -> tracker Status "Pricing Approved" BLOCKED = "BLOCKED" # -> tracker Status "Pricing Blocked" NEEDS_REVIEW = "NEEDS_REVIEW" class BuyBoxStatus(str, Enum): WON = "WON" # we are the Featured Offer LOST_PRICE = "LOST_PRICE" # lost Featured Offer on price LOST_ELIGIBILITY = "LOST_ELIGIBILITY" # lost on account/listing eligibility, not price SUPPRESSED = "SUPPRESSED" # Buy Box hidden entirely (price significantly high) UNKNOWN = "UNKNOWN" class SkuInput(BaseModel): """One row read from the master tracker before pricing.""" sku: str asin: str | None = None product_name: str = "" marketplace: str = "US" category: str = "" landed_cost: float = Field(..., description="COGS from Inventory Planner, per unit") target_price: float = Field(..., description="Proposed / current selling price to evaluate") # Optional inputs; if absent we fall back to the fee API / mock. fba_fee: float | None = None referral_pct: float | None = Field( default=None, description="Modeled/negotiated referral fraction, e.g. 0.12" ) dimensions: str | None = None weight: str | None = None class FeeQuote(BaseModel): """Normalized per-unit fee/cost breakdown from a data provider at a given price. COSMOS fills this from `takehome-calculator`; the mock fills it synthetically. Dollar amounts are per unit at `selling_price`. """ selling_price: float landed_cost: float # COGS (purchase + freight + duty) fba_fee: float = 0.0 referral_amt: float = 0.0 # Amazon referral fee, in dollars returns_reserve: float = 0.0 other_fees: float = 0.0 # inbound placement, low-inventory, EPR, VAT, handling… net_takehome: float | None = None # provider's authoritative net profit/unit, if given source: str = "unknown" class FeeStack(BaseModel): """Full per-unit fee stack + derived margin figures (playbook SOP A).""" selling_price: float landed_cost: float fba_fee: float referral_pct: float referral_amt: float returns_reserve: float storage_alloc: float total_cost: float profit: float contribution_margin_pct: float break_even_price: float map_floor: float # The referral basis used to derive this stack ("modeled" or "worst_case"). referral_basis: str = "worst_case" class CompetitorOffer(BaseModel): """One seller offer on the ASIN's Amazon listing (from Apify offers / Buy Box).""" seller_name: str = "Unknown seller" seller_id: str | None = None price: float | None = None is_buy_box: bool = False condition: str | None = None # True when seller_id matches APIFY_SELLER_ID — this is our own offer, not a rival. # Our own offers are excluded from the competitive band. is_ours: bool = False class CompetitiveSnapshot(BaseModel): """Result of the competitive / Buy Box scan (playbook SOP B).""" asin: str | None = None buy_box_status: BuyBoxStatus = BuyBoxStatus.UNKNOWN buy_box_price: float | None = None buy_box_seller_name: str | None = None buy_box_seller_id: str | None = None competitive_low: float | None = None competitive_median: float | None = None competitive_high: float | None = None is_suppressed: bool = False reason: str = "" # Named sellers + prices on this ASIN (featured first when present). offers: list[CompetitorOffer] = Field(default_factory=list) class PricingDecision(BaseModel): """The proposal presented to the human and (on approval) written to the tracker.""" sku: str asin: str | None = None decision: Decision recommended_price: float fee_stack: FeeStack competitive: CompetitiveSnapshot # Deterministic gate reasons (from evaluate node). gate_reasons: list[str] = Field(default_factory=list) # LLM-written, human-readable narrative (rationale node). rationale: str = "" competitor_summary: str = "" suppression_note: str = "" recommended_action: str = "" def tracker_row(self) -> dict: """Map to the master-tracker columns this role owns (playbook §11).""" fs = self.fee_stack status = { Decision.APPROVED: "Pricing Approved", Decision.BLOCKED: "Pricing Blocked", Decision.NEEDS_REVIEW: "Pricing Review", }[self.decision] return { "SKU": self.sku, "ASIN": self.asin or "", "Landed Cost": round(fs.landed_cost, 2), "FBA Fee": round(fs.fba_fee, 2), "Referral Fee %": round(fs.referral_pct * 100, 2), "Referral Fee $": round(fs.referral_amt, 2), "Break-even Price": round(fs.break_even_price, 2), "MAP Floor": round(fs.map_floor, 2), "Target Selling Price": round(self.recommended_price, 2), "Contribution Margin %": round(fs.contribution_margin_pct * 100, 2), "Buy Box Status": self.competitive.buy_box_status.value, "Buy Box Seller": self.competitive.buy_box_seller_name or "", "Buy Box Price": ( round(self.competitive.buy_box_price, 2) if self.competitive.buy_box_price is not None else "" ), "Competitor Offers": "; ".join( f"{o.seller_name} @ " f"{('$' + format(o.price, '.2f')) if o.price is not None else 'n/a'}" + (" [BB]" if o.is_buy_box else "") for o in self.competitive.offers[:8] ), "Suppression Risk": "YES" if self.competitive.is_suppressed else "NO", "Status": status, } class LlmNarrative(BaseModel): """Structured output schema the LLM must return (no numbers invented here).""" rationale: str competitor_summary: str suppression_note: str recommended_action: str