food-market/tests/food-market.IntegrationTests/InventoryPostUnpostTests.cs
nns 365dc5c1a8
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
test(inventory): обновлены устаревшие ассерты (ActualQty=BookQty, no-diff post = no-op)
После рефактора модели МойСклада (839fcaf) авто-загруженные строки получают
ActualQty=BookQty (а не 0), и проведение без расхождений — no-op (204, без
документов, остаток не меняется). Обновил два устаревших ассерта + все 4 теста
InventoryPostUnpostTests зелёные (вкл. cancel-results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 01:27:14 +05:00

209 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 InventoryPostUnpostTests
{
private readonly ApiFactory _factory;
public InventoryPostUnpostTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
[Fact]
public async Task Create_loads_book_qty_post_applies_diff_to_stock()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"inv-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P1-{Guid.NewGuid():N}", 100m, RandomBarcode());
var p2 = await api.CreateProductAsync(refs, $"P2-{Guid.NewGuid():N}", 200m, RandomBarcode());
// Подкладываем 10 шт p1 и 5 шт p2 через Enter.
foreach (var (pid, qty, cost) in new[] { (p1, 10m, 50m), (p2, 5m, 100m) })
{
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 = pid, quantity = qty, unitCost = cost } },
});
enter.EnsureSuccessStatusCode();
var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
}
// Создаём инвентаризацию без строк — контроллер подгрузит сам.
var create = await api.Http.PostAsJsonAsync("/api/inventory/inventories", new
{
date = DateTime.UtcNow,
storeId = refs.StoreId,
notes = "ежемесячный пересчёт",
lines = (object[])Array.Empty<object>(),
});
create.EnsureSuccessStatusCode();
var inv = await create.Content.ReadFromJsonAsync<JsonElement>();
var invId = inv.GetProperty("id").GetString();
var lines = inv.GetProperty("lines").EnumerateArray().ToList();
lines.Should().HaveCountGreaterOrEqualTo(2, "контроллер подгрузил bookQty для всех товаров склада");
var lineP1 = lines.First(l => l.GetProperty("productId").GetString() == p1);
lineP1.GetProperty("bookQty").GetDecimal().Should().Be(10m);
// Авто-загруженные строки «посчитаны по учёту»: ActualQty = BookQty, Diff = 0
// (чтобы проведение не обнуляло непересчитанные позиции — см. 839fcaf).
lineP1.GetProperty("actualQty").GetDecimal().Should().Be(10m);
// Вносим фактические: 12 шт p1 (излишек +2), 3 шт p2 (недостача -2).
var put = await api.Http.PutAsJsonAsync($"/api/inventory/inventories/{invId}", new
{
date = DateTime.UtcNow,
storeId = refs.StoreId,
notes = "проведено",
lines = new[]
{
new { productId = p1, actualQty = 12m },
new { productId = p2, actualQty = 3m },
},
});
put.EnsureSuccessStatusCode();
// Post → стоки 12 / 3.
using var post = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/post", new { });
post.IsSuccessStatusCode.Should().BeTrue($"post вернул {(int)post.StatusCode}: {await post.Content.ReadAsStringAsync()}");
(await api.StockOfAsync(refs.StoreId, p1)).Should().Be(12m);
(await api.StockOfAsync(refs.StoreId, p2)).Should().Be(3m);
// Unpost при ПРОВЕДЁННЫХ связанных документах → 409 (не удаляет молча).
using var badUnpost = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/unpost", new { });
((int)badUnpost.StatusCode).Should().Be(409, "нельзя вернуть в черновик, пока связанные документы проведены");
// Cancel-results → откат к 10/5; связанные документы остаются ЧЕРНОВИКАМИ (не удалены).
using var cancel = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/cancel-results", new { });
cancel.IsSuccessStatusCode.Should().BeTrue($"cancel-results вернул {(int)cancel.StatusCode}: {await cancel.Content.ReadAsStringAsync()}");
(await api.StockOfAsync(refs.StoreId, p1)).Should().Be(10m);
(await api.StockOfAsync(refs.StoreId, p2)).Should().Be(5m);
var invGet = await (await api.Http.GetAsync($"/api/inventory/inventories/{invId}")).Content.ReadFromJsonAsync<JsonElement>();
var related = invGet.GetProperty("related").EnumerateArray().ToList();
related.Should().NotBeEmpty("связанные Оприходование/Списание не удалены, остались черновиками");
related.Should().OnlyContain(r => r.GetProperty("posted").GetBoolean() == false);
// Теперь связанные — только черновики, unpost возвращает инвентаризацию в черновик.
using var unpost2 = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/unpost", new { });
unpost2.IsSuccessStatusCode.Should().BeTrue();
}
[Fact]
public async Task CancelResults_then_repost_restores_stock_to_actual()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"inv-cancel-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
// 10 шт на складе.
var seed = await api.Http.PostAsJsonAsync("/api/inventory/enters", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed", lines = new[] { new { productId = p1, quantity = 10m, unitCost = 50m } },
});
seed.EnsureSuccessStatusCode();
var seedId = (await seed.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{seedId}/post", new { })).EnsureSuccessStatusCode();
// Инвентаризация: факт 13 (излишек +3). complete → create-enter → post.
var create = await api.Http.PostAsJsonAsync("/api/inventory/inventories", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, notes = "излишек",
lines = new[] { new { productId = p1, actualQty = 13m } },
});
create.EnsureSuccessStatusCode();
var invId = (await create.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/complete", new { })).EnsureSuccessStatusCode();
var ce = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/create-enter", new { });
ce.EnsureSuccessStatusCode();
var enterId = (await ce.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("enterId").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{enterId}/post", new { })).EnsureSuccessStatusCode();
(await api.StockOfAsync(refs.StoreId, p1)).Should().Be(13m, "излишек проведён");
// Отмена результатов → остаток 10, оприходование НЕ удалено (черновик).
using var cancel = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/cancel-results", new { });
cancel.IsSuccessStatusCode.Should().BeTrue($"cancel-results: {(int)cancel.StatusCode} {await cancel.Content.ReadAsStringAsync()}");
(await api.StockOfAsync(refs.StoreId, p1)).Should().Be(10m);
using var enterStill = await api.Http.GetAsync($"/api/inventory/enters/{enterId}");
enterStill.StatusCode.Should().Be(System.Net.HttpStatusCode.OK, "оприходование не удалено");
// Повторное проведение → остаток снова = факт (13).
using var repost = await api.Http.PostAsJsonAsync($"/api/inventory/enters/{enterId}/post", new { });
repost.IsSuccessStatusCode.Should().BeTrue();
(await api.StockOfAsync(refs.StoreId, p1)).Should().Be(13m);
}
[Fact]
public async Task Post_with_no_diffs_is_noop()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"inv-nodiff-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
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 = p1, quantity = 3m, unitCost = 50m } },
});
enter.EnsureSuccessStatusCode();
var eid = (await enter.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
var create = await api.Http.PostAsJsonAsync("/api/inventory/inventories", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, notes = "match", lines = (object[])Array.Empty<object>(),
});
create.EnsureSuccessStatusCode();
var inv = await create.Content.ReadFromJsonAsync<JsonElement>();
var invId = inv.GetProperty("id").GetString();
// Фактическое = учётному (3 == 3, diff = 0): проведение завершает
// инвентаризацию, но НЕ создаёт документов и НЕ меняет остаток.
await api.Http.PutAsJsonAsync($"/api/inventory/inventories/{invId}", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, notes = "no-diff",
lines = new[] { new { productId = p1, actualQty = 3m } },
});
using var post = await api.Http.PostAsJsonAsync($"/api/inventory/inventories/{invId}/post", new { });
post.IsSuccessStatusCode.Should().BeTrue($"post при нулевых расхождениях: {(int)post.StatusCode} {await post.Content.ReadAsStringAsync()}");
(await api.StockOfAsync(refs.StoreId, p1)).Should().Be(3m, "без расхождений остаток не меняется");
var invGet = await (await api.Http.GetAsync($"/api/inventory/inventories/{invId}")).Content.ReadFromJsonAsync<JsonElement>();
invGet.GetProperty("related").EnumerateArray().Should().BeEmpty("без расхождений связанные документы не создаются");
}
[Fact]
public async Task Tenant_isolation_inventory()
{
var a = new ApiActor(_factory.CreateClient());
var b = new ApiActor(_factory.CreateClient());
await a.SignupAndLoginAsync($"inv-iso-a-{Guid.NewGuid():N}");
await b.SignupAndLoginAsync($"inv-iso-b-{Guid.NewGuid():N}");
var refsA = await a.LoadRefsAsync();
var resp = await a.Http.PostAsJsonAsync("/api/inventory/inventories", new
{
date = DateTime.UtcNow, storeId = refsA.StoreId, notes = "iso", lines = (object[])Array.Empty<object>(),
});
resp.EnsureSuccessStatusCode();
var invId = (await resp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString();
var bList = await b.ListAsync("/api/inventory/inventories?pageSize=200");
bList.Should().NotContain(x => x.GetProperty("id").GetString() == invId);
using var direct = await b.Http.GetAsync($"/api/inventory/inventories/{invId}");
direct.StatusCode.Should().Be(System.Net.HttpStatusCode.NotFound);
}
}