using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.IntegrationTests.Support; using Xunit; namespace foodmarket.IntegrationTests; /// Промокод: создание + применение к чеку (Percent от Subtotal, /// scope=All) + проверка что Total уменьшился ровно на промо-скидку. /// Проверка валидации: невалидный код → 400 с понятным сообщением. [Collection(ApiCollection.Name)] public class PromotionFlowTests { private readonly ApiFactory _factory; public PromotionFlowTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Promocode_percent_discounts_total() { var actor = new ApiActor(_factory.CreateClient()); var email = $"pro-{Guid.NewGuid():N}@example.kz"; (await actor.SignupAsync(email, "Passw0rd!", "Pro Org")).EnsureSuccessStatusCode(); actor.UseToken(await actor.TokenAsync(email, "Passw0rd!")); // Промокод SALE20 = 20% var promoResp = await actor.Http.PostAsJsonAsync("/api/promotions", new { name = "Скидка 20%", description = (string?)null, code = "SALE20", type = 1 /* Percent */, value = 20m, scope = 1 /* All */, minSaleAmount = 0m, startsAt = DateTime.UtcNow.AddDays(-1), endsAt = (DateTime?)null, isActive = true, productGroupIds = Array.Empty(), productIds = Array.Empty(), }); promoResp.EnsureSuccessStatusCode(); var (productId, storeId, retailPointId, currencyId) = await SeedAsync(actor); // Чек на 500, промокод SALE20 → total = 400 var saleResp = await actor.Http.PostAsJsonAsync("/api/sales/retail", new { date = DateTime.UtcNow, storeId, retailPointId, currencyId, payment = 0, isReturn = false, lines = new[] { new { productId, quantity = 5m, unitPrice = 100m, discount = 0m, vatPercent = 12m } }, subtotal = 500m, discountTotal = 0m, total = 500m, paidCash = 500m, paidCard = 0m, promotionCode = "SALE20", }); saleResp.EnsureSuccessStatusCode(); var sale = await saleResp.Content.ReadFromJsonAsync(); sale.GetProperty("promotionDiscount").GetDecimal().Should().Be(100m); // 20% от 500 sale.GetProperty("total").GetDecimal().Should().Be(400m); sale.GetProperty("promotionCode").GetString().Should().Be("SALE20"); } [Fact] public async Task Invalid_promocode_returns_400_with_message() { var actor = new ApiActor(_factory.CreateClient()); var email = $"proi-{Guid.NewGuid():N}@example.kz"; (await actor.SignupAsync(email, "Passw0rd!", "ProI Org")).EnsureSuccessStatusCode(); actor.UseToken(await actor.TokenAsync(email, "Passw0rd!")); var (productId, storeId, retailPointId, currencyId) = await SeedAsync(actor); var saleResp = 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 = 12m } }, subtotal = 100m, discountTotal = 0m, total = 100m, paidCash = 100m, paidCard = 0m, promotionCode = "NOPE-NOEXIST", }); saleResp.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest); var body = await saleResp.Content.ReadFromJsonAsync(); body.GetProperty("error").GetString().Should().MatchRegex("(NOPE|не активен|не найден)"); body.GetProperty("field").GetString().Should().Be("promotionCode"); } private async Task<(string ProductId, string StoreId, string RetailPointId, string CurrencyId)> SeedAsync(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 = "Promo test", article = $"PRM-{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 = "7000000000017", type = 1, isPrimary = true } }, }); prodResp.EnsureSuccessStatusCode(); var prod = await prodResp.Content.ReadFromJsonAsync(); var productId = prod.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 = 50m, 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()!); } }