Files
Core_Be/Repos/ProfitLossRepository.cs
T
HarithaRandunu 2eb88b058e Add inventory management features including Sales, Stock Returns, and Suppliers
- 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.
2026-10-07 15:43:20 +05:30

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