using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.IntegrationTests.Support; using Xunit; namespace foodmarket.IntegrationTests; /// Схема цен владельца 12.10: закупочная (purchasePrice) = цена строки /// последней проведённой приёмки; себестоимость — скользящее среднее; продажная /// меняется приёмкой только при ручной правке в строке. Unpost возвращает /// закупочную из предыдущей проведённой приёмки. [Collection(ApiCollection.Name)] public class SupplyPricingSchemeTests { private readonly ApiFactory _factory; public SupplyPricingSchemeTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); private async Task<(decimal? purchase, decimal cost, decimal? retail)> ReadPricesAsync(ApiActor api, string productId, string retailTypeId) { var p = await api.GetJsonAsync($"/api/catalog/products/{productId}"); decimal? purchase = p.TryGetProperty("purchasePrice", out var pp) && pp.ValueKind == JsonValueKind.Number ? pp.GetDecimal() : null; decimal cost = p.GetProperty("cost").GetDecimal(); decimal? retail = null; foreach (var row in p.GetProperty("prices").EnumerateArray()) if (row.GetProperty("priceTypeId").GetString() == retailTypeId) retail = row.GetProperty("amount").GetDecimal(); return (purchase, cost, retail); } [Fact] public async Task Supply_without_overrides_sets_purchase_and_cost_keeps_sale_price() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"pp-plain-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var productId = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 200m, RandomBarcode()); // Приёмка 10 шт по 100, без правки продажной цены в строке. await api.CreatePostedSupplyAsync(refs, supplier, productId, 10m, 100m); var (purchase, cost, retail) = await ReadPricesAsync(api, productId, refs.PriceTypeId); purchase.Should().Be(100m, "закупочная = цена строки приёмки"); cost.Should().Be(100m, "себестоимость от нулевого остатка = цена приёмки (скользящее среднее)"); retail.Should().Be(200m, "продажная не меняется, если её не правили в строке"); } [Fact] public async Task Supply_with_line_sale_override_updates_card_sale_price() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"pp-override-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var productId = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 200m, RandomBarcode()); var draft = await api.Http.PostAsJsonAsync("/api/purchases/supplies", new { date = DateTime.UtcNow, supplierId = supplier, storeId = refs.StoreId, currencyId = refs.CurrencyId, notes = (string?)null, lines = new[] { new { productId, quantity = 5m, unitPrice = 90m, retailPriceManuallyOverridden = true, retailPriceOverride = 250m } }, }); draft.EnsureSuccessStatusCode(); var supplyId = (await draft.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supplyId}/post", new { })).EnsureSuccessStatusCode(); var (purchase, _, retail) = await ReadPricesAsync(api, productId, refs.PriceTypeId); purchase.Should().Be(90m); retail.Should().Be(250m, "продажная обновилась из ручной правки в строке приёмки"); } [Fact] public async Task Unpost_restores_purchase_price_from_previous_supply() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"pp-unpost-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var productId = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 200m, RandomBarcode()); await api.CreatePostedSupplyAsync(refs, supplier, productId, 10m, 100m); // закупочная = 100 var (_, dummyB) = await api.CreatePostedSupplyAsync(refs, supplier, productId, 5m, 120m); // закупочная = 120 _ = dummyB; // Находим вторую приёмку и отменяем проведение. var supplies = await api.ListAsync("/api/purchases/supplies?pageSize=200"); var second = supplies.First(s => s.GetProperty("total").GetDecimal() == 600m).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{second}/unpost", new { })).EnsureSuccessStatusCode(); var (purchase, _, _) = await ReadPricesAsync(api, productId, refs.PriceTypeId); purchase.Should().Be(100m, "после отмены проведения закупочная вернулась из предыдущей приёмки"); } }