using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.IntegrationTests.Support; using Xunit; namespace foodmarket.IntegrationTests; /// Автонаценка при приёмке — только по переключателю магазина /// RecalcRetailOnSupplyByGroupMarkup (по умолчанию ВЫКЛ). ВЫКЛ: приёмка без /// ручной правки продажной её не меняет, даже если у группы задана наценка. /// ВКЛ: пересчитывает по наценке. Ручная правка в строке работает в обоих режимах. [Collection(ApiCollection.Name)] public class MarkupToggleTests { private readonly ApiFactory _factory; public MarkupToggleTests(ApiFactory factory) => _factory = factory; private static string RandomBarcode() => string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10))); private async Task CreateGroupWithMarkupAsync(ApiActor api, decimal markup) { var resp = await api.Http.PostAsJsonAsync("/api/catalog/product-groups", new { name = $"MG-{Guid.NewGuid():N}", parentId = (string?)null, sortOrder = 0, markupPercent = markup }); resp.EnsureSuccessStatusCode(); return (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!; } private async Task CreateProductInGroupAsync(ApiActor api, ApiActor.Refs refs, string groupId, decimal retail, string barcode) { var resp = await api.Http.PostAsJsonAsync("/api/catalog/products", new { name = $"P-{Guid.NewGuid():N}", unitOfMeasureId = refs.UnitId, vat = 12, vatEnabled = true, productGroupId = groupId, packaging = 0, purchasePrice = (decimal?)null, purchaseCurrencyId = refs.CurrencyId, prices = new[] { new { priceTypeId = refs.PriceTypeId, amount = retail, currencyId = refs.CurrencyId } }, barcodes = new[] { new { code = barcode, type = 0, isPrimary = true } }, }); var body = await resp.Content.ReadAsStringAsync(); resp.IsSuccessStatusCode.Should().BeTrue(body); return JsonDocument.Parse(body).RootElement.GetProperty("id").GetString()!; } private async Task SetRecalcFlagAsync(ApiActor api, bool enabled) { var s = await api.GetJsonAsync("/api/organization/settings"); var dict = JsonSerializer.Deserialize>(s.GetRawText())!; dict["recalcRetailOnSupplyByGroupMarkup"] = JsonSerializer.SerializeToElement(enabled); var resp = await api.Http.PutAsJsonAsync("/api/organization/settings", dict); resp.EnsureSuccessStatusCode(); var after = await api.GetJsonAsync("/api/organization/settings"); after.GetProperty("recalcRetailOnSupplyByGroupMarkup").GetBoolean().Should().Be(enabled, "флаг должен сохраниться"); } private async Task RetailAsync(ApiActor api, string productId, string retailTypeId) { var p = await api.GetJsonAsync($"/api/catalog/products/{productId}"); foreach (var row in p.GetProperty("prices").EnumerateArray()) if (row.GetProperty("priceTypeId").GetString() == retailTypeId) return row.GetProperty("amount").GetDecimal(); return null; } [Fact] public async Task Markup_off_by_default_supply_keeps_sale_price() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"mk-off-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var group = await CreateGroupWithMarkupAsync(api, 50m); var productId = await CreateProductInGroupAsync(api, refs, group, 200m, RandomBarcode()); // Приёмка 10 шт по 100, без правки продажной. Флаг ВЫКЛ (по умолчанию). await api.CreatePostedSupplyAsync(refs, supplier, productId, 10m, 100m); (await RetailAsync(api, productId, refs.PriceTypeId)).Should().Be(200m, "при выключенном переключателе наценка группы не применяется — продажная не меняется"); } [Fact] public async Task Markup_on_supply_recomputes_sale_price() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"mk-on-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var group = await CreateGroupWithMarkupAsync(api, 50m); var gj = await api.GetJsonAsync($"/api/catalog/product-groups/{group}"); gj.GetProperty("markupPercent").GetDecimal().Should().Be(50m, "наценка группы сохранилась"); var productId = await CreateProductInGroupAsync(api, refs, group, 200m, RandomBarcode()); var pj = await api.GetJsonAsync($"/api/catalog/products/{productId}"); pj.GetProperty("productGroupId").GetString().Should().Be(group, "товар в группе с наценкой"); await SetRecalcFlagAsync(api, true); // cost от нулевого остатка = 100 → продажная = ceil(100 × 1.5) = 150. await api.CreatePostedSupplyAsync(refs, supplier, productId, 10m, 100m); (await RetailAsync(api, productId, refs.PriceTypeId)).Should().Be(150m, "при включённом переключателе продажная пересчитывается по наценке группы"); } [Fact] public async Task Line_override_wins_regardless_of_toggle() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"mk-ovr-{Guid.NewGuid():N}"); var refs = await api.LoadRefsAsync(); var supplier = await api.CreateCounterpartyAsync($"S-{Guid.NewGuid():N}"); var group = await CreateGroupWithMarkupAsync(api, 50m); var productId = await CreateProductInGroupAsync(api, refs, group, 200m, RandomBarcode()); // Флаг ВЫКЛ, но в строке задана продажная вручную → применяется override. 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 = 3m, unitPrice = 100m, retailPriceManuallyOverridden = true, retailPriceOverride = 333m } }, }); draft.EnsureSuccessStatusCode(); var supplyId = (await draft.Content.ReadFromJsonAsync()).GetProperty("id").GetString(); (await api.Http.PostAsJsonAsync($"/api/purchases/supplies/{supplyId}/post", new { })).EnsureSuccessStatusCode(); (await RetailAsync(api, productId, refs.PriceTypeId)).Should().Be(333m, "ручная правка продажной в строке применяется независимо от переключателя"); } }