using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.IntegrationTests.Support; using Xunit; namespace foodmarket.IntegrationTests; /// End-to-end проверка фискализации через MockFiscalProvider: /// в новой организации PUT /api/organization/fiscal с provider=1 (Mock), /// создаём чек, проводим POST → ожидаем что в ответе и в последующем GET /// FiscalNumber начинается с MOCK-. /// /// Используем общий — это критично, т.к. /// xUnit в одном процессе не любит нескольких WebApplicationFactory<Program> /// инстансов одновременно (бросает «entry point exited without ever /// building an IHost»). Mock-провайдер активируется per-org через БД, /// а не глобальным config-override'ом. [Collection(ApiCollection.Name)] public class FiscalMockFlowTests { private readonly ApiFactory _factory; public FiscalMockFlowTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Posting_retail_sale_with_mock_provider_sets_fiscal_number() { var actor = new ApiActor(_factory.CreateClient()); await actor.SignupAndLoginAsync($"fiscal-{Guid.NewGuid():N}"); // Включаем Mock-провайдер на этой организации. var putResp = await actor.Http.PutAsJsonAsync("/api/organization/fiscal", new { provider = 1, // Mock newApiKey = (string?)null, newApiSecret = (string?)null, cashboxUniqueNumber = (string?)null, apiBaseUrl = (string?)null, }); putResp.EnsureSuccessStatusCode(); var fiscalCfg = await putResp.Content.ReadFromJsonAsync(); fiscalCfg.GetProperty("provider").GetInt32().Should().Be(1); fiscalCfg.GetProperty("providerName").GetString().Should().Contain("Mock"); // Сидим товар + приёмку, чтобы было что продавать. var (productId, storeId, retailPointId, currencyId) = await SeedProductWithStockAsync(actor); // Создаём чек на 1000 ₸, paidCash=1000. var draftResp = await actor.Http.PostAsJsonAsync("/api/sales/retail", new { date = DateTime.UtcNow, storeId, retailPointId, currencyId, payment = 0, isReturn = false, lines = new[] { new { productId, quantity = 10m, unitPrice = 100m, discount = 0m, vatPercent = 12m } }, subtotal = 1000m, discountTotal = 0m, total = 1000m, paidCash = 1000m, paidCard = 0m, }); draftResp.EnsureSuccessStatusCode(); var saleId = (await draftResp.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!; // Post чек. var postResp = await actor.Http.PostAsync($"/api/sales/retail/{saleId}/post", null); postResp.EnsureSuccessStatusCode(); // Читаем чек обратно — FiscalNumber должен быть MOCK-… var getResp = await actor.Http.GetAsync($"/api/sales/retail/{saleId}"); getResp.EnsureSuccessStatusCode(); var sale = await getResp.Content.ReadFromJsonAsync(); sale.GetProperty("fiscalNumber").GetString().Should().StartWith("MOCK-", "MockFiscalProvider должен был отработать на post'е и сохранить FiscalNumber"); sale.GetProperty("fiscalQrCode").GetString().Should().NotBeNullOrEmpty(); sale.GetProperty("fiscalUrl").GetString().Should().StartWith("https://mock.ofd.local/"); sale.GetProperty("fiscalProviderKind").GetInt32().Should().Be(1, "FiscalProviderKind.Mock = 1"); } [Fact] public async Task Test_send_with_mock_provider_returns_ok_and_fiscal_number() { var actor = new ApiActor(_factory.CreateClient()); await actor.SignupAndLoginAsync($"fiscal-test-{Guid.NewGuid():N}"); // Выбираем Mock. (await actor.Http.PutAsJsonAsync("/api/organization/fiscal", new { provider = 1, newApiKey = (string?)null, newApiSecret = (string?)null, cashboxUniqueNumber = (string?)null, apiBaseUrl = (string?)null, })).EnsureSuccessStatusCode(); var resp = await actor.Http.PostAsJsonAsync("/api/organization/fiscal/test-send", new { }); resp.EnsureSuccessStatusCode(); var body = await resp.Content.ReadFromJsonAsync(); body.GetProperty("ok").GetBoolean().Should().BeTrue(); body.GetProperty("fiscalNumber").GetString().Should().StartWith("MOCK-"); } [Fact] public async Task Default_provider_none_does_not_fiscalize() { // Без явной установки FiscalProvider он остаётся 0 (None) — поведение // обратно-совместимое: чек посту, но FiscalNumber пустой. var actor = new ApiActor(_factory.CreateClient()); await actor.SignupAndLoginAsync($"fiscal-none-{Guid.NewGuid():N}"); var (productId, storeId, retailPointId, currencyId) = await SeedProductWithStockAsync(actor); var draft = await actor.Http.PostAsJsonAsync("/api/sales/retail", new { date = DateTime.UtcNow, storeId, retailPointId, currencyId, payment = 0, isReturn = false, lines = new[] { new { productId, quantity = 1m, unitPrice = 100m, discount = 0m, vatPercent = 0m } }, subtotal = 100m, discountTotal = 0m, total = 100m, paidCash = 100m, paidCard = 0m, }); var saleId = (await draft.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!; (await actor.Http.PostAsync($"/api/sales/retail/{saleId}/post", null)).EnsureSuccessStatusCode(); var sale = await actor.GetJsonAsync($"/api/sales/retail/{saleId}"); // fiscalNumber == null означает «провайдер None / не дёрнули» var fiscal = sale.GetProperty("fiscalNumber"); (fiscal.ValueKind == JsonValueKind.Null || string.IsNullOrEmpty(fiscal.GetString())) .Should().BeTrue("при Provider=None фискальный номер не записывается"); } /// Аналог LoyaltyFlowTests.SeedProductAsync — копия чтобы не /// тащить кросс-теcтовые helper'ы. Создаёт товар с приёмкой на 100 шт. private static async Task<(string ProductId, string StoreId, string RetailPointId, string CurrencyId)> SeedProductWithStockAsync(ApiActor actor) { var units = (await actor.GetJsonAsync("/api/catalog/units-of-measure?pageSize=200")) .GetProperty("items").EnumerateArray().First(x => x.GetProperty("code").GetString() == "796"); var groups = (await actor.GetJsonAsync("/api/catalog/product-groups")) .GetProperty("items").EnumerateArray().First(); var pts = (await actor.GetJsonAsync("/api/catalog/price-types")) .GetProperty("items").EnumerateArray().First(x => x.GetProperty("isRetail").GetBoolean()); var curs = (await actor.GetJsonAsync("/api/catalog/currencies")) .GetProperty("items").EnumerateArray().First(x => x.GetProperty("code").GetString() == "KZT"); var stores = (await actor.GetJsonAsync("/api/catalog/stores")) .GetProperty("items").EnumerateArray().First(x => x.GetProperty("isMain").GetBoolean()); var retailPoints = (await actor.GetJsonAsync("/api/catalog/retail-points")) .GetProperty("items").EnumerateArray().First(); var prodResp = await actor.Http.PostAsJsonAsync("/api/catalog/products", new { name = "Fiscal test", article = $"FSC-{Guid.NewGuid():N}", unitOfMeasureId = units.GetProperty("id").GetString(), vat = 12, vatEnabled = true, productGroupId = groups.GetProperty("id").GetString(), packaging = 1, prices = new[] { new { priceTypeId = pts.GetProperty("id").GetString(), amount = 100m, currencyId = curs.GetProperty("id").GetString() } }, barcodes = new[] { new { code = $"700000{Guid.NewGuid().GetHashCode():X}".Replace("-", "").Substring(0, 12) + "0", type = 1, isPrimary = true } }, }); prodResp.EnsureSuccessStatusCode(); var productId = (await prodResp.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!; var supplier = await (await actor.Http.PostAsJsonAsync("/api/catalog/counterparties", new { name = "Sup", type = 2 })).Content.ReadFromJsonAsync(); var supply = await (await actor.Http.PostAsJsonAsync("/api/purchases/supplies", new { date = DateTime.UtcNow, supplierId = supplier.GetProperty("id").GetString(), storeId = stores.GetProperty("id").GetString(), currencyId = curs.GetProperty("id").GetString(), lines = new[] { new { productId, quantity = 100m, unitPrice = 50m } }, })).Content.ReadFromJsonAsync(); (await actor.Http.PostAsync($"/api/purchases/supplies/{supply.GetProperty("id").GetString()}/post", null)) .EnsureSuccessStatusCode(); return (productId, stores.GetProperty("id").GetString()!, retailPoints.GetProperty("id").GetString()!, curs.GetProperty("id").GetString()!); } }