feat(products): compute price tiers from objective price/use
This commit is contained in:
parent
c5ea38880c
commit
83ba4cc5c0
13 changed files with 664 additions and 48 deletions
177
backend/tests/test_products_pricing.py
Normal file
177
backend/tests/test_products_pricing.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import uuid
|
||||
|
||||
from innercontext.api import products as products_api
|
||||
from innercontext.models import Product
|
||||
from innercontext.models.enums import DayTime, ProductCategory
|
||||
|
||||
|
||||
def _product(
|
||||
*, category: ProductCategory, price_amount: float, size_ml: float
|
||||
) -> Product:
|
||||
return Product(
|
||||
id=uuid.uuid4(),
|
||||
name=f"{category}-{price_amount}",
|
||||
brand="Brand",
|
||||
category=category,
|
||||
recommended_time=DayTime.BOTH,
|
||||
leave_on=True,
|
||||
price_amount=price_amount,
|
||||
price_currency="PLN",
|
||||
size_ml=size_ml,
|
||||
)
|
||||
|
||||
|
||||
def test_compute_pricing_outputs_groups_by_category(monkeypatch):
|
||||
monkeypatch.setattr(products_api, "convert_to_pln", lambda amount, currency: amount)
|
||||
|
||||
serums = [
|
||||
_product(category=ProductCategory.SERUM, price_amount=float(i), size_ml=35.0)
|
||||
for i in range(10, 90, 10)
|
||||
]
|
||||
cleansers = [
|
||||
_product(
|
||||
category=ProductCategory.CLEANSER, price_amount=float(i), size_ml=200.0
|
||||
)
|
||||
for i in range(40, 120, 10)
|
||||
]
|
||||
outputs = products_api._compute_pricing_outputs(serums + cleansers)
|
||||
|
||||
serum_tiers = [outputs[p.id][0] for p in serums]
|
||||
cleanser_tiers = [outputs[p.id][0] for p in cleansers]
|
||||
|
||||
assert serum_tiers[0] == "budget"
|
||||
assert serum_tiers[-1] == "luxury"
|
||||
assert cleanser_tiers[0] == "budget"
|
||||
assert cleanser_tiers[-1] == "luxury"
|
||||
|
||||
|
||||
def test_price_tier_is_null_when_not_enough_products(client, monkeypatch):
|
||||
monkeypatch.setattr(products_api, "convert_to_pln", lambda amount, currency: amount)
|
||||
|
||||
base = {
|
||||
"brand": "B",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
"size_ml": 35.0,
|
||||
"price_currency": "PLN",
|
||||
}
|
||||
for i in range(7):
|
||||
response = client.post(
|
||||
"/products",
|
||||
json={
|
||||
**base,
|
||||
"name": f"Serum {i}",
|
||||
"category": "serum",
|
||||
"price_amount": 30 + i,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
products = client.get("/products").json()
|
||||
assert len(products) == 7
|
||||
assert all(p["price_tier"] is None for p in products)
|
||||
assert all(p["price_per_use_pln"] is not None for p in products)
|
||||
|
||||
|
||||
def test_price_tier_is_computed_on_list(client, monkeypatch):
|
||||
monkeypatch.setattr(products_api, "convert_to_pln", lambda amount, currency: amount)
|
||||
|
||||
base = {
|
||||
"brand": "B",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
"size_ml": 35.0,
|
||||
"price_currency": "PLN",
|
||||
"category": "serum",
|
||||
}
|
||||
for i in range(8):
|
||||
response = client.post(
|
||||
"/products",
|
||||
json={**base, "name": f"Serum {i}", "price_amount": 20 + i * 10},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
products = client.get("/products").json()
|
||||
assert len(products) == 8
|
||||
assert any(p["price_tier"] == "budget" for p in products)
|
||||
assert any(p["price_tier"] == "luxury" for p in products)
|
||||
|
||||
|
||||
def test_price_tier_uses_fallback_for_medium_categories(client, monkeypatch):
|
||||
monkeypatch.setattr(products_api, "convert_to_pln", lambda amount, currency: amount)
|
||||
|
||||
serum_base = {
|
||||
"brand": "B",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
"size_ml": 35.0,
|
||||
"price_currency": "PLN",
|
||||
"category": "serum",
|
||||
}
|
||||
for i in range(8):
|
||||
response = client.post(
|
||||
"/products",
|
||||
json={**serum_base, "name": f"Serum {i}", "price_amount": 20 + i * 5},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
toner_base = {
|
||||
"brand": "B",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
"size_ml": 100.0,
|
||||
"price_currency": "PLN",
|
||||
"category": "toner",
|
||||
}
|
||||
for i in range(5):
|
||||
response = client.post(
|
||||
"/products",
|
||||
json={**toner_base, "name": f"Toner {i}", "price_amount": 10 + i * 5},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
products = client.get("/products?category=toner").json()
|
||||
assert len(products) == 5
|
||||
assert all(p["price_tier"] is not None for p in products)
|
||||
assert all(p["price_tier_source"] == "fallback" for p in products)
|
||||
|
||||
|
||||
def test_price_tier_stays_null_for_tiny_categories_even_with_fallback_pool(
|
||||
client, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(products_api, "convert_to_pln", lambda amount, currency: amount)
|
||||
|
||||
serum_base = {
|
||||
"brand": "B",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
"size_ml": 35.0,
|
||||
"price_currency": "PLN",
|
||||
"category": "serum",
|
||||
}
|
||||
for i in range(8):
|
||||
response = client.post(
|
||||
"/products",
|
||||
json={**serum_base, "name": f"Serum {i}", "price_amount": 20 + i * 5},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
oil_base = {
|
||||
"brand": "B",
|
||||
"recommended_time": "both",
|
||||
"leave_on": True,
|
||||
"size_ml": 30.0,
|
||||
"price_currency": "PLN",
|
||||
"category": "oil",
|
||||
}
|
||||
for i in range(3):
|
||||
response = client.post(
|
||||
"/products",
|
||||
json={**oil_base, "name": f"Oil {i}", "price_amount": 30 + i * 10},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
oils = client.get("/products?category=oil").json()
|
||||
assert len(oils) == 3
|
||||
assert all(p["price_tier"] is None for p in oils)
|
||||
assert all(p["price_tier_source"] == "insufficient_data" for p in oils)
|
||||
Loading…
Add table
Add a link
Reference in a new issue