v0.1.14 - b2b portal
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user