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()!);
}
}