using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GoodBooks.PAY.PAYDAL.CustomCode.Loyalty; using GoodBooks.PAY.PAYDAL.DTO.Loyalty; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using static GB5Shared.GB5Constant.Constant; namespace GoodBooks.PAY.PAYBLL.Loyalty { // ───────────────────────────────────────────────────────────────────────── // CustomerLoyaltyBLL // // TLOYALTYLEDGER is APPEND-ONLY. No UPDATE on ledger rows. // Points reversals and expirations use negative-point INSERT rows. // ───────────────────────────────────────────────────────────────────────── public class CustomerLoyaltyBLL : ICustomerLoyaltyBLL { private readonly ICustomerLoyaltyEnrollmentDAL _enrollmentDal; private readonly ILoyaltyLedgerDAL _ledgerDal; private readonly ILoyaltyEarnCfgDAL _earnCfgDal; private readonly ILoyaltyTierDAL _tierDal; private readonly ILoyaltyEarnCfgBLL _earnCfgBll; private readonly AutoNumber _autoNumber; private readonly EventLogPublish _eventLogPublish; private readonly IQueryExecutor _queryExecutor; private readonly ILogger _logger; public CustomerLoyaltyBLL( ICustomerLoyaltyEnrollmentDAL enrollmentDal, ILoyaltyLedgerDAL ledgerDal, ILoyaltyEarnCfgDAL earnCfgDal, ILoyaltyTierDAL tierDal, ILoyaltyEarnCfgBLL earnCfgBll, AutoNumber autoNumber, EventLogPublish eventLogPublish, IQueryExecutor queryExecutor, ILogger logger) { _enrollmentDal = enrollmentDal; _ledgerDal = ledgerDal; _earnCfgDal = earnCfgDal; _tierDal = tierDal; _earnCfgBll = earnCfgBll; _autoNumber = autoNumber; _eventLogPublish = eventLogPublish; _queryExecutor = queryExecutor; _logger = logger; } // ── EnrollCustomer (idempotent) ─────────────────────────────────────── public async Task EnrollCustomer( CustomerLoyaltyEnrollmentDTO dto, LoginDTO login, CancellationToken ct) { try { // Idempotency guard — return success if already enrolled int count = await _enrollmentDal .CountEnrollment(dto.CustomerId, dto.LoyaltyProgramId, login) .ConfigureAwait(false); if (count > 0) return SuccessResponse.SaveSuccess; // already enrolled — not an error // Auto-number enrollment ID var autoNum = await _autoNumber .GetAutoNumber(1, "LOYALTYENROLLMENT", login) .ConfigureAwait(false); dto.EnrollmentId = autoNum.StartNumber; dto.EnrolledOn = DateTime.UtcNow; dto.CurrentPoints = 0m; dto.LifetimePoints = 0m; dto.TenantId = login.ClientId; dto.CreatedById = login.UserId; dto.CreatedOn = DateTime.UtcNow; dto.ModifiedById = login.UserId; dto.ModifiedOn = DateTime.UtcNow; // Auto-number ledger welcome row var ledgerAutoNum = await _autoNumber .GetAutoNumber(1, "LOYALTYLEDGER", login) .ConfigureAwait(false); var welcomeRow = new LoyaltyLedgerDTO { LoyaltyLedgerId = ledgerAutoNum.StartNumber, EnrollmentId = dto.EnrollmentId, LoyaltyProgramId = dto.LoyaltyProgramId, CustomerId = dto.CustomerId, EntryType = 1, // Earn (0-point welcome marker) Points = 0m, ReferenceOrderId = 0, Remarks = "Enrollment", CreatedById = login.UserId, CreatedOn = DateTime.UtcNow, TenantId = login.ClientId }; var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _enrollmentDal.InsertEnrollment(dto, login, tx).ConfigureAwait(false); await _ledgerDal.InsertLedgerEntry(welcomeRow, login, tx).ConfigureAwait(false); await _queryExecutor.CommitAsync(tx).ConfigureAwait(false); } catch { await _queryExecutor.RollbackAsync(tx).ConfigureAwait(false); throw; } try { await _eventLogPublish.PublishEventLogAsync( "Customer Loyalty Enrollment", new { dto.EnrollmentId, dto.CustomerId, dto.LoyaltyProgramId }, EventTypeConstant.ENROLLCUSTOMEREVENTTYPEID, 0, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "Event log failed for Enrollment {EnrollmentId}", dto.EnrollmentId); } return $"{SuccessResponse.SaveSuccessMessage} {dto.EnrollmentId}"; } catch (Exception ex) { _logger.LogError(ex, "EnrollCustomer failed for CustomerId {CustomerId}", dto.CustomerId); throw; } } // ── GetEnrollment ───────────────────────────────────────────────────── public async Task GetEnrollment(int enrollmentId, LoginDTO login, CancellationToken ct) { try { return await _enrollmentDal.GetEnrollment(enrollmentId, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetEnrollment failed for EnrollmentId {EnrollmentId}", enrollmentId); throw; } } // ── GetCustomerLoyaltyBalance ───────────────────────────────────────── public async Task GetCustomerLoyaltyBalance(int enrollmentId, LoginDTO login, CancellationToken ct) { try { decimal availablePoints = await _ledgerDal .GetAvailablePoints(enrollmentId, login) .ConfigureAwait(false); return JsonConvert.SerializeObject(new { AvailablePoints = availablePoints }); } catch (Exception ex) { _logger.LogError(ex, "GetCustomerLoyaltyBalance failed for EnrollmentId {EnrollmentId}", enrollmentId); throw; } } // ── GetLoyaltyLedger ────────────────────────────────────────────────── public async Task GetLoyaltyLedger( int enrollmentId, int pageOffset, int pageSize, LoginDTO login, CancellationToken ct) { try { return await _ledgerDal .GetLoyaltyLedger(enrollmentId, pageOffset, pageSize, login) .ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetLoyaltyLedger failed for EnrollmentId {EnrollmentId}", enrollmentId); throw; } } // ── EarnPointsAsync ─────────────────────────────────────────────────── public async Task EarnPointsAsync( int enrollmentId, int payOrderId, decimal netPayableAmt, int? productId, int? categoryId, int payGatewayId, LoginDTO login, CancellationToken ct) { try { // Load enrollment to get program ID and current tier var enrollment = await _enrollmentDal .GetEnrollmentByCustomerProgram(0, 0, login) // placeholder — caller passes enrollmentId .ConfigureAwait(false); // When called with enrollmentId, load directly // NOTE: fallback — just get enrollment by ID via GetEnrollment JSON and parse string enrollJson = await _enrollmentDal.GetEnrollment(enrollmentId, login).ConfigureAwait(false); var enrollDto = JsonConvert.DeserializeObject(enrollJson); if (enrollDto is null) return; // Load active earn rules for the program var rules = await _earnCfgDal .GetEarnCfgsByProgram(enrollDto.LoyaltyProgramId, login) .ConfigureAwait(false); // Load current tier for multiplier LoyaltyTierDTO? tier = null; if (enrollDto.LoyaltyTierId > 0) tier = await _tierDal.GetTierForPoints(enrollDto.LoyaltyProgramId, enrollDto.LifetimePoints, login) .ConfigureAwait(false); // Resolve earn rule (priority engine) var earnRule = _earnCfgBll.ResolveEarnRule(rules, productId, categoryId, payGatewayId, enrollDto.LoyaltyTierId > 0 ? enrollDto.LoyaltyTierId : null); if (earnRule is null) { _logger.LogInformation("No earn rule matched for Enrollment {EnrollmentId} PayOrder {PayOrderId}", enrollmentId, payOrderId); return; } decimal earnedPoints = _earnCfgBll.CalculateEarnPoints(netPayableAmt, earnRule, tier); if (earnedPoints <= 0m) return; var ledgerAutoNum = await _autoNumber.GetAutoNumber(1, "LOYALTYLEDGER", login).ConfigureAwait(false); var ledgerDto = new LoyaltyLedgerDTO { LoyaltyLedgerId = ledgerAutoNum.StartNumber, EnrollmentId = enrollmentId, LoyaltyProgramId = enrollDto.LoyaltyProgramId, CustomerId = enrollDto.CustomerId, EntryType = 1, // Earn Points = earnedPoints, ReferenceOrderId = payOrderId, EarnCfgId = earnRule.LoyaltyEarnCfgId, Remarks = $"Payment earn — Order #{payOrderId}", CreatedById = login.UserId, CreatedOn = DateTime.UtcNow, TenantId = login.ClientId }; decimal newCurrentPoints = enrollDto.CurrentPoints + earnedPoints; decimal newLifetimePoints = enrollDto.LifetimePoints + earnedPoints; var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _ledgerDal.InsertLedgerEntry(ledgerDto, login, tx).ConfigureAwait(false); await _enrollmentDal.UpdateEnrollmentPoints( enrollmentId, newCurrentPoints, newLifetimePoints, login, tx).ConfigureAwait(false); // Tier upgrade check var newTier = await _tierDal.GetTierForPoints( enrollDto.LoyaltyProgramId, newLifetimePoints, login).ConfigureAwait(false); if (newTier is not null && newTier.LoyaltyTierId != enrollDto.LoyaltyTierId) { await _enrollmentDal.UpdateEnrollmentTier(enrollmentId, newTier.LoyaltyTierId, login, tx) .ConfigureAwait(false); _logger.LogInformation("Tier upgraded for Enrollment {EnrollmentId} → Tier {TierId}", enrollmentId, newTier.LoyaltyTierId); } await _queryExecutor.CommitAsync(tx).ConfigureAwait(false); } catch { await _queryExecutor.RollbackAsync(tx).ConfigureAwait(false); throw; } } catch (Exception ex) { _logger.LogError(ex, "EarnPointsAsync failed for Enrollment {EnrollmentId} PayOrder {PayOrderId}", enrollmentId, payOrderId); throw; } } // ── ExpirePointsAsync ───────────────────────────────────────────────── // APPEND-ONLY — inserts negative-point compensating rows. // No UPDATE on any TLOYALTYLEDGER row. public async Task ExpirePointsAsync(LoginDTO login, CancellationToken ct) { try { var expiringList = await _ledgerDal.GetExpiringPoints(login).ConfigureAwait(false); foreach (var (enrollmentId, pointsExpiring) in expiringList) { if (pointsExpiring <= 0m) continue; ct.ThrowIfCancellationRequested(); try { // Load current enrollment for current-points update string enrollJson = await _enrollmentDal.GetEnrollment(enrollmentId, login) .ConfigureAwait(false); var enrollDto = JsonConvert.DeserializeObject(enrollJson); if (enrollDto is null) continue; var ledgerAutoNum = await _autoNumber .GetAutoNumber(1, "LOYALTYLEDGER", login).ConfigureAwait(false); var expiryRow = new LoyaltyLedgerDTO { LoyaltyLedgerId = ledgerAutoNum.StartNumber, EnrollmentId = enrollmentId, LoyaltyProgramId = enrollDto.LoyaltyProgramId, CustomerId = enrollDto.CustomerId, EntryType = 3, // Expire Points = -pointsExpiring, // Negative compensating row ReferenceOrderId = 0, Remarks = "Points expired", CreatedById = login.UserId, CreatedOn = DateTime.UtcNow, TenantId = login.ClientId }; decimal newCurrentPoints = Math.Max(0m, enrollDto.CurrentPoints - pointsExpiring); var tx = await _queryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _ledgerDal.InsertLedgerEntry(expiryRow, login, tx).ConfigureAwait(false); // LifetimePoints unchanged on expiry — only CurrentPoints decreases await _enrollmentDal.UpdateEnrollmentPoints( enrollmentId, newCurrentPoints, enrollDto.LifetimePoints, login, tx) .ConfigureAwait(false); await _queryExecutor.CommitAsync(tx).ConfigureAwait(false); } catch { await _queryExecutor.RollbackAsync(tx).ConfigureAwait(false); throw; } _logger.LogInformation("Expired {Points} points for Enrollment {EnrollmentId}", pointsExpiring, enrollmentId); } catch (Exception ex) { _logger.LogError(ex, "ExpirePoints failed for Enrollment {EnrollmentId}", enrollmentId); // Continue processing remaining enrollments } } } catch (Exception ex) { _logger.LogError(ex, "ExpirePointsAsync failed"); throw; } } } }