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 TransferPostUnpostTests
{
private readonly ApiFactory _factory;
public TransferPostUnpostTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
/// Создать второй склад через api (главный уже есть из bootstrap).
private async Task CreateSecondStore(ApiActor api, string name)
{
var resp = await api.Http.PostAsJsonAsync("/api/catalog/stores", new
{
name, code = (string?)null, address = "Алматы 2",
phone = (string?)null, managerName = (string?)null,
isMain = false, isActive = true,
});
resp.EnsureSuccessStatusCode();
return (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString()!;
}
[Fact]
public async Task Post_creates_paired_movements_unpost_reverts_no_orphans()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"xfer-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var to = await CreateSecondStore(api, $"Склад-2-{Guid.NewGuid():N}");
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 { })).EnsureSuccessStatusCode();
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(10m);
// Создать перемещение: 4 шт с первого склада на второй.
var xfer = await api.Http.PostAsJsonAsync("/api/inventory/transfers", new
{
date = DateTime.UtcNow,
fromStoreId = refs.StoreId, toStoreId = to,
notes = "test",
lines = new[] { new { productId, quantity = 4m, unitCost = 50m } },
});
xfer.EnsureSuccessStatusCode();
var xferId = (await xfer.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
// Post → 6 на источнике, 4 на получателе.
using var post = await api.Http.PostAsJsonAsync($"/api/inventory/transfers/{xferId}/post", new { });
post.IsSuccessStatusCode.Should().BeTrue($"post вернул {(int)post.StatusCode}: {await post.Content.ReadAsStringAsync()}");
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(6m);
(await api.StockOfAsync(to, productId)).Should().Be(4m);
// Проверяем что движений ровно 2 (TransferOut + TransferIn) — никаких orphan.
var movs = await api.ListAsync($"/api/inventory/movements?productId={productId}&pageSize=100");
var byThisDoc = movs.Where(m =>
m.GetProperty("documentId").ValueKind == JsonValueKind.String &&
m.GetProperty("documentId").GetString() == xferId).ToList();
byThisDoc.Should().HaveCount(2, "перемещение порождает строго пару движений Out+In");
// Unpost → 10 и 0. Атомарность: ИЛИ оба отменены, ИЛИ ни одного.
using var unpost = await api.Http.PostAsJsonAsync($"/api/inventory/transfers/{xferId}/unpost", new { });
unpost.IsSuccessStatusCode.Should().BeTrue();
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(10m);
(await api.StockOfAsync(to, productId)).Should().Be(0m);
// По документу теперь 4 движения (2 forward + 2 reversal), но stock-инвариант держится.
movs = await api.ListAsync($"/api/inventory/movements?productId={productId}&pageSize=100");
var byThisDocAfter = movs.Where(m =>
m.GetProperty("documentId").ValueKind == JsonValueKind.String &&
m.GetProperty("documentId").GetString() == xferId).ToList();
byThisDocAfter.Should().HaveCount(4);
}
[Fact]
public async Task Cannot_create_transfer_to_same_store()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"xfer-same-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var productId = await api.CreateProductAsync(refs, $"Prod-{Guid.NewGuid():N}", 100m, RandomBarcode());
using var resp = await api.Http.PostAsJsonAsync("/api/inventory/transfers", new
{
date = DateTime.UtcNow,
fromStoreId = refs.StoreId, toStoreId = refs.StoreId,
notes = "same",
lines = new[] { new { productId, quantity = 1m, unitCost = 10m } },
});
((int)resp.StatusCode).Should().Be(400);
}
[Fact]
public async Task Post_blocked_when_from_store_short()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"xfer-short-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var to = await CreateSecondStore(api, $"Склад-S-{Guid.NewGuid():N}");
var productId = await api.CreateProductAsync(refs, $"Prod-{Guid.NewGuid():N}", 100m, RandomBarcode());
// На первом 0 шт; пытаемся перевести 5 — должно отбиться.
var xfer = await api.Http.PostAsJsonAsync("/api/inventory/transfers", new
{
date = DateTime.UtcNow,
fromStoreId = refs.StoreId, toStoreId = to,
notes = "short",
lines = new[] { new { productId, quantity = 5m, unitCost = 10m } },
});
xfer.EnsureSuccessStatusCode();
var xferId = (await xfer.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
using var post = await api.Http.PostAsJsonAsync($"/api/inventory/transfers/{xferId}/post", new { });
((int)post.StatusCode).Should().Be(409);
// Никаких движений не должно появиться.
(await api.StockOfAsync(refs.StoreId, productId)).Should().Be(0m);
(await api.StockOfAsync(to, productId)).Should().Be(0m);
}
[Fact]
public async Task Tenant_isolation_transfer()
{
var a = new ApiActor(_factory.CreateClient());
var b = new ApiActor(_factory.CreateClient());
await a.SignupAndLoginAsync($"xfer-iso-a-{Guid.NewGuid():N}");
await b.SignupAndLoginAsync($"xfer-iso-b-{Guid.NewGuid():N}");
var refsA = await a.LoadRefsAsync();
var toA = await CreateSecondStore(a, $"Склад-A2-{Guid.NewGuid():N}");
var productA = await a.CreateProductAsync(refsA, $"Prod-{Guid.NewGuid():N}", 100m, RandomBarcode());
var resp = await a.Http.PostAsJsonAsync("/api/inventory/transfers", new
{
date = DateTime.UtcNow,
fromStoreId = refsA.StoreId, toStoreId = toA,
notes = "iso",
lines = new[] { new { productId = productA, quantity = 1m, unitCost = 10m } },
});
resp.EnsureSuccessStatusCode();
var xferId = (await resp.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
var bList = await b.ListAsync("/api/inventory/transfers?pageSize=200");
bList.Should().NotContain(x => x.GetProperty("id").GetString() == xferId);
using var direct = await b.Http.GetAsync($"/api/inventory/transfers/{xferId}");
direct.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
}
}