Corp/backend/tests/commerce/test_services.py

181 lines
5.5 KiB
Python

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