using GB5Shared.DTO.Framework.Login; using GoodBooks.PAY.PAYBLL.Integration; using GoodBooks.PAY.PAYBLL.Loyalty; using GoodBooks.PAY.PAYDAL.CustomCode.Cashback; using GoodBooks.PAY.PAYDAL.DTO.Cashback; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace GoodBooks.PAY.PAYBLL.Cashback { // ───────────────────────────────────────────────────────────────────────── // CashbackLedgerBLL // // ProcessPendingPayoutsAsync processes up to 50 PENDING rows per cycle // (PayoutStatus=1, ScheduledPayoutOn <= GETUTCDATE()). // // PayoutMode dispatch: // 1 = CreditNote → IFinanceIntegrationService.PostCreditNoteAsync (Phase 1) // 2 = GatewayPayout → NotImplementedException (Phase 2) // 3 = Wallet → NotImplementedException (Phase 2) // 4 = LoyaltyPoints → ICustomerLoyaltyBLL.EarnPointsAsync // // On success: UpdatePayoutStatus → PayoutStatus=3 (Paid) // On failure, retryable: increment RetryCount → PayoutStatus=4 (Failed) // On failure, exhausted: PayoutStatus=5 (Abandoned) // // Each row is processed independently — failures do not abort the batch. // // Config key: PayJobs:MaxCashbackRetries (default 3) // ───────────────────────────────────────────────────────────────────────── public class CashbackLedgerBLL : ICashbackLedgerBLL { private readonly ICashbackLedgerDAL _dal; private readonly IFinanceIntegrationService _financeService; private readonly ICustomerLoyaltyBLL _loyaltyBll; private readonly IConfiguration _config; private readonly ILogger _logger; public CashbackLedgerBLL( ICashbackLedgerDAL dal, IFinanceIntegrationService financeService, ICustomerLoyaltyBLL loyaltyBll, IConfiguration config, ILogger logger) { _dal = dal; _financeService = financeService; _loyaltyBll = loyaltyBll; _config = config; _logger = logger; } // ── GetCashbackLedger ────────────────────────────────────────────────── public async Task GetCashbackLedger(int cashbackLedgerId, LoginDTO login, CancellationToken ct) { try { return await _dal.GetCashbackLedger(cashbackLedgerId, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetCashbackLedger failed for CashbackLedgerId {CashbackLedgerId}", cashbackLedgerId); throw; } } // ── GetCashbackLedgerByOrder ─────────────────────────────────────────── public async Task GetCashbackLedgerByOrder(int payOrderId, LoginDTO login, CancellationToken ct) { try { return await _dal.GetCashbackLedgerListByOrder(payOrderId, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetCashbackLedgerByOrder failed for PayOrderId {PayOrderId}", payOrderId); throw; } } // ── ProcessPendingPayoutsAsync ───────────────────────────────────────── public async Task ProcessPendingPayoutsAsync(LoginDTO login, CancellationToken ct) { int maxRetries = _config.GetValue("PayJobs:MaxCashbackRetries", 3); IEnumerable pendingRows; try { pendingRows = await _dal.GetPendingPayouts(login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "ProcessPendingPayoutsAsync failed to fetch pending rows"); throw; } foreach (var row in pendingRows) { ct.ThrowIfCancellationRequested(); try { await DispatchPayoutAsync(row, maxRetries, login, ct).ConfigureAwait(false); } catch (Exception ex) { // Each row is independent — log and continue _logger.LogError(ex, "Payout dispatch failed for CashbackLedgerId {CashbackLedgerId} PayoutMode {PayoutMode}", row.CashbackLedgerId, row.PayoutMode); } } } // ── Private helpers ──────────────────────────────────────────────────── private async Task DispatchPayoutAsync( CashbackLedgerDTO row, int maxRetries, LoginDTO login, CancellationToken ct) { switch (row.PayoutMode) { case 1: // CreditNote await DispatchCreditNoteAsync(row, maxRetries, login, ct).ConfigureAwait(false); break; case 2: // GatewayPayout — Phase 2 try { throw new NotImplementedException("Gateway payout — Phase 2"); } catch (Exception ex) { await HandlePayoutFailureAsync(row, ex.Message, maxRetries, login).ConfigureAwait(false); } break; case 3: // Wallet — Phase 2 try { throw new NotImplementedException("Wallet payout — Phase 2"); } catch (Exception ex) { await HandlePayoutFailureAsync(row, ex.Message, maxRetries, login).ConfigureAwait(false); } break; case 4: // LoyaltyPoints await DispatchLoyaltyPointsAsync(row, maxRetries, login, ct).ConfigureAwait(false); break; default: _logger.LogWarning( "Unknown PayoutMode {PayoutMode} for CashbackLedgerId {CashbackLedgerId} — skipping", row.PayoutMode, row.CashbackLedgerId); break; } } private async Task DispatchCreditNoteAsync( CashbackLedgerDTO row, int maxRetries, LoginDTO login, CancellationToken ct) { try { await _financeService.PostCreditNoteAsync(row, login, ct).ConfigureAwait(false); // Success — mark Paid (status 3) var successDto = BuildStatusUpdate(row, payoutStatus: 3, failureReason: null); await _dal.UpdatePayoutStatus(successDto, login).ConfigureAwait(false); _logger.LogInformation( "CreditNote payout succeeded for CashbackLedgerId {CashbackLedgerId}", row.CashbackLedgerId); } catch (Exception ex) { _logger.LogError(ex, "CreditNote payout failed for CashbackLedgerId {CashbackLedgerId} RetryCount {RetryCount}", row.CashbackLedgerId, row.RetryCount); await HandlePayoutFailureAsync(row, ex.Message, maxRetries, login).ConfigureAwait(false); } } private async Task DispatchLoyaltyPointsAsync( CashbackLedgerDTO row, int maxRetries, LoginDTO login, CancellationToken ct) { try { // Convert cashback amount to loyalty points using the customer's active enrollment. // EnrollmentId is not stored on the cashback row — use CustomerId to find it. // EarnPointsAsync resolves the earn rule and enrollment by enrollmentId. // Here we treat CashbackAmt as the "net payable amount" for points calculation. await _loyaltyBll.EarnPointsAsync( enrollmentId: 0, // 0 signals lookup-by-customerId path in BLL payOrderId: row.PayOrderId, netPayableAmt: row.CashbackAmt, productId: null, categoryId: null, payGatewayId: 0, login: login, ct: ct).ConfigureAwait(false); // Success — mark Paid (status 3) var successDto = BuildStatusUpdate(row, payoutStatus: 3, failureReason: null); await _dal.UpdatePayoutStatus(successDto, login).ConfigureAwait(false); _logger.LogInformation( "LoyaltyPoints payout succeeded for CashbackLedgerId {CashbackLedgerId} CustomerId {CustomerId}", row.CashbackLedgerId, row.CustomerId); } catch (Exception ex) { _logger.LogError(ex, "LoyaltyPoints payout failed for CashbackLedgerId {CashbackLedgerId} RetryCount {RetryCount}", row.CashbackLedgerId, row.RetryCount); await HandlePayoutFailureAsync(row, ex.Message, maxRetries, login).ConfigureAwait(false); } } private async Task HandlePayoutFailureAsync( CashbackLedgerDTO row, string errorMessage, int maxRetries, LoginDTO login) { int newRetryCount = row.RetryCount + 1; // PayoutStatus: 4 = Failed (retryable), 5 = Abandoned (exhausted) byte newStatus = newRetryCount >= maxRetries ? (byte)5 : (byte)4; string truncatedError = errorMessage.Length > 500 ? errorMessage[..500] : errorMessage; var failureDto = BuildStatusUpdate(row, payoutStatus: newStatus, failureReason: truncatedError); failureDto.RetryCount = newRetryCount; try { await _dal.UpdatePayoutStatus(failureDto, login).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Failed to update payout status for CashbackLedgerId {CashbackLedgerId}", row.CashbackLedgerId); } if (newStatus == 5) { _logger.LogWarning( "CashbackLedgerId {CashbackLedgerId} abandoned after {RetryCount} retries", row.CashbackLedgerId, newRetryCount); } } private static CashbackLedgerDTO BuildStatusUpdate( CashbackLedgerDTO source, byte payoutStatus, string? failureReason) { return new CashbackLedgerDTO { CashbackLedgerId = source.CashbackLedgerId, PayoutStatus = payoutStatus, PayoutOn = payoutStatus == 3 ? DateTime.UtcNow : source.PayoutOn, RetryCount = source.RetryCount, FailureReason = failureReason, ModifiedById = source.ModifiedById, ModifiedOn = DateTime.UtcNow, TenantId = source.TenantId }; } } }