using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;
namespace foodmarket.IntegrationTests;
[Collection(ApiCollection.Name)]
public class LossPostUnpostTests
{
private readonly ApiFactory _factory;
public LossPostUnpostTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
/// Положить через Enter, потом списать, проверить остаток и обратимость.
[Fact]
public async Task Posting_loss_decrements_stock_unpost_restores()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"loss-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var productId = await api.CreateProductAsync(refs, $"Prod-{Guid.NewGuid():N}", 200m, RandomBarcode());
// Сначала оприходуем 10 шт чтобы было что списывать.
var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed", lines = new[] { new { productId, quantity = 10m, unitCost = 50m } },
});
enter.EnsureSuccessStatusCode();
var enterId = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{enterId}/post", new { }))
.IsSuccessStatusCode.Should().BeTrue();
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(10m);
// Списываем 3 шт с причиной Expired.
var loss = await api.Http.PostAsJsonAsync("/api/inventory/losses", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
reason = 1, // Expired
notes = "просрочка партии",
lines = new[] { new { productId, quantity = 3m, unitCost = 50m } },
});
loss.EnsureSuccessStatusCode();
var lossId = (await loss.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
using var post = await api.Http.PostAsJsonAsync($"/api/inventory/losses/{lossId}/post", new { });
post.IsSuccessStatusCode.Should().BeTrue($"post вернул {(int)post.StatusCode}: {await post.Content.ReadAsStringAsync()}");
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(7m);
// Unpost → возвращаем на склад.
using var unpost = await api.Http.PostAsJsonAsync($"/api/inventory/losses/{lossId}/unpost", new { });
unpost.IsSuccessStatusCode.Should().BeTrue();
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(10m);
}
[Fact]
public async Task Cannot_write_off_more_than_available()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"loss-over-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var productId = await api.CreateProductAsync(refs, $"Prod-{Guid.NewGuid():N}", 200m, RandomBarcode());
// 2 шт на остатке.
var enter = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed-small", lines = new[] { new { productId, quantity = 2m, unitCost = 50m } },
});
enter.EnsureSuccessStatusCode();
var enterId = (await enter.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{enterId}/post", new { })).EnsureSuccessStatusCode();
// Списать 5 шт — больше чем есть.
var loss = await api.Http.PostAsJsonAsync("/api/inventory/losses", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId, reason = 0,
notes = "over", lines = new[] { new { productId, quantity = 5m, unitCost = 50m } },
});
loss.EnsureSuccessStatusCode();
var lossId = (await loss.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
using var post = await api.Http.PostAsJsonAsync($"/api/inventory/losses/{lossId}/post", new { });
((int)post.StatusCode).Should().Be(409);
// Остаток не должен измениться.
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(2m);
}
[Fact]
public async Task Tenant_isolation_loss()
{
var a = new ApiActor(_factory.CreateClient());
var b = new ApiActor(_factory.CreateClient());
await a.SignupAndLoginAsync($"loss-iso-a-{Guid.NewGuid():N}");
await b.SignupAndLoginAsync($"loss-iso-b-{Guid.NewGuid():N}");
var refsA = await a.LoadRefsAsync();
var productA = await a.CreateProductAsync(refsA, $"Prod-{Guid.NewGuid():N}", 100m, RandomBarcode());
var resp = await a.Http.PostAsJsonAsync("/api/inventory/losses", new
{
date = DateTime.UtcNow, storeId = refsA.StoreId, currencyId = refsA.CurrencyId, reason = 0,
notes = "iso", lines = new[] { new { productId = productA, quantity = 1m, unitCost = 10m } },
});
resp.EnsureSuccessStatusCode();
var lossId = (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
var bList = await b.ListAsync("/api/inventory/losses?pageSize=200");
bList.Should().NotContain(x => x.GetProperty("id").GetString() == lossId);
using var direct = await b.Http.GetAsync($"/api/inventory/losses/{lossId}");
direct.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
}
}