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