Add Polaris FastAPI backend: models/schemas, pricing+scoring+order router, importer, Celery tasks, API routers, seed script
This commit is contained in:
87
backend/app/engines/pricing.py
Normal file
87
backend/app/engines/pricing.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Pricing engine.
|
||||
|
||||
Applies active `price_rules` (cost -> markup bands) and computes net profit:
|
||||
|
||||
retail = cost * (1 + markup_pct/100) (floored at cost + shipping + fees)
|
||||
net profit = retail - cost - shipping - fees
|
||||
margin_pct = profit / retail * 100
|
||||
|
||||
`min_price` (product floor) is respected — retail is never below it.
|
||||
"""
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from app.models import PriceRule
|
||||
|
||||
CENT = Decimal("0.01")
|
||||
|
||||
|
||||
def _q(value) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(value or 0))
|
||||
except Exception:
|
||||
return Decimal("0")
|
||||
|
||||
|
||||
def get_active_rules(db):
|
||||
return (
|
||||
db.query(PriceRule)
|
||||
.filter(PriceRule.active.is_(True))
|
||||
.order_by(PriceRule.min_cost)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def find_rule(db, cost):
|
||||
cost = _q(cost)
|
||||
for rule in get_active_rules(db):
|
||||
if cost >= _q(rule.min_cost) and (rule.max_cost is None or cost < _q(rule.max_cost)):
|
||||
return rule
|
||||
return None
|
||||
|
||||
|
||||
def calculate_price(db, cost, shipping=0, fees=0, min_price=None):
|
||||
"""Return dict(retail, profit, margin, margin_pct, markup_pct, rule_id, ...) or None.
|
||||
|
||||
None means no active rule applies — callers must NOT publish such products.
|
||||
"""
|
||||
cost = _q(cost)
|
||||
shipping = _q(shipping)
|
||||
fees = _q(fees)
|
||||
rule = find_rule(db, cost)
|
||||
if rule is None:
|
||||
return None
|
||||
|
||||
markup_pct = _q(rule.markup_pct)
|
||||
retail = (cost * (1 + markup_pct / 100)).quantize(CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
# floor: cost + shipping + fees, raised by min_margin_pct if configured
|
||||
floor = (cost + shipping + fees).quantize(CENT, rounding=ROUND_HALF_UP)
|
||||
min_margin_pct = _q(rule.min_margin_pct)
|
||||
if min_margin_pct > 0:
|
||||
floor = max(floor, (floor + retail * min_margin_pct / 100).quantize(CENT, rounding=ROUND_HALF_UP))
|
||||
if retail < floor:
|
||||
retail = floor
|
||||
|
||||
# never below the product's minimum price
|
||||
if min_price is not None and retail < _q(min_price):
|
||||
retail = _q(min_price).quantize(CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
profit = (retail - cost - shipping - fees).quantize(CENT, rounding=ROUND_HALF_UP)
|
||||
margin_pct = (profit / retail * 100).quantize(Decimal("0.01")) if retail > 0 else Decimal("0")
|
||||
|
||||
return {
|
||||
"cost": cost,
|
||||
"shipping": shipping,
|
||||
"fees": fees,
|
||||
"retail": retail,
|
||||
"profit": profit,
|
||||
"margin": profit,
|
||||
"margin_pct": float(margin_pct),
|
||||
"markup_pct": float(markup_pct),
|
||||
"rule_id": rule.id,
|
||||
}
|
||||
|
||||
|
||||
def estimate_net_profit(retail, cost, shipping=0, fees=0):
|
||||
"""Net profit = retail - (cost + shipping + fees)."""
|
||||
return (_q(retail) - _q(cost) - _q(shipping) - _q(fees)).quantize(CENT, rounding=ROUND_HALF_UP)
|
||||
Reference in New Issue
Block a user