using LiteCharms.Features.Hasher; using LiteCharms.Features.Hasher.Configuration; using LiteCharms.Features.Mediator; using LiteCharms.Features.MidrandBooks.Orders; using LiteCharms.Features.MidrandBooks.Payments.Models; namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers; public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions hasherOptions, ILogger logger) : INotificationHandler { private readonly HasherSettings hasherSettings = hasherOptions.Value; public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken) { using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(PayfastPaymentConfirmationReceivedEvent).Name}"); activity?.SetTag("event.correlation_id", notification.CorrelationId); await using var scope = services.CreateAsyncScope(); var hashService = scope.ServiceProvider.GetRequiredService(); var orderService = scope.ServiceProvider.GetRequiredService(); var paymentService = scope.ServiceProvider.GetRequiredService(); var payfastService = scope.ServiceProvider.GetRequiredService(); var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null."); var dict = payload.ToParamDictionary(); var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase); if (localSignature.IsFailed) throw new Exception("Failed to generate local signature for incoming webhook payload."); if (!string.Equals(localSignature.Value, payload.Signature, StringComparison.OrdinalIgnoreCase)) { logger.LogCritical("Incoming webhook signature verification failed. Possible payload tampering."); return; } var hashResult = hashService.DecodeLongIdHash(payload.MerchantPaymentId!); if (hashResult.IsFailed) throw new Exception("Failed to decode application tracking hash key identifier."); var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken); if (orderResult.IsFailed) throw new Exception("Target system order entity context cannot be traced."); var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.Id, cancellationToken); if (paymentResult.IsFailed) throw new Exception("Target payment ledger entity cannot be resolved."); decimal.TryParse(payload.AmountGross, CultureInfo.InvariantCulture, out var gross); decimal.TryParse(payload.AmountFee, CultureInfo.InvariantCulture, out var fee); decimal.TryParse(payload.AmountNet, CultureInfo.InvariantCulture, out var net); string status = payload.PaymentStatus ?? "UNKNOWN"; var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken); if (isAlreadyProcessed.Value) { logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping validation routines.", payload.MerchantPaymentId); return; } if (notification.PerformBackgroundChecks) { var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, notification.AllowLoopback, cancellationToken); if (isHostValid.IsFailed) throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); if (!isHostValid.Value) throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks."); var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); if (!isAmountValid.Value) throw new Exception("Security validation exception: Transaction cost variance bounds breached."); var paramList = new List(); foreach (var kvp in dict) { if (!string.IsNullOrEmpty(kvp.Value)) { string encoded = HttpUtility.UrlEncode(kvp.Value.Trim()); string safeValue = PayfastService.PercentEncodingRegex.Replace(encoded, m => m.Value.ToLowerInvariant()); paramList.Add($"{kvp.Key}={safeValue}"); } } string rawParamString = string.Join("&", paramList); var serverConfirmation = await payfastService.ValidateServerConfirmationAsync(rawParamString, isSandbox: true, cancellationToken); if (serverConfirmation.IsFailed) throw new Exception("Security validation exception: Payfast central handshake server rejected payload legitimacy."); } await payfastService.WriteLedgerEntryAsync(new CreateGatewayLedgerEntry { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, MerchantPaymentId = payload.MerchantPaymentId!, PayfastPaymentId = payload.PaymentId, CustomerEmail = payload.EmailAddress, AmountFee = fee, AmountGross = gross, AmountNet = net, PaymentStatus = status, }, cancellationToken); if (status.Equals("COMPLETE", StringComparison.OrdinalIgnoreCase)) { var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, PaymentGatewayReference = payload.PaymentId!, Status = LedgerStatuses.Completed, CustomerId = orderResult.Value.CustomerId, }, cancellationToken); if (ledgerWriteResult.IsFailed) throw new Exception("Failed to write ledger entry for payment confirmation."); var completePaymentResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken); if (completePaymentResult.IsFailed) throw new Exception("Failed to update payment status to 'Paid' for payment confirmation."); var updateOrderResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); if (updateOrderResult.IsFailed) throw new Exception("Failed to update order status to 'Completed' for payment confirmation."); logger.LogInformation("Order payment verified secure and cleared successfully."); } else { LedgerStatuses ledgerStatus; if (status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase)) ledgerStatus = LedgerStatuses.Cancelled; else ledgerStatus = LedgerStatuses.Failed; var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, PaymentGatewayReference = payload.PaymentId!, Status = ledgerStatus, CustomerId = orderResult.Value.CustomerId, }, cancellationToken); logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status); } activity?.SetStatus(ActivityStatusCode.Ok); } }