using Fitness_core_BE.DTOs.Ledger; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Models; using Microsoft.EntityFrameworkCore; namespace Fitness_core_BE.Repos { public class ProfitLossRepository : IProfitLossRepository { // Every method here is already scoped to a single caller-owned clientId (resolved // from the ambient tenant), so no cross-tenant fan-out is needed. PaymentTransactions // (member-payment rows), StaffPayments and LedgerEntries are tenant-bucket; // ClientInvoices (platform billing) is central. private readonly TenantDbContext _context; private readonly CentralDbContext _central; public ProfitLossRepository(TenantDbContext context, CentralDbContext central) { _context = context; _central = central; } public async Task> GetDerivedEntriesAsync( Guid clientId, DateTime fromUtc, DateTime toUtc, CancellationToken ct = default) { var memberPayments = await _context.PaymentTransactions .AsNoTracking() .Where(payment => payment.FK_ClientId == clientId && payment.FK_CustomerInvoiceId != null && payment.IsApproved == true && payment.IsActive == true && payment.PayDate != null && payment.PayDate >= fromUtc && payment.PayDate < toUtc) .Select(payment => new LedgerEntryDto { EntryType = "Income", Category = "Member Payments", Description = payment.PayNote, Amount = payment.PayAmount, EntryDate = payment.PayDate!.Value, IsManual = false }) .ToListAsync(ct); var platformInvoices = await _central.ClientInvoices .AsNoTracking() .Where(invoice => invoice.ClientSubscription.FKClientId == clientId && invoice.PaidAmount > 0 && invoice.ClientInvoiceDate >= fromUtc && invoice.ClientInvoiceDate < toUtc) .Select(invoice => new LedgerEntryDto { EntryType = "Expense", Category = "Platform Fees", Description = "HexaFitness platform invoice", Amount = invoice.PaidAmount, EntryDate = invoice.ClientInvoiceDate, IsManual = false }) .ToListAsync(ct); var staffPayments = await _context.StaffPayments .AsNoTracking() .Where(payment => payment.Staff.FKClientId == clientId && payment.PaidAt >= fromUtc && payment.PaidAt < toUtc) .Select(payment => new LedgerEntryDto { EntryType = "Expense", Category = "Staff Payroll", Description = payment.Note, Amount = payment.Amount, EntryDate = payment.PaidAt, IsManual = false }) .ToListAsync(ct); // Paid facility bookings (guest/walk-in) -- a plain Booking read, not a // PaymentTransaction, since Booking has no invoice at all (see BookingInvoice, kept // deliberately separate from CustomerInvoice/PaymentTransaction's membership-only // wiring). PaidAt is the recognition date, not BookedAt/CheckedOutAt. var bookingPayments = await _context.Bookings .AsNoTracking() .Where(booking => booking.FKClientId == clientId && booking.PaymentStatus == "Paid" && booking.PaidAt != null && booking.PaidAt >= fromUtc && booking.PaidAt < toUtc) .Select(booking => new LedgerEntryDto { EntryType = "Income", Category = "Facility Bookings", Description = booking.GuestName, Amount = booking.AmountCharged, EntryDate = booking.PaidAt!.Value, IsManual = false }) .ToListAsync(ct); // Inventory: a Sale's revenue, a GRN's cost, and a StockReturn's Amount (staff- // entered -- see StockReturn's doc comment). Every return Category still posts the // same way here (one Expense category, reducing profit) even though CustomerReturn // is the one category that adds the stock back -- the P&L side stays uniform while // the stock-direction side doesn't. var productSales = await _context.Sales .AsNoTracking() .Where(sale => sale.FKClientId == clientId && sale.SoldAt >= fromUtc && sale.SoldAt < toUtc) .Select(sale => new LedgerEntryDto { EntryType = "Income", Category = "Product Sales", Description = sale.SaleNumber, Amount = sale.TotalAmount, EntryDate = sale.SoldAt, IsManual = false }) .ToListAsync(ct); var inventoryPurchases = await _context.GoodsReceivedNotes .AsNoTracking() .Where(grn => grn.FKClientId == clientId && grn.ReceivedAt >= fromUtc && grn.ReceivedAt < toUtc) .Select(grn => new LedgerEntryDto { EntryType = "Expense", Category = "Inventory Purchases", Description = grn.GrnNumber, Amount = grn.TotalAmount, EntryDate = grn.ReceivedAt, IsManual = false }) .ToListAsync(ct); var inventoryWriteOffs = await _context.StockReturns .AsNoTracking() .Where(stockReturn => stockReturn.FKClientId == clientId && stockReturn.ReturnedAt >= fromUtc && stockReturn.ReturnedAt < toUtc) .Select(stockReturn => new LedgerEntryDto { EntryType = "Expense", Category = "Inventory Write-offs", Description = stockReturn.Category + " — " + stockReturn.Product!.Name, Amount = stockReturn.Amount, EntryDate = stockReturn.ReturnedAt, IsManual = false }) .ToListAsync(ct); memberPayments.AddRange(platformInvoices); memberPayments.AddRange(staffPayments); memberPayments.AddRange(bookingPayments); memberPayments.AddRange(productSales); memberPayments.AddRange(inventoryPurchases); memberPayments.AddRange(inventoryWriteOffs); return memberPayments; } public Task> GetManualEntriesAsync( Guid clientId, DateTime fromUtc, DateTime toUtc, CancellationToken ct = default) { return _context.LedgerEntries .AsNoTracking() .Where(entry => entry.FKClientId == clientId && entry.EntryDate >= fromUtc && entry.EntryDate < toUtc) .ToListAsync(ct); } public Task GetManualEntryByIdAsync( Guid id, CancellationToken ct = default) { return _context.LedgerEntries.FirstOrDefaultAsync(entry => entry.Id == id, ct); } public async Task AddManualEntryAsync( LedgerEntry entry, CancellationToken ct = default) { await _context.LedgerEntries.AddAsync(entry, ct); } public void RemoveManualEntry(LedgerEntry entry) { _context.LedgerEntries.Remove(entry); } } }