using LiteCharms.Features.Hasher; 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, ILogger logger) : INotificationHandler { 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 is null."); 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."); var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken); if (isAlreadyProcessed.Value) { logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping processing routines.", payload.MerchantPaymentId); return; } var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross); if (!isAmountValid.Value) throw new Exception("Security validation exception: Transaction cost variance bounds breached (Price Tampering Detected)."); 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"; 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.MerchantPaymentId!, 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'."); var updateOrderResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken); if (updateOrderResult.IsFailed) throw new Exception("Failed to update order status to 'Completed'."); logger.LogInformation("Order payment verified secure and cleared successfully."); } else { LedgerStatuses ledgerStatus = status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase) ? LedgerStatuses.Cancelled : LedgerStatuses.Failed; await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry { OrderId = orderResult.Value.Id, PaymentId = paymentResult.Value.Id, PaymentGatewayReference = payload.MerchantPaymentId!, Status = ledgerStatus, CustomerId = orderResult.Value.CustomerId, }, cancellationToken); logger.LogInformation("Webhook pipeline logged non-success entry to ledger with status: {Status}", status); } activity?.SetStatus(ActivityStatusCode.Ok); } }