Corp/backend/tests/commerce/test_wildberries.py
2026-09-07 19:43:48 +03:00

323 lines
10 KiB
Python

from decimal import Decimal
from unittest.mock import Mock, patch
import httpx
import pytest
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from commerce.integrations.wildberries import WildberriesClient
from commerce.models import (
Cost,
CostType,
IntegrationConnection,
IntegrationStatus,
Product,
ProductBinding,
Sale,
SaleEventType,
SourceRecord,
SourceSystem,
)
from commerce.services.credentials import encrypt_credentials
from commerce.services.wildberries_sync import (
normalize_wildberries_finance_row,
sync_wildberries_connection,
)
from commerce.tasks import sync_moysklad_run
pytestmark = pytest.mark.django_db
def test_wildberries_client_uses_bare_token_and_current_finance_endpoint(settings):
settings.WILDBERRIES_FINANCE_API_BASE_URL = "https://finance.example"
requests = []
def handler(request):
requests.append(request)
return httpx.Response(204, request=request)
client = WildberriesClient(credentials={"token": " Bearer wb-token "})
client.client.close()
client.client = httpx.Client(
headers=client.client.headers,
transport=httpx.MockTransport(handler),
)
try:
assert (
client.financial_report(date_from="2026-08-01", date_to="2026-08-18") == []
)
finally:
client.close()
assert requests[0].headers["Authorization"] == "wb-token"
assert requests[0].url.path == "/api/finance/v1/sales-reports/detailed"
assert requests[0].read()
assert '"rrdId":0' in requests[0].content.decode().replace(" ", "")
def test_wildberries_finance_ping_validates_finance_category(settings):
settings.WILDBERRIES_FINANCE_API_BASE_URL = "https://finance.example"
def handler(request):
return httpx.Response(
200, json={"TS": "2026-08-18", "Status": "OK"}, request=request
)
client = WildberriesClient(credentials={"token": "wb-token"})
client.client.close()
client.client = httpx.Client(
headers=client.client.headers,
transport=httpx.MockTransport(handler),
)
try:
assert client.finance_ping() is None
finally:
client.close()
def test_wildberries_finance_row_creates_sale_binding_and_expenses():
source = SourceSystem.objects.create(code="wildberries", name="Wildberries")
connection = IntegrationConnection.objects.create(
source=source,
provider="wildberries",
encrypted_credentials="unused",
)
product = Product.objects.create(
sku="CANONICAL-1",
barcode="460000000001",
name="Канонический товар",
)
row = {
"rrdId": 1232610467,
"reportId": 1234567,
"createDate": "2026-08-18",
"saleDt": "2026-08-17T12:30:00Z",
"rrDate": "2026-08-18",
"srid": "sale-row-1",
"docTypeName": "Продажа",
"nmId": 7654321,
"vendorCode": "WB-ART-1",
"sku": "460000000001",
"techSize": "42",
"title": "Товар WB",
"quantity": "2",
"retailAmount": "367.50",
"ppvzSalesCommission": "55.12",
"acquiringFee": "14.89",
"deliveryAmount": 99,
"deliveryService": "20",
"rebillLogisticCost": "1.35",
"paidStorage": "10",
"paidAcceptance": "5",
"deduction": "2",
"penalty": "7",
"cashbackCommissionChange": "0.50",
"additionalPayment": "50",
"currency": "RUB",
}
matched, costs, unmatched, counts_as_sale = normalize_wildberries_finance_row(
connection=connection,
row=row,
)
assert matched is True
assert unmatched is False
assert counts_as_sale is True
assert costs == 10
binding = ProductBinding.objects.get(source=source, external_id="7654321")
assert binding.product == product
sale = Sale.objects.get(source=source, external_id="financial:1232610467")
assert sale.event_type == SaleEventType.SALE
assert sale.counts_as_sale is True
assert sale.occurred_at.isoformat() == "2026-08-17T12:30:00+00:00"
assert timezone.localdate(sale.source_updated_at).isoformat() == "2026-08-18"
assert sale.channel == "wildberries"
line = sale.lines.get()
assert line.product == product
assert line.quantity == 2
assert line.gross_amount == Decimal("367.50")
assert line.metadata["barcode"] == "460000000001"
assert line.metadata["size"] == "42"
assert set(line.costs.values_list("cost_type", flat=True)) == {
CostType.COMMISSION,
CostType.ACQUIRING,
CostType.DELIVERY,
CostType.STORAGE,
CostType.OTHER,
}
assert line.costs.get(external_id="1232610467:delivery").amount == Decimal("20")
assert line.costs.get(external_id="1232610467:rebill_logistics").amount == Decimal(
"1.35"
)
assert line.costs.get(
external_id="1232610467:additional_payment"
).amount == Decimal("-50")
assert not line.costs.filter(amount=99).exists()
assert SourceRecord.objects.filter(
source=source,
entity_type="financial_report_line",
external_id="1232610467",
).exists()
def test_wildberries_row_update_removes_disappeared_costs():
source = SourceSystem.objects.create(code="wildberries", name="Wildberries")
connection = IntegrationConnection.objects.create(
source=source,
provider="wildberries",
encrypted_credentials="unused",
)
row = {
"rrdId": 22,
"saleDate": "2026-08-17",
"docTypeName": "Возврат",
"retailAmount": "100",
"penalty": "7",
}
normalize_wildberries_finance_row(connection=connection, row=row)
assert Cost.objects.count() == 1
row["penalty"] = "0"
normalize_wildberries_finance_row(connection=connection, row=row)
assert Cost.objects.count() == 0
assert Sale.objects.get().event_type == SaleEventType.RETURN
def test_wildberries_service_row_does_not_inflate_sales_or_unmatched_products():
source = SourceSystem.objects.create(code="wildberries", name="Wildberries")
connection = IntegrationConnection.objects.create(
source=source,
provider="wildberries",
encrypted_credentials="unused",
)
row = {
"rrdId": 23,
"rrDate": "2026-08-17",
"docTypeName": "Логистика",
"deliveryService": "25",
"deliveryAmount": 3,
}
matched, costs, unmatched, counts_as_sale = normalize_wildberries_finance_row(
connection=connection,
row=row,
)
assert matched is False
assert unmatched is False
assert counts_as_sale is False
assert costs == 1
sale = Sale.objects.get()
assert sale.counts_as_sale is False
assert sale.lines.get().quantity == 0
assert sale.lines.get().costs.get().amount == Decimal("25")
def test_wildberries_sync_uses_cursor_until_empty_204_page():
source = SourceSystem.objects.create(code="wildberries", name="Wildberries")
connection = IntegrationConnection.objects.create(
source=source,
provider="wildberries",
encrypted_credentials=encrypt_credentials({"token": "test"}),
)
row = {
"rrdId": 45,
"saleDt": "2026-08-17",
"docTypeName": "Продажа",
"quantity": 1,
"retailAmount": "100",
}
client = Mock()
client.financial_report.side_effect = [[row], []]
with patch("commerce.services.wildberries_sync.WildberriesClient") as client_class:
client_class.return_value.__enter__.return_value = client
first = sync_wildberries_connection(connection)
connection.source.refresh_from_db()
second = sync_wildberries_connection(connection)
assert first.report_complete is False
assert first.sales == 1
assert connection.source.metadata["finance_initial_complete"] is True
assert "finance_rrd_id" not in connection.source.metadata
assert second.report_complete is True
assert client.financial_report.call_args_list[0].kwargs["rrd_id"] == 0
assert client.financial_report.call_args_list[1].kwargs["rrd_id"] == 45
def test_staff_can_create_and_test_wildberries_connection(api_client, user_factory):
user = user_factory(staff=True)
api_client.force_authenticate(user=user)
created = api_client.post(
reverse("api:v1:commerce:connection-list"),
{
"provider": "wildberries",
"auth_type": "token",
"token": "secret-wb-token",
},
format="json",
)
assert created.status_code == status.HTTP_201_CREATED
assert created.data["provider"] == "wildberries"
assert created.data["sync_interval_minutes"] == 60
assert "token" not in created.data
with patch(
"api.v1.commerce.viewsets.test_wildberries_connection",
return_value={
"account_id": "seller-1",
"account_name": "Магазин WB",
"tin": "1234567890",
},
):
tested = api_client.post(
reverse("api:v1:commerce:connection-test", args=[created.data["id"]])
)
assert tested.status_code == status.HTTP_200_OK
connection = IntegrationConnection.objects.get(provider="wildberries")
assert connection.status == IntegrationStatus.READY
assert connection.account_name == "Магазин WB"
assert connection.source.metadata["tin"] == "1234567890"
def test_background_run_dispatches_wildberries_synchronizer():
source = SourceSystem.objects.create(code="wildberries", name="Wildberries")
connection = IntegrationConnection.objects.create(
source=source,
provider="wildberries",
encrypted_credentials=encrypt_credentials({"token": "test"}),
status=IntegrationStatus.QUEUED,
task_id="wildberries-task",
)
run = connection.sync_runs.create(task_id="wildberries-task")
stats = Mock()
stats.to_dict.return_value = {
"processed": 5,
"created_products": 0,
"matched_products": 4,
"stock_snapshots": 0,
"images": 0,
"image_errors": 0,
"sales": 5,
"sale_lines": 5,
"costs": 12,
}
with (
patch("commerce.tasks.sync_wildberries_connection", return_value=stats) as sync,
patch("commerce.tasks.send_connection_update"),
):
result = sync_moysklad_run.run(str(run.pk))
sync.assert_called_once()
run.refresh_from_db()
assert result["costs"] == 12
assert run.sales == 5
assert run.costs == 12