from datetime import timedelta from decimal import Decimal import pytest from django.utils import timezone from commerce.models import ( Cost, CostType, Product, ProductBinding, Sale, SaleEventType, SaleLine, SourceSystem, StockSnapshot, ) from commerce.services import get_product_metrics, ingest_source_record, match_product pytestmark = pytest.mark.django_db def test_ingestion_is_idempotent_and_detects_payload_changes(): source = SourceSystem.objects.create(code="moysklad", name="МойСклад") first = ingest_source_record( source=source, entity_type="product", external_id="product-1", payload={"name": "Фишка", "stock": 10}, ) repeated = ingest_source_record( source=source, entity_type="product", external_id="product-1", payload={"stock": 10, "name": "Фишка"}, ) changed = ingest_source_record( source=source, entity_type="product", external_id="product-1", payload={"name": "Фишка", "stock": 8}, ) assert first.created is True assert repeated.created is False assert repeated.changed is False assert changed.changed is True assert changed.record.pk == first.record.pk assert changed.record.payload["stock"] == 8 def test_product_matching_prefers_binding_and_rejects_ambiguous_sku(): source = SourceSystem.objects.create(code="retailcrm", name="RetailCRM") first = Product.objects.create(sku="FISHKA-RED", name="Фишка красная") second = Product.objects.create(sku="FISHKA-BLUE", name="Фишка синяя") ProductBinding.objects.create( product=first, source=source, external_id="offer-1", external_sku="shared-sku", ) ProductBinding.objects.create( product=second, source=source, external_id="offer-2", external_sku="shared-sku", ) explicit = match_product(source=source, external_id="offer-1") ambiguous = match_product(source=source, external_sku="shared-sku") canonical = match_product(source=source, external_sku="fishka-blue") assert explicit.product == first assert explicit.method == "external_id" assert ambiguous.matched is False assert canonical.product == second assert canonical.method == "canonical_sku" def test_product_metrics_include_returns_costs_and_latest_stock(): now = timezone.now() source = SourceSystem.objects.create(code="moysklad", name="МойСклад") ads = SourceSystem.objects.create(code="ads", name="Реклама") product = Product.objects.create(sku="FISHKA", name="Фишка") sale = Sale.objects.create( source=source, external_id="sale-1", event_type=SaleEventType.SALE, channel="site", occurred_at=now - timedelta(days=2), ) sale_line = SaleLine.objects.create( sale=sale, product=product, external_id="line-1", quantity=Decimal("2"), gross_amount=Decimal("1000"), discount_amount=Decimal("100"), cost_amount=Decimal("400"), ) returned = Sale.objects.create( source=source, external_id="return-1", event_type=SaleEventType.RETURN, channel="site", occurred_at=now - timedelta(days=1), ) SaleLine.objects.create( sale=returned, product=product, external_id="return-line-1", quantity=Decimal("1"), gross_amount=Decimal("200"), cost_amount=Decimal("80"), ) Cost.objects.create( source=source, product=product, external_id="commission-1", cost_type=CostType.COMMISSION, amount=Decimal("50"), occurred_at=now, ) Cost.objects.create( source=ads, sale_line=sale_line, external_id="ad-1", cost_type=CostType.ADVERTISING, amount=Decimal("20"), occurred_at=now, ) StockSnapshot.objects.create( product=product, source=source, warehouse_external_id="main", quantity=Decimal("10"), reserved=Decimal("2"), captured_at=now - timedelta(hours=2), ) StockSnapshot.objects.create( product=product, source=source, warehouse_external_id="main", quantity=Decimal("7"), reserved=Decimal("1"), captured_at=now, ) StockSnapshot.objects.create( product=product, source=source, warehouse_external_id="reserve", quantity=Decimal("3"), captured_at=now, ) result = get_product_metrics(product) rub = result["monetary"][0] assert rub["currency"] == "RUB" assert rub["quantity"] == Decimal("1") assert rub["gross_revenue"] == Decimal("800") assert rub["discounts"] == Decimal("100") assert rub["net_revenue"] == Decimal("700") assert rub["cost_of_goods"] == Decimal("320") assert rub["additional_costs"] == Decimal("70") assert rub["contribution_margin"] == Decimal("310") channels = {row["channel"]: row for row in result["by_channel"]} assert channels["site"]["net_revenue"] == Decimal("700") assert channels["site"]["additional_costs"] == Decimal("20") assert channels["site"]["contribution_margin"] == Decimal("360") assert channels["unattributed"]["additional_costs"] == Decimal("50") assert len(result["stock"]) == 2 main = next( item for item in result["stock"] if item["warehouse_external_id"] == "main" ) assert main["quantity"] == Decimal("7") assert main["available"] == Decimal("6")