Run formatting tools on Phase 1 changes: - black (code formatter) - isort (import sorter) - ruff (linter) All linting checks pass.
206 lines
6.1 KiB
Python
206 lines
6.1 KiB
Python
import uuid
|
|
|
|
from sqlmodel import select
|
|
|
|
from innercontext.api import products as products_api
|
|
from innercontext.models import PricingRecalcJob, Product
|
|
from innercontext.models.enums import DayTime, ProductCategory
|
|
from innercontext.services.pricing_jobs import process_one_pending_pricing_job
|
|
|
|
|
|
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, session, 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
|
|
|
|
assert process_one_pending_pricing_job(session)
|
|
|
|
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_by_worker(client, session, 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
|
|
|
|
assert process_one_pending_pricing_job(session)
|
|
|
|
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, session, 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
|
|
|
|
assert process_one_pending_pricing_job(session)
|
|
|
|
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, session, 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
|
|
|
|
assert process_one_pending_pricing_job(session)
|
|
|
|
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)
|
|
|
|
|
|
def test_product_write_enqueues_pricing_job(client, session):
|
|
response = client.post(
|
|
"/products",
|
|
json={
|
|
"name": "Serum X",
|
|
"brand": "B",
|
|
"category": "serum",
|
|
"recommended_time": "both",
|
|
"leave_on": True,
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
jobs = session.exec(select(PricingRecalcJob)).all()
|
|
assert len(jobs) == 1
|
|
assert jobs[0].status in {"pending", "running", "succeeded"}
|