123 lines
4.0 KiB
Python
123 lines
4.0 KiB
Python
"""Order router — select highest-scoring supplier and record profit.
|
|
|
|
select_supplier: eligible = active supplier, linked supplier_product, stock >= qty.
|
|
route_order: assigns supplier, computes supplier_cost / shipping_cost / fees,
|
|
records net profit = retail_total - supplier_cost - shipping_cost - fees.
|
|
"""
|
|
from decimal import ROUND_HALF_UP, Decimal
|
|
|
|
from app.engines import scoring
|
|
from app.models import AuditLog, Product, Supplier, SupplierProduct
|
|
|
|
CENT = Decimal("0.01")
|
|
FEE_RATE = Decimal("0.05") # payment processing estimate
|
|
FEE_FLAT = Decimal("0.30")
|
|
|
|
|
|
def _f(value) -> float:
|
|
try:
|
|
return float(value or 0)
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
|
|
|
|
def select_supplier(db, product_id, qty):
|
|
"""Return (result_dict, error_str). result = {supplier, supplier_product, score}."""
|
|
product = db.query(Product).filter(Product.id == product_id).first()
|
|
if product is None:
|
|
return None, "product not found"
|
|
|
|
rows = (
|
|
db.query(SupplierProduct, Supplier)
|
|
.join(Supplier, SupplierProduct.supplier_id == Supplier.id)
|
|
.filter(
|
|
SupplierProduct.product_id == product_id,
|
|
Supplier.status == "active",
|
|
SupplierProduct.supplier_inventory >= qty,
|
|
)
|
|
.all()
|
|
)
|
|
if not rows:
|
|
return None, "no eligible supplier"
|
|
|
|
pool = {"avg_cost": sum(_f(sp.supplier_cost) for sp, _ in rows) / len(rows)}
|
|
scored = [
|
|
(scoring.score_supplier(db, supplier, sp, qty, pool), sp, supplier)
|
|
for sp, supplier in rows
|
|
]
|
|
best_score, best_sp, best_supplier = max(scored, key=lambda t: t[0])
|
|
return {
|
|
"supplier": best_supplier,
|
|
"supplier_product": best_sp,
|
|
"score": best_score,
|
|
}, None
|
|
|
|
|
|
def route_order(db, order):
|
|
"""Route a newly created order to the best supplier(s) and record profit."""
|
|
supplier_id = None
|
|
supplier_cost = Decimal("0")
|
|
shipping_cost = Decimal("0")
|
|
routing_detail = []
|
|
|
|
for item in order.items or []:
|
|
result, err = select_supplier(db, item["product_id"], int(item["qty"]))
|
|
if err:
|
|
routing_detail.append({"product_id": item["product_id"], "error": err})
|
|
continue
|
|
sp = result["supplier_product"]
|
|
if supplier_id is None:
|
|
supplier_id = sp.supplier_id
|
|
supplier_cost += Decimal(str(sp.supplier_cost or 0)) * int(item["qty"])
|
|
shipping_cost += Decimal(str(sp.shipping_cost or 0))
|
|
routing_detail.append(
|
|
{
|
|
"product_id": item["product_id"],
|
|
"supplier": result["supplier"].name,
|
|
"supplier_score": result["score"],
|
|
}
|
|
)
|
|
|
|
order.supplier_id = supplier_id
|
|
order.supplier_cost = supplier_cost.quantize(CENT, rounding=ROUND_HALF_UP)
|
|
order.shipping_cost = shipping_cost.quantize(CENT, rounding=ROUND_HALF_UP)
|
|
order.fees = (Decimal(str(order.retail_total or 0)) * FEE_RATE + FEE_FLAT).quantize(
|
|
CENT, rounding=ROUND_HALF_UP
|
|
)
|
|
order.profit = (
|
|
Decimal(str(order.retail_total or 0))
|
|
- order.supplier_cost
|
|
- order.shipping_cost
|
|
- order.fees
|
|
).quantize(CENT, rounding=ROUND_HALF_UP)
|
|
|
|
db.add(
|
|
AuditLog(
|
|
actor="order_router",
|
|
action="order_routed",
|
|
entity="order",
|
|
entity_id=order.order_number,
|
|
detail={
|
|
"supplier_id": str(supplier_id) if supplier_id else None,
|
|
"profit": float(order.profit),
|
|
"routing": routing_detail,
|
|
},
|
|
)
|
|
)
|
|
|
|
if supplier_id is None:
|
|
order.status = "new" # stays unfulfilled; no eligible supplier
|
|
db.commit()
|
|
return {"routed": False, "reason": "no eligible supplier for any item"}
|
|
|
|
order.status = "pending_payment"
|
|
db.commit()
|
|
return {
|
|
"routed": True,
|
|
"supplier_id": str(supplier_id),
|
|
"profit": float(order.profit),
|
|
"supplier_cost": float(order.supplier_cost),
|
|
"shipping_cost": float(order.shipping_cost),
|
|
"fees": float(order.fees),
|
|
}
|