using FluentAssertions;
using foodmarket.Application.Inventory;
using foodmarket.Domain.Inventory;
using foodmarket.Infrastructure.Inventory;
using foodmarket.UnitTests.Support;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace foodmarket.UnitTests;
/// StockService.ApplyMovement: материализация Stock + запись StockMovement.
/// FK отключены — тестируем арифметику остатка и факт записи движения, не целостность.
public class StockServiceTests
{
private static StockMovementDraft Draft(Guid product, Guid store, decimal qty) =>
new(product, store, qty, MovementType.Supply, "supply",
DocumentId: Guid.NewGuid(), DocumentNumber: "DOC-1", UnitCost: 100m, OccurredAt: DateTime.UtcNow);
[Fact]
public async Task Creates_stock_and_movement_on_first_application()
{
using var sqlite = new SqliteDb(foreignKeys: false);
var org = Guid.NewGuid();
var tenant = new FakeTenantContext { OrganizationId = org };
var product = Guid.NewGuid();
var store = Guid.NewGuid();
decimal returned;
using (var db = sqlite.Create(tenant))
{
returned = await new StockService(db, tenant).ApplyMovementAsync(Draft(product, store, 10m));
await db.SaveChangesAsync();
}
returned.Should().Be(10m);
using (var db = sqlite.Create(tenant))
{
var stock = await db.Stocks.SingleAsync(s => s.ProductId == product && s.StoreId == store);
stock.Quantity.Should().Be(10m);
stock.OrganizationId.Should().Be(org);
(await db.StockMovements.CountAsync()).Should().Be(1);
}
}
// Каждое проведение документа = свой SaveChanges, поэтому повторное движение
// по тому же товару находит уже материализованный Stock и инкрементит его.
[Fact]
public async Task Second_application_increments_existing_stock()
{
using var sqlite = new SqliteDb(foreignKeys: false);
var tenant = new FakeTenantContext { OrganizationId = Guid.NewGuid() };
var product = Guid.NewGuid();
var store = Guid.NewGuid();
using var db = sqlite.Create(tenant);
var svc = new StockService(db, tenant);
await svc.ApplyMovementAsync(Draft(product, store, 10m));
await db.SaveChangesAsync();
var afterSecond = await svc.ApplyMovementAsync(Draft(product, store, 5m));
await db.SaveChangesAsync();
afterSecond.Should().Be(15m);
(await db.Stocks.SingleAsync(s => s.ProductId == product && s.StoreId == store)).Quantity.Should().Be(15m);
(await db.StockMovements.CountAsync()).Should().Be(2);
}
[Fact]
public async Task Negative_movement_decrements_stock()
{
using var sqlite = new SqliteDb(foreignKeys: false);
var tenant = new FakeTenantContext { OrganizationId = Guid.NewGuid() };
var product = Guid.NewGuid();
var store = Guid.NewGuid();
using var db = sqlite.Create(tenant);
var svc = new StockService(db, tenant);
await svc.ApplyMovementAsync(Draft(product, store, 10m));
await db.SaveChangesAsync();
var afterSale = await svc.ApplyMovementAsync(Draft(product, store, -3m));
await db.SaveChangesAsync();
afterSale.Should().Be(7m);
}
[Fact]
public async Task Throws_when_tenant_not_set()
{
using var sqlite = new SqliteDb(foreignKeys: false);
var tenant = new FakeTenantContext { OrganizationId = null };
using var db = sqlite.Create(tenant);
var svc = new StockService(db, tenant);
await FluentActions
.Awaiting(() => svc.ApplyMovementAsync(Draft(Guid.NewGuid(), Guid.NewGuid(), 1m)))
.Should().ThrowAsync();
}
}