using CartWise.Application.Services; using CartWise.Domain.Entities; using CartWise.Domain.Enums; using CartWise.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace CartWise.Application.Tests; public class PurchaseServiceTests : IDisposable { private static readonly DateTime Now = new(2026, 8, 10, 12, 0, 0, DateTimeKind.Utc); private readonly CartWiseDbContext _db; private readonly PurchaseService _sut; public PurchaseServiceTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _db = new CartWiseDbContext(options); _sut = new PurchaseService(_db, new PriceService(_db, TimeProvider.System), TimeProvider.System); } public void Dispose() { _db.Dispose(); GC.SuppressFinalize(this); } [Fact] public async Task StartPurchaseAsync_CreatesPurchase() { var householdId = Guid.NewGuid(); var result = await _sut.StartPurchaseAsync(householdId, "user-1", Now); Assert.True(result.IsSuccess); Assert.Equal(householdId, result.Value!.HouseholdId); Assert.False(result.Value.IsCompleted); } [Fact] public async Task AddPurchaseItemAsync_AddsItemAndPersistsIt() { var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); var result = await _sut.AddPurchaseItemAsync(started.Value!.HouseholdId, started.Value.PurchaseId, "Milk", 3.99m); Assert.True(result.IsSuccess); Assert.Equal("Milk", result.Value!.Description); var reloaded = await _sut.GetPurchaseAsync(started.Value.HouseholdId, started.Value.PurchaseId); Assert.Single(reloaded!.Items); } [Fact] public async Task AddPurchaseItemAsync_FailsForPurchaseInAnotherHousehold() { var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); var result = await _sut.AddPurchaseItemAsync(Guid.NewGuid(), started.Value!.PurchaseId, "Milk", 3.99m); Assert.False(result.IsSuccess); } [Fact] public async Task AddPurchaseItemAsync_FailsWhenPurchaseAlreadyCompleted() { var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); await _sut.AddPurchaseItemAsync(started.Value!.HouseholdId, started.Value.PurchaseId, "Milk", 3.99m); await _sut.CompletePurchaseAsync(started.Value.HouseholdId, started.Value.PurchaseId); var result = await _sut.AddPurchaseItemAsync(started.Value.HouseholdId, started.Value.PurchaseId, "Bread", 2.5m); Assert.False(result.IsSuccess); } [Fact] public async Task CompletePurchaseAsync_ComputesSubtotalAndTotal() { var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); await _sut.AddPurchaseItemAsync(started.Value!.HouseholdId, started.Value.PurchaseId, "Milk", 3.99m); await _sut.AddPurchaseItemAsync(started.Value.HouseholdId, started.Value.PurchaseId, "Bread", 2.50m); var result = await _sut.CompletePurchaseAsync(started.Value.HouseholdId, started.Value.PurchaseId, tax: 0.50m); Assert.True(result.IsSuccess); Assert.Equal(6.49m, result.Value!.Subtotal); Assert.Equal(0.50m, result.Value.Tax); Assert.Equal(6.99m, result.Value.Total); } [Fact] public async Task CompletePurchaseAsync_FailsForPurchaseInAnotherHousehold() { var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); var result = await _sut.CompletePurchaseAsync(Guid.NewGuid(), started.Value!.PurchaseId); Assert.False(result.IsSuccess); } [Fact] public async Task CompletePurchaseAsync_DoesNotCreateObservationForItemWithoutProduct() { var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); await _sut.AddPurchaseItemAsync(started.Value!.HouseholdId, started.Value.PurchaseId, "Unresolved item", 3.99m); await _sut.CompletePurchaseAsync(started.Value.HouseholdId, started.Value.PurchaseId); Assert.Empty(await _db.PriceObservations.ToListAsync()); } [Fact] public async Task CompletePurchaseAsync_CreatesObservationWithUnitPriceForResolvedProduct() { var product = new Product("Peanut Butter 16 oz", ProductSourceType.Manual, Now, sizeValue: 16m, sizeUnit: "oz"); _db.Products.Add(product); await _db.SaveChangesAsync(); var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); await _sut.AddPurchaseItemAsync( started.Value!.HouseholdId, started.Value.PurchaseId, "Peanut Butter", 4.00m, quantity: 1m, productId: product.ProductId); await _sut.CompletePurchaseAsync(started.Value.HouseholdId, started.Value.PurchaseId); var observation = Assert.Single(await _db.PriceObservations.ToListAsync()); Assert.Equal(product.ProductId, observation.ProductId); Assert.Equal(started.Value.HouseholdId, observation.HouseholdId); Assert.Equal(4.00m, observation.Price); Assert.Equal(0.25m, observation.UnitPrice); Assert.Equal(PriceObservationSourceType.UserEntered, observation.SourceType); Assert.Equal(1m, observation.ConfidenceScore); } [Fact] public async Task CompletePurchaseAsync_CreatesObservationWithoutUnitPriceWhenProductHasNoSize() { var product = new Product("Mystery Item", ProductSourceType.Manual, Now); _db.Products.Add(product); await _db.SaveChangesAsync(); var started = await _sut.StartPurchaseAsync(Guid.NewGuid(), "user-1", Now); await _sut.AddPurchaseItemAsync(started.Value!.HouseholdId, started.Value.PurchaseId, "Mystery Item", 2.00m, productId: product.ProductId); await _sut.CompletePurchaseAsync(started.Value.HouseholdId, started.Value.PurchaseId); var observation = Assert.Single(await _db.PriceObservations.ToListAsync()); Assert.Null(observation.UnitPrice); } [Fact] public async Task GetHouseholdPurchasesAsync_ReturnsOnlyThatHouseholdsPurchasesNewestFirst() { var householdA = Guid.NewGuid(); var householdB = Guid.NewGuid(); await _sut.StartPurchaseAsync(householdA, "user-1", Now.AddDays(-1)); await _sut.StartPurchaseAsync(householdA, "user-1", Now); await _sut.StartPurchaseAsync(householdB, "user-2", Now); var results = await _sut.GetHouseholdPurchasesAsync(householdA); Assert.Equal(2, results.Count); Assert.True(results[0].PurchasedUtc >= results[1].PurchasedUtc); } }