using Fitness_core_BE.Exceptions; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; using Fitness_core_BE.Models.Receipt; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace Fitness_core_BE.Services { public class ReceiptService : IReceiptService { private readonly ICustomerInvoiceRepository _customerInvoiceRepository; private readonly ICustomerSubscriptionRepository _customerSubscriptionRepository; private readonly IClientRepository _clientRepository; private readonly IClientInvoiceRepository _clientInvoiceRepository; private readonly IPaymentRepository _paymentRepository; private readonly IThirdPartyService _thirdPartyService; public ReceiptService( ICustomerInvoiceRepository customerInvoiceRepository, ICustomerSubscriptionRepository customerSubscriptionRepository, IClientRepository clientRepository, IClientInvoiceRepository clientInvoiceRepository, IPaymentRepository paymentRepository, IThirdPartyService thirdPartyService) { _customerInvoiceRepository = customerInvoiceRepository; _customerSubscriptionRepository = customerSubscriptionRepository; _clientRepository = clientRepository; _clientInvoiceRepository = clientInvoiceRepository; _paymentRepository = paymentRepository; _thirdPartyService = thirdPartyService; } public async Task BuildForInvoiceAsync(Guid customerInvoiceId, decimal? paymentAmount = null, Guid? requireClientId = null) { var invoice = await _customerInvoiceRepository.GetCustomerInvoiceById(customerInvoiceId) ?? throw new NotFoundException("Invoice not found"); if (requireClientId.HasValue && invoice.FKClientId != requireClientId.Value) throw new UnauthorizedException("This invoice does not belong to your gym."); var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(invoice.FKCustomerSubscriptionId); string planName = subscription?.CustomerPlan?.PlanName ?? "Membership"; Guid? customerId = subscription?.FKCustomerId; Customer? customer = customerId.HasValue ? await _clientRepository.GetCustomerById(customerId.Value) : null; var client = await _clientRepository.GetClientByIdAsync(invoice.FKClientId); string gymName = client?.ClientBusinesses?.FirstOrDefault()?.BusinessName ?? client?.ClientName ?? "GYM OS"; decimal paid = await _paymentRepository.GetApprovedPaidAmountAsync(customerInvoiceId); return new ReceiptModel { ClientId = invoice.FKClientId, ReceiptNo = $"GYMOS-{customerInvoiceId.ToString("N")[..8].ToUpperInvariant()}", Date = DateTime.UtcNow, GymName = gymName, MemberName = customer?.CusName ?? "Member", MemberPhone = customer?.PrimaryPH, MemberEmail = customer?.Email, PlanName = planName, AmountPaid = paymentAmount ?? paid, InvoiceAmount = invoice.Amount, Balance = invoice.Amount - paid, Currency = "LKR" }; } public async Task BuildForClientInvoiceAsync(Guid clientInvoiceId, decimal? paymentAmount = null, Guid? requireClientId = null) { var invoice = await _clientInvoiceRepository.GetClientInvoiceById(clientInvoiceId) ?? throw new NotFoundException("Invoice not found"); var clientId = invoice.ClientSubscription?.FKClientId ?? Guid.Empty; if (requireClientId.HasValue && clientId != requireClientId.Value) throw new UnauthorizedException("This invoice does not belong to your gym."); var client = invoice.ClientSubscription?.Client ?? await _clientRepository.GetClientByIdAsync(clientId); string gymName = client?.ClientBusinesses?.FirstOrDefault()?.BusinessName ?? client?.ClientName ?? "Gym"; var payments = await _paymentRepository.GetPaymentsByClientInvoiceIdAsync(clientInvoiceId); decimal paid = payments.Where(p => p.IsApproved == true).Sum(p => p.PayAmount); string itemSummary = invoice.Items?.FirstOrDefault()?.ItemName ?? "Platform fee"; return new ReceiptModel { ClientId = clientId, ReceiptNo = $"HEXA-{clientInvoiceId.ToString("N")[..8].ToUpperInvariant()}", Date = DateTime.UtcNow, GymName = "HexaFitness", MemberName = gymName, MemberPhone = client?.ClientPhone, MemberEmail = client?.ClientEmail, PlanName = itemSummary, AmountPaid = paymentAmount ?? paid, InvoiceAmount = invoice.Amount, Balance = invoice.Amount - paid, Currency = "LKR" }; } public byte[] GeneratePdf(ReceiptModel m) { string money(decimal v) => $"{m.Currency} {v:N2}"; var document = QuestPDF.Fluent.Document.Create(container => { container.Page(page => { page.Size(PageSizes.A5); page.Margin(28); page.DefaultTextStyle(x => x.FontSize(11).FontColor(Colors.Grey.Darken3)); page.Header().Column(col => { col.Item().Text("GYM OS").FontSize(20).Bold().FontColor(Colors.Blue.Darken2); col.Item().Text(m.GymName).FontSize(13).SemiBold(); col.Item().PaddingTop(2).Text("Payment Receipt").FontSize(10).FontColor(Colors.Grey.Medium); col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Lighten2); }); page.Content().PaddingVertical(12).Column(col => { col.Spacing(6); col.Item().Row(row => { row.RelativeItem().Text($"Receipt No: {m.ReceiptNo}"); row.RelativeItem().AlignRight().Text($"Date: {m.Date:yyyy-MM-dd}"); }); col.Item().PaddingTop(4).Text("Billed to").FontColor(Colors.Grey.Medium).FontSize(9); col.Item().Text(m.MemberName).SemiBold(); if (!string.IsNullOrWhiteSpace(m.MemberPhone)) col.Item().Text(m.MemberPhone!).FontSize(10); if (!string.IsNullOrWhiteSpace(m.MemberEmail)) col.Item().Text(m.MemberEmail!).FontSize(10); col.Item().PaddingTop(10).Table(table => { table.ColumnsDefinition(c => { c.RelativeColumn(3); c.RelativeColumn(2); }); table.Cell().Element(HeaderCell).Text("Description"); table.Cell().Element(HeaderCell).AlignRight().Text("Amount"); table.Cell().Element(BodyCell).Text(m.PlanName); table.Cell().Element(BodyCell).AlignRight().Text(money(m.AmountPaid)); static IContainer HeaderCell(IContainer c) => c.Background(Colors.Grey.Lighten3).PaddingVertical(5).PaddingHorizontal(8).DefaultTextStyle(t => t.SemiBold()); static IContainer BodyCell(IContainer c) => c.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5).PaddingHorizontal(8); }); col.Item().PaddingTop(10).AlignRight().Column(totals => { totals.Item().Text($"Amount paid: {money(m.AmountPaid)}").SemiBold(); totals.Item().Text($"Invoice total: {money(m.InvoiceAmount)}").FontSize(10).FontColor(Colors.Grey.Medium); totals.Item().Text($"Balance due: {money(m.Balance)}").FontSize(10) .FontColor(m.Balance > 0 ? Colors.Red.Medium : Colors.Green.Darken1); }); }); page.Footer().AlignCenter().Column(col => { col.Item().PaddingTop(6).LineHorizontal(1).LineColor(Colors.Grey.Lighten2); col.Item().PaddingTop(6).Text("Thank you for your payment.").FontSize(10); col.Item().Text($"{m.GymName} · powered by GYM OS").FontSize(8).FontColor(Colors.Grey.Medium); }); }); }); return document.GeneratePdf(); } public async Task SendReceiptAsync(ReceiptModel m) { byte[] pdf; try { pdf = GeneratePdf(m); } catch (Exception ex) { Console.WriteLine($"Receipt PDF generation failed (non-critical): {ex.Message}"); return; } if (!string.IsNullOrWhiteSpace(m.MemberEmail)) { try { string html = $"

Hi {m.MemberName},

" + $"

Thank you for your payment of {m.Currency} {m.AmountPaid:N2} for {m.PlanName} at {m.GymName}.

" + (m.Balance > 0 ? $"

Outstanding balance: {m.Currency} {m.Balance:N2}.

" : "") + $"

Your receipt ({m.ReceiptNo}) is attached.

" + $"

— {m.GymName}
powered by GYM OS

"; await _thirdPartyService.SendEmailAsync(m.MemberEmail!, $"Receipt {m.ReceiptNo} — {m.GymName}", html, pdf, $"{m.ReceiptNo}.pdf"); } catch (Exception ex) { Console.WriteLine($"Receipt email failed (non-critical): {ex.Message}"); } } if (!string.IsNullOrWhiteSpace(m.MemberPhone)) { try { string sms = $"You paid {m.Currency} {m.AmountPaid:N2} for {m.PlanName} at {m.GymName}. Thank you! - GYM OS"; await _thirdPartyService.SendSmsAsync(m.MemberPhone!, null, sms); } catch (Exception ex) { Console.WriteLine($"Receipt SMS failed (non-critical): {ex.Message}"); } } } } }