fix error in gitignore
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""Backend-authoritative pricing engine.
|
||||
|
||||
Produces a full, explainable breakdown and is always the source of truth —
|
||||
the frontend may display estimates but must never be trusted for financial
|
||||
values.
|
||||
"""
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
CHARGE_TYPES = {"fixed", "per_kg", "per_unit", "percentage", "tiered", "fee"}
|
||||
|
||||
|
||||
class PricingEngine:
|
||||
def calculate(self, weight_kg, selected_steps, currency: str = "IRR") -> dict:
|
||||
weight = Decimal(str(weight_kg))
|
||||
lines = []
|
||||
subtotal = Decimal("0")
|
||||
|
||||
# Pass 1 — charge rules.
|
||||
for step in selected_steps:
|
||||
for rule in step.pricing_rules.filter(active=True, rule_type__in=CHARGE_TYPES).order_by("priority"):
|
||||
line = self._apply_charge_rule(rule, weight)
|
||||
if line is None:
|
||||
continue
|
||||
lines.append(line)
|
||||
subtotal += Decimal(line["amount"])
|
||||
|
||||
# Pass 2 — discounts (fixed or percentage of subtotal).
|
||||
discounts = Decimal("0")
|
||||
for step in selected_steps:
|
||||
for rule in step.pricing_rules.filter(active=True, rule_type="discount"):
|
||||
amt = Decimal(str(rule.amount))
|
||||
if (rule.config or {}).get("percentage"):
|
||||
amt = subtotal * amt / Decimal("100")
|
||||
discounts += amt
|
||||
lines.append(
|
||||
self._line(rule, weight, "discount", Decimal("0"), amt)
|
||||
)
|
||||
|
||||
# Pass 3 — taxes on (subtotal - discounts).
|
||||
taxes = Decimal("0")
|
||||
taxable = subtotal - discounts
|
||||
for step in selected_steps:
|
||||
for rule in step.pricing_rules.filter(active=True, rule_type="tax"):
|
||||
amt = taxable * Decimal(str(rule.amount)) / Decimal("100")
|
||||
taxes += amt
|
||||
lines.append(self._line(rule, weight, "tax", Decimal("0"), amt))
|
||||
|
||||
total = subtotal - discounts + taxes
|
||||
if total < 0:
|
||||
total = Decimal("0")
|
||||
|
||||
return {
|
||||
"currency": currency,
|
||||
"lines": lines,
|
||||
"subtotal": str(self._q(subtotal)),
|
||||
"discounts": str(self._q(discounts)),
|
||||
"taxes": str(self._q(taxes)),
|
||||
"total": str(self._q(total)),
|
||||
}
|
||||
|
||||
def _q(self, v: Decimal) -> Decimal:
|
||||
return v.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
|
||||
def _line(self, rule, weight, kind, unit_price, amount, quantity=None) -> dict:
|
||||
return {
|
||||
"step_key": rule.step.key,
|
||||
"rule": rule.name,
|
||||
"kind": kind,
|
||||
"quantity": str(quantity if quantity is not None else weight),
|
||||
"unit_price": str(self._q(unit_price)),
|
||||
"amount": str(self._q(amount)),
|
||||
}
|
||||
|
||||
def _apply_charge_rule(self, rule, weight):
|
||||
kind = rule.rule_type
|
||||
cfg = rule.config or {}
|
||||
amount = Decimal(str(rule.amount))
|
||||
unit_price = Decimal("0")
|
||||
quantity = weight
|
||||
if kind == "fixed":
|
||||
unit_price = amount
|
||||
elif kind == "per_kg":
|
||||
unit_price = amount
|
||||
amount = amount * weight
|
||||
elif kind == "per_unit":
|
||||
units = Decimal(str(cfg.get("units", 1)))
|
||||
unit_price = amount
|
||||
amount = amount * units
|
||||
quantity = units
|
||||
elif kind == "percentage":
|
||||
base = Decimal(str(cfg.get("base", 0)))
|
||||
unit_price = amount
|
||||
amount = base * amount / Decimal("100")
|
||||
elif kind == "tiered":
|
||||
unit_price = self._tier_unit_price(cfg.get("tiers", []), weight)
|
||||
amount = unit_price * weight
|
||||
elif kind == "fee":
|
||||
unit_price = Decimal("0")
|
||||
else:
|
||||
return None
|
||||
|
||||
min_c = cfg.get("min_charge")
|
||||
max_c = cfg.get("max_charge")
|
||||
if min_c is not None and amount < Decimal(str(min_c)):
|
||||
amount = Decimal(str(min_c))
|
||||
if max_c is not None and amount > Decimal(str(max_c)):
|
||||
amount = Decimal(str(max_c))
|
||||
|
||||
return self._line(rule, weight, kind, unit_price, amount, quantity)
|
||||
|
||||
def _tier_unit_price(self, tiers, weight) -> Decimal:
|
||||
best = Decimal("0")
|
||||
for tier in tiers or []:
|
||||
low = Decimal(str(tier.get("from", 0)))
|
||||
if weight >= low:
|
||||
best = Decimal(str(tier.get("price", 0)))
|
||||
return best
|
||||
Reference in New Issue
Block a user