Add Polaris FastAPI backend: models/schemas, pricing+scoring+order router, importer, Celery tasks, API routers, seed script
This commit is contained in:
70
backend/app/engines/scoring.py
Normal file
70
backend/app/engines/scoring.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Supplier scoring.
|
||||
|
||||
Weighted score (0-100):
|
||||
40% price — cheaper supplier cost vs. the eligible pool
|
||||
20% inventory — stock sufficiency for the requested qty
|
||||
15% speed — shipping_time ('3-5 days' -> avg days)
|
||||
10% fulfillment — supplier_performance.fulfillment_rate
|
||||
10% returns — inverse of supplier_performance.return_rate
|
||||
5% history — reliability_score blended with stock_accuracy
|
||||
"""
|
||||
import re
|
||||
|
||||
|
||||
def _num(value, default=0.0):
|
||||
try:
|
||||
return float(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _shipping_days(sp):
|
||||
text = (sp.shipping_time or "").strip()
|
||||
numbers = re.findall(r"\d+", text)
|
||||
if not numbers:
|
||||
return 5.0
|
||||
return float(sum(int(n) for n in numbers)) / len(numbers)
|
||||
|
||||
|
||||
def score_supplier(db, supplier, sp, qty, price_pool=None):
|
||||
perf = supplier.performance
|
||||
|
||||
# 1) price (40%) — cheaper than pool average is better
|
||||
cost = _num(sp.supplier_cost)
|
||||
if price_pool and price_pool.get("avg_cost"):
|
||||
avg = price_pool["avg_cost"]
|
||||
price_score = max(0.0, 100.0 - (cost / avg - 1.0) * 100.0)
|
||||
else:
|
||||
price_score = 50.0
|
||||
|
||||
# 2) inventory (20%) — can we fill the quantity?
|
||||
inventory = int(sp.supplier_inventory or 0)
|
||||
inv_score = 100.0 if inventory >= qty else max(0.0, (inventory / qty) * 100.0)
|
||||
|
||||
# 3) speed (15%) — fewer shipping days is better
|
||||
days = _shipping_days(sp)
|
||||
speed_score = max(0.0, 100.0 - days * 12.0)
|
||||
|
||||
# 4) fulfillment (10%)
|
||||
fulfillment = 100.0 if perf is None else _num(perf.fulfillment_rate, 100.0)
|
||||
|
||||
# 5) returns (10%) — 0% returns = 100 points, 20% returns = 0 points
|
||||
return_rate = 0.0 if perf is None else _num(perf.return_rate, 0.0)
|
||||
returns_score = max(0.0, 100.0 - return_rate * 5.0)
|
||||
|
||||
# 6) history (5%) — reliability blended with stock accuracy
|
||||
reliability = _num(supplier.reliability_score, 50.0)
|
||||
if perf is None:
|
||||
history_score = reliability
|
||||
else:
|
||||
history_score = 0.5 * reliability + 0.5 * _num(perf.stock_accuracy, 100.0)
|
||||
|
||||
total = (
|
||||
0.40 * price_score
|
||||
+ 0.20 * inv_score
|
||||
+ 0.15 * speed_score
|
||||
+ 0.10 * fulfillment
|
||||
+ 0.10 * returns_score
|
||||
+ 0.05 * history_score
|
||||
)
|
||||
return round(min(100.0, max(0.0, total)), 2)
|
||||
Reference in New Issue
Block a user