Files
reseller-platform/backend/app/engines/order_router.py

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),
}