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"}