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