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,
"ручная правка продажной в строке применяется независимо от переключателя");
}
}