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