using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Infrastructure.Data; using PowderCoating.Infrastructure.Repositories; using PowderCoating.Web.Controllers; namespace PowderCoating.UnitTests; public class DepositsControllerTests { // ── Record — input validation (no user resolution needed) ───────────── [Fact] public async Task Record_WhenAmountIsZero_ReturnsJsonError() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: 0m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Contains("greater than zero", doc.RootElement.GetProperty("message").GetString()); } [Fact] public async Task Record_WhenAmountIsNegative_ReturnsJsonError() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: -50m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); } [Fact] public async Task Record_WhenNeitherJobNorQuoteProvided_ReturnsJsonError() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Record(jobId: null, quoteId: null, customerId: 1, amount: 100m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Contains("Job or Quote must be specified", doc.RootElement.GetProperty("message").GetString()); } [Fact] public async Task Record_WhenInvalidPaymentMethod_ReturnsJsonError() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: 100m, paymentMethod: "BitcoinPizza", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Contains("Invalid payment method", doc.RootElement.GetProperty("message").GetString()); } [Fact] public async Task Record_WhenUserNotFound_ReturnsUnauthorized() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: null); var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: 100m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); Assert.IsType(result); } // ── Record — success paths ──────────────────────────────────────────── [Fact] public async Task Record_WithJobId_CreatesDepositAndReturnsSuccessJson() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(companyId: 1)); var receivedDate = new DateTime(2026, 4, 15); var result = await controller.Record(jobId: 42, quoteId: null, customerId: 7, amount: 250m, paymentMethod: "Check", receivedDate: receivedDate, reference: "REF-001", notes: "Half up front"); using var doc = ParseJson(result); Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Equal(250m, doc.RootElement.GetProperty("amount").GetDecimal()); Assert.Equal("Check", doc.RootElement.GetProperty("paymentMethod").GetString()); Assert.Equal("04/15/2026", doc.RootElement.GetProperty("receivedDate").GetString()); Assert.Equal("REF-001", doc.RootElement.GetProperty("reference").GetString()); Assert.Equal("Half up front", doc.RootElement.GetProperty("notes").GetString()); var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync(); Assert.Equal(42, deposit.JobId); Assert.Null(deposit.QuoteId); Assert.Equal(7, deposit.CustomerId); Assert.Equal(250m, deposit.Amount); Assert.Equal(PaymentMethod.Check, deposit.PaymentMethod); } [Fact] public async Task Record_WithQuoteId_CreatesDepositLinkedToQuote() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(companyId: 1)); var result = await controller.Record(jobId: null, quoteId: 99, customerId: 5, amount: 500m, paymentMethod: "CreditDebitCard", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Equal("Credit / Debit Card", doc.RootElement.GetProperty("paymentMethod").GetString()); var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync(); Assert.Equal(99, deposit.QuoteId); Assert.Null(deposit.JobId); } [Fact] public async Task Record_GeneratesReceiptNumberInExpectedFormat() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(companyId: 1)); var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: 100m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); var receiptNumber = doc.RootElement.GetProperty("receiptNumber").GetString()!; // Format: DEP-YYMM-#### (e.g. DEP-2604-0001) Assert.Matches(@"^DEP-\d{4}-\d{4}$", receiptNumber); Assert.EndsWith("-0001", receiptNumber); } [Fact] public async Task Record_SecondDepositInSameMonth_IncrementsSequenceNumber() { await using var context = CreateContext(); var user = CreateUser(companyId: 1); var controller = CreateController(context, currentUser: user); // First deposit await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: 100m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); // Second deposit var result = await controller.Record(jobId: 2, quoteId: null, customerId: 1, amount: 200m, paymentMethod: "Cash", receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); var receiptNumber = doc.RootElement.GetProperty("receiptNumber").GetString()!; Assert.EndsWith("-0002", receiptNumber); } [Fact] public async Task Record_PaymentMethodDisplayStrings_AreHumanReadable() { await using var context = CreateContext(); var user = CreateUser(companyId: 1); var cases = new[] { ("Cash", "Cash"), ("Check", "Check"), ("CreditDebitCard", "Credit / Debit Card"), ("BankTransferACH", "Bank Transfer / ACH"), ("DigitalPayment", "Digital Payment"), }; foreach (var (method, expected) in cases) { var controller = CreateController(context, currentUser: user); var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1, amount: 10m, paymentMethod: method, receivedDate: DateTime.UtcNow, reference: null, notes: null); using var doc = ParseJson(result); if (doc.RootElement.GetProperty("success").GetBoolean()) Assert.Equal(expected, doc.RootElement.GetProperty("paymentMethod").GetString()); } } // ── Delete ──────────────────────────────────────────────────────────── [Fact] public async Task Delete_WhenDepositNotFound_ReturnsJsonError() { await using var context = CreateContext(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Delete(id: 999, returnUrl: null); using var doc = ParseJson(result); Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Contains("not found", doc.RootElement.GetProperty("message").GetString()); } [Fact] public async Task Delete_WhenDepositAlreadyAppliedToInvoice_ReturnsJsonError() { await using var context = CreateContext(); context.Deposits.Add(new Deposit { Id = 1, CompanyId = 1, ReceiptNumber = "DEP-2604-0001", CustomerId = 1, JobId = 1, Amount = 100m, PaymentMethod = PaymentMethod.Cash, ReceivedDate = DateTime.UtcNow, AppliedToInvoiceId = 55 // already applied }); await context.SaveChangesAsync(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Delete(id: 1, returnUrl: null); using var doc = ParseJson(result); Assert.False(doc.RootElement.GetProperty("success").GetBoolean()); Assert.Contains("already been applied", doc.RootElement.GetProperty("message").GetString()); } [Fact] public async Task Delete_WhenUnappliedDeposit_SoftDeletesAndReturnsSuccess() { await using var context = CreateContext(); context.Deposits.Add(new Deposit { Id = 2, CompanyId = 1, ReceiptNumber = "DEP-2604-0001", CustomerId = 1, JobId = 1, Amount = 75m, PaymentMethod = PaymentMethod.Cash, ReceivedDate = DateTime.UtcNow, AppliedToInvoiceId = null // not yet applied }); await context.SaveChangesAsync(); var controller = CreateController(context, currentUser: CreateUser(1)); var result = await controller.Delete(id: 2, returnUrl: null); using var doc = ParseJson(result); Assert.True(doc.RootElement.GetProperty("success").GetBoolean()); var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync(d => d.Id == 2); Assert.True(deposit.IsDeleted); } // ── Helpers ─────────────────────────────────────────────────────────── private static DepositsController CreateController( ApplicationDbContext context, ApplicationUser? currentUser) { var uow = new UnitOfWork(context); var userManager = CreateUserManagerMock(); userManager .Setup(m => m.GetUserAsync(It.IsAny())) .ReturnsAsync(currentUser); var controller = new DepositsController( uow, userManager.Object, Mock.Of>()); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; return controller; } private static ApplicationUser CreateUser(int companyId) => new() { Id = "test-user", CompanyId = companyId, UserName = "test@example.com", Email = "test@example.com", FirstName = "Test", LastName = "User" }; private static Mock> CreateUserManagerMock() { var store = new Mock>(); return new Mock>( store.Object, null!, null!, null!, null!, null!, null!, null!, null!); } private static JsonDocument ParseJson(IActionResult result) { var json = Assert.IsType(result); var serialized = JsonSerializer.Serialize(json.Value); return JsonDocument.Parse(serialized); } private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; // Use a SuperAdmin user (no CompanyId claim) so that IsPlatformAdmin = true and the // global query filter evaluates to !IsDeleted only — not !IsDeleted && CompanyId == null, // which would filter out every row. DefaultHttpContext.Session throws, so we mock // HttpContext itself and stub Session.TryGetValue to return false (no impersonation). var identity = new ClaimsIdentity( new[] { new Claim(ClaimTypes.Role, "SuperAdmin") }, "Test"); var principal = new ClaimsPrincipal(identity); byte[]? noBytes = null; var sessionMock = new Mock(); sessionMock.Setup(s => s.TryGetValue(It.IsAny(), out noBytes)).Returns(false); var httpContextMock = new Mock(); httpContextMock.SetupGet(c => c.User).Returns(principal); httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object); var accessor = new Mock(); accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); return new ApplicationDbContext(options, accessor.Object, null!); } }