first commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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<ReceiptModel> 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<ReceiptModel> 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 =
|
||||
$"<p>Hi {m.MemberName},</p>" +
|
||||
$"<p>Thank you for your payment of <b>{m.Currency} {m.AmountPaid:N2}</b> for <b>{m.PlanName}</b> at {m.GymName}.</p>" +
|
||||
(m.Balance > 0 ? $"<p>Outstanding balance: {m.Currency} {m.Balance:N2}.</p>" : "") +
|
||||
$"<p>Your receipt ({m.ReceiptNo}) is attached.</p>" +
|
||||
$"<p>— {m.GymName}<br/><small>powered by GYM OS</small></p>";
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user