2eb88b058e
- Implement Sale and SaleItem models for tracking sales transactions. - Create StockReturn model to manage stock adjustments for various reasons. - Introduce Supplier model to represent suppliers of products. - Develop InventoryRepository for CRUD operations on products, suppliers, sales, and stock returns. - Implement InventoryService to handle business logic for inventory management. - Update TenantDbContext to include new models and relationships. - Enhance ProfitLossRepository to account for sales, purchases, and stock returns in profit and loss calculations. - Modify Program.cs to register new inventory-related services.
208 lines
8.3 KiB
C#
208 lines
8.3 KiB
C#
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<List<LedgerEntryDto>> 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<List<LedgerEntry>> 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<LedgerEntry?> 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);
|
|
}
|
|
}
|
|
}
|