Files
data-entry-app/backend/tests/test_ordering_pricing.py
T

157 lines
5.3 KiB
Python
Raw Normal View History

2026-06-11 23:56:02 +12:00
"""Unit tests for the backend-owned ordering pricing engine."""
from __future__ import annotations
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.db.session import Base
from app.models.ordering import (
CatalogueProduct,
CustomerPriceAssignment,
CustomerProductPrice,
PriceList,
PriceListItem,
PriceTier,
)
from app.services.ordering_pricing import resolve_price
CUSTOMER_ID = 1
TENANT = "test-tenant"
def _session() -> Session:
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
return sessionmaker(bind=engine, expire_on_commit=False)()
def _product(db: Session, **overrides) -> CatalogueProduct:
values = {
"tenant_id": TENANT,
"name": "Maize 1T",
"sku": "MAIZE-1T",
"category": "grains",
"base_price": 100.0,
"min_order_quantity": 1.0,
"requires_quote": False,
}
values.update(overrides)
product = CatalogueProduct(**values)
db.add(product)
db.flush()
return product
def test_base_price_with_no_rules():
db = _session()
product = _product(db)
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=2)
assert res.unit_price == 100.0
assert res.price_source == "base"
assert res.requires_quote is False
assert res.line_total(2) == 200.0
def test_customer_discount_applies_to_base():
db = _session()
product = _product(db)
db.add(CustomerPriceAssignment(tenant_id=TENANT, client_account_id=CUSTOMER_ID, discount_percent=10.0))
db.flush()
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=1)
assert res.unit_price == 90.0
assert res.price_source == "base"
assert res.discount_percent == 10.0
def test_price_list_overrides_base():
db = _session()
product = _product(db)
pl = PriceList(tenant_id=TENANT, code="WHL", name="Wholesale")
db.add(pl)
db.flush()
db.add(PriceListItem(tenant_id=TENANT, price_list_id=pl.id, product_id=product.id, unit_price=80.0))
db.add(CustomerPriceAssignment(tenant_id=TENANT, client_account_id=CUSTOMER_ID, price_list_id=pl.id, discount_percent=10.0))
db.flush()
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=1)
# Price list wins over base+discount.
assert res.unit_price == 80.0
assert res.price_source == "price_list"
def test_customer_fixed_price_wins_over_price_list():
db = _session()
product = _product(db)
pl = PriceList(tenant_id=TENANT, code="WHL", name="Wholesale")
db.add(pl)
db.flush()
db.add(PriceListItem(tenant_id=TENANT, price_list_id=pl.id, product_id=product.id, unit_price=80.0))
db.add(CustomerPriceAssignment(tenant_id=TENANT, client_account_id=CUSTOMER_ID, price_list_id=pl.id))
db.add(
CustomerProductPrice(
tenant_id=TENANT, client_account_id=CUSTOMER_ID, product_id=product.id, unit_price=72.5, rule_type="contract"
)
)
db.flush()
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=1)
assert res.unit_price == 72.5
assert res.price_source == "contract"
def test_quantity_tier_wins_when_threshold_met():
db = _session()
product = _product(db)
cpp = CustomerProductPrice(
tenant_id=TENANT, client_account_id=CUSTOMER_ID, product_id=product.id, unit_price=90.0, rule_type="fixed"
)
db.add(cpp)
db.flush()
db.add(PriceTier(tenant_id=TENANT, customer_product_price_id=cpp.id, min_quantity=10, unit_price=85.0))
db.add(PriceTier(tenant_id=TENANT, customer_product_price_id=cpp.id, min_quantity=50, unit_price=80.0))
db.flush()
# Below first tier → flat fixed price.
assert resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=5).unit_price == 90.0
# Meets first tier.
mid = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=10)
assert mid.unit_price == 85.0
assert mid.price_source == "tiered"
# Meets highest tier.
assert resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=60).unit_price == 80.0
def test_product_requires_quote():
db = _session()
product = _product(db, requires_quote=True)
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=1)
assert res.requires_quote is True
assert res.unit_price is None
assert res.price_source == "quote"
assert res.line_total(5) is None
def test_customer_quote_rule_forces_quote():
db = _session()
product = _product(db)
db.add(
CustomerProductPrice(
tenant_id=TENANT, client_account_id=CUSTOMER_ID, product_id=product.id, unit_price=None, rule_type="quote"
)
)
db.flush()
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=1)
assert res.requires_quote is True
assert res.price_source == "quote"
def test_no_price_falls_back_to_quote():
db = _session()
product = _product(db, base_price=None)
res = resolve_price(db, client_account_id=CUSTOMER_ID, product=product, quantity=1)
assert res.requires_quote is True
assert res.price_source == "quote"