Corp/backend/tests/commerce/test_api.py
2026-08-18 09:28:58 +03:00

277 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from decimal import Decimal
import pytest
from django.core.files.base import ContentFile
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from commerce.models import (
Cost,
Product,
ProductBinding,
ProductImage,
Sale,
SaleLine,
SourceSystem,
StockSnapshot,
)
pytestmark = pytest.mark.django_db
def test_commerce_api_requires_authentication(api_client):
response = api_client.get(reverse("api:v1:commerce:product-list"))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_employee_can_read_but_cannot_create_product(api_client, user_factory):
user = user_factory()
Product.objects.create(sku="FISHKA", name="Фишка")
api_client.force_authenticate(user=user)
list_response = api_client.get(reverse("api:v1:commerce:product-list"))
create_response = api_client.post(
reverse("api:v1:commerce:product-list"),
{"sku": "NEW", "name": "Новый товар", "kind": "product"},
format="json",
)
assert list_response.status_code == status.HTTP_200_OK
assert list_response.data["count"] == 1
assert create_response.status_code == status.HTTP_403_FORBIDDEN
def test_product_search_matches_unicode_substring_without_case(api_client, user_factory):
user = user_factory()
Product.objects.create(sku="SEA-001", name="Автомат «Бродяги морей»")
Product.objects.create(sku="OTHER-001", name="Другой товар")
api_client.force_authenticate(user=user)
response = api_client.get(
reverse("api:v1:commerce:product-list"),
{"search": "бродяг"},
)
assert response.status_code == status.HTTP_200_OK
assert response.data["count"] == 1
assert response.data["results"][0]["name"] == "Автомат «Бродяги морей»"
def test_staff_can_create_product(api_client, user_factory):
user = user_factory(staff=True)
api_client.force_authenticate(user=user)
response = api_client.post(
reverse("api:v1:commerce:product-list"),
{"sku": "NEW", "name": "Новый товар", "kind": "product"},
format="json",
)
assert response.status_code == status.HTTP_201_CREATED
assert Product.objects.filter(sku="NEW").exists()
def test_product_api_exposes_local_image_urls(api_client, user_factory):
user = user_factory()
source = SourceSystem.objects.create(code="moysklad", name="МойСклад")
product = Product.objects.create(sku="PHOTO", name="Товар с фото")
image = ProductImage(
product=product,
source=source,
external_id="image-1",
filename="photo.jpg",
is_primary=True,
)
image.file.save("image-1.jpg", ContentFile(b"image"), save=False)
image.thumbnail.save("image-1.webp", ContentFile(b"thumbnail"), save=False)
image.save()
api_client.force_authenticate(user=user)
response = api_client.get(reverse("api:v1:commerce:product-list"))
assert response.status_code == status.HTTP_200_OK
serialized = response.data["results"][0]["images"][0]
assert serialized["is_primary"] is True
assert serialized["url"].endswith(".jpg")
assert serialized["thumbnail_url"].endswith(".webp")
def test_deleting_source_in_use_returns_conflict(api_client, user_factory):
user = user_factory(staff=True)
source = SourceSystem.objects.create(code="moysklad", name="МойСклад")
product = Product.objects.create(sku="MS-001", name="Товар")
ProductBinding.objects.create(
product=product,
source=source,
external_id="external-1",
)
api_client.force_authenticate(user=user)
response = api_client.delete(
reverse("api:v1:commerce:source-detail", args=[source.pk])
)
assert response.status_code == status.HTTP_409_CONFLICT
assert response.data["code"] == "source_in_use"
assert response.data["protected_count"] == 1
assert SourceSystem.objects.filter(pk=source.pk).exists()
def test_staff_can_manage_normalized_commerce_facts(api_client, user_factory):
user = user_factory(staff=True)
source = SourceSystem.objects.create(code="moysklad", name="МойСклад")
product = Product.objects.create(sku="FISHKA", name="Фишка")
api_client.force_authenticate(user=user)
sale_response = api_client.post(
reverse("api:v1:commerce:sale-list"),
{
"source": source.pk,
"external_id": "order-1",
"occurred_at": timezone.now().isoformat(),
},
format="json",
)
assert sale_response.status_code == status.HTTP_201_CREATED
line_response = api_client.post(
reverse("api:v1:commerce:saleline-list"),
{
"sale": sale_response.data["id"],
"product": product.pk,
"external_id": "line-1",
"quantity": "2",
"gross_amount": "500.00",
"discount_amount": "50.00",
"cost_amount": "200.00",
},
format="json",
)
assert line_response.status_code == status.HTTP_201_CREATED
cost_response = api_client.post(
reverse("api:v1:commerce:cost-list"),
{
"source": source.pk,
"product": product.pk,
"cost_type": "commission",
"amount": "25.00",
"occurred_at": timezone.now().isoformat(),
},
format="json",
)
assert cost_response.status_code == status.HTTP_201_CREATED, cost_response.data
stock_response = api_client.post(
reverse("api:v1:commerce:stocksnapshot-list"),
{
"source": source.pk,
"product": product.pk,
"warehouse_external_id": "main",
"quantity": "10",
"reserved": "2",
"captured_at": timezone.now().isoformat(),
},
format="json",
)
assert stock_response.status_code == status.HTTP_201_CREATED
assert SaleLine.objects.filter(pk=line_response.data["id"]).exists()
assert Cost.objects.filter(pk=cost_response.data["id"]).exists()
assert StockSnapshot.objects.filter(pk=stock_response.data["id"]).exists()
def test_management_searches_costs_and_stock_by_product_or_warehouse(
api_client, user_factory
):
user = user_factory(staff=True)
source = SourceSystem.objects.create(code="moysklad", name="МойСклад")
product = Product.objects.create(sku="SEARCH-42", name="Товар для поиска")
Cost.objects.create(
source=source,
product=product,
external_id="cost-42",
channel="marketplace",
cost_type="commission",
amount=Decimal("10"),
occurred_at=timezone.now(),
)
StockSnapshot.objects.create(
source=source,
product=product,
warehouse_external_id="warehouse-42",
warehouse_name="Главный склад",
quantity=Decimal("5"),
captured_at=timezone.now(),
)
api_client.force_authenticate(user=user)
cost_response = api_client.get(
reverse("api:v1:commerce:cost-list"), {"search": "SEARCH-42"}
)
stock_response = api_client.get(
reverse("api:v1:commerce:stocksnapshot-list"),
{"search": "Главный склад"},
)
assert cost_response.status_code == status.HTTP_200_OK
assert cost_response.data["count"] == 1
assert stock_response.status_code == status.HTTP_200_OK
assert stock_response.data["count"] == 1
def test_product_analytics_endpoint(api_client, user_factory):
user = user_factory()
source = SourceSystem.objects.create(code="insales", name="InSales")
product = Product.objects.create(sku="FISHKA", name="Фишка")
sale = Sale.objects.create(
source=source,
external_id="order-1",
occurred_at=timezone.now(),
channel="site",
)
SaleLine.objects.create(
sale=sale,
product=product,
external_id="line-1",
quantity=Decimal("1"),
gross_amount=Decimal("500"),
cost_amount=Decimal("200"),
)
api_client.force_authenticate(user=user)
response = api_client.get(
reverse("api:v1:commerce:product-analytics", args=[product.pk])
)
assert response.status_code == status.HTTP_200_OK
assert response.data["product_id"] == product.pk
assert response.data["monetary"][0]["net_revenue"] == Decimal("500")
assert response.data["monetary"][0]["contribution_margin"] == Decimal("300")
def test_staff_can_merge_imported_product_into_canonical(api_client, user_factory):
user = user_factory(staff=True)
source = SourceSystem.objects.create(code="moysklad", name="МойСклад")
target = Product.objects.create(sku="CANONICAL", name="Основной товар")
imported = Product.objects.create(sku="MS-001", name="Импортированный товар")
binding = imported.bindings.create(
source=source,
external_id="external-1",
external_sku="MS-001",
)
api_client.force_authenticate(user=user)
response = api_client.post(
reverse("api:v1:commerce:product-merge", args=[target.pk]),
{"source_product_ids": [str(imported.pk)]},
format="json",
)
assert response.status_code == status.HTTP_200_OK
assert response.data["merged_products"] == 1
assert response.data["moved_bindings"] == 1
binding.refresh_from_db()
assert binding.product == target
assert not Product.objects.filter(pk=imported.pk).exists()