using System.Text.Json; using Microsoft.EntityFrameworkCore; using Tiku.Application.Commerce; using Tiku.Domain.Commerce; namespace Tiku.Infrastructure.Commerce; internal sealed class CommercePaymentNotificationService(CommerceServiceDependencies dependencies) : CommerceServiceBase(dependencies), ICommercePaymentNotificationService { public async Task ProcessPaymentNotificationAsync( Guid tenantId, string provider, IReadOnlyDictionary headers, string rawBody, JsonElement body, CancellationToken cancellationToken = default) { var normalizedProvider = NormalizeProvider(provider); var notification = await paymentGateway.ParsePaymentNotificationAsync( normalizedProvider, new PaymentNotificationRequest( tenantId, normalizedProvider, headers, rawBody, body), cancellationToken); if (!notification.SignatureValid) throw new CommerceException("Payment notification signature is invalid.", "payment_signature_invalid"); var alreadyProcessed = await commercePersistence.PaymentEvents.AnyAsync( item => item.Provider == normalizedProvider && item.EventId == notification.EventId && item.ProcessedAt != null, cancellationToken); if (alreadyProcessed) return new PaymentNotificationProcessResult( normalizedProvider, notification.EventId, notification.OrderNo, "processed", true); var order = await commercePersistence.Orders .SingleOrDefaultAsync(item => item.TenantId == tenantId && item.OrderNo == notification.OrderNo, cancellationToken) ?? throw new CommerceException("Order was not found.", "order_not_found"); if (order.UserId is null) throw new CommerceException("Order does not belong to a user.", "order_user_missing"); if (order.AmountCents != notification.AmountCents) { commercePersistence.PaymentEvents.Add(new PaymentEvent { TenantId = tenantId, Provider = normalizedProvider, EventType = notification.EventType, EventId = notification.EventId, SignatureValid = true, Payload = notification.RawPayload, Error = "payment_amount_mismatch" }); await commercePersistence.SaveChangesAsync(cancellationToken); throw new CommerceException("Payment amount does not match order amount.", "payment_amount_mismatch"); } var payment = await commercePersistence.Payments .Where(item => item.TenantId == tenantId && item.OrderId == order.Id && item.Provider == normalizedProvider) .OrderByDescending(item => item.CreatedAt) .FirstOrDefaultAsync(cancellationToken); if (payment is null) { payment = new Payment { TenantId = tenantId, OrderId = order.Id, Provider = normalizedProvider, Method = order.PayMethod, Status = PaymentStatus.Pending, AmountCents = order.AmountCents }; commercePersistence.Payments.Add(payment); } if (notification.Paid && order.Status == OrderStatus.Pending) await MarkPaidAsync( new CommerceActor(tenantId, order.UserId.Value), order, payment, notification.ProviderTradeNo, notification.RawPayload, notification.EventType, notification.EventId, notification.SignatureValid, notification.PaidAt, cancellationToken); else commercePersistence.PaymentEvents.Add(new PaymentEvent { TenantId = tenantId, PaymentId = payment.Id, Provider = normalizedProvider, EventType = notification.EventType, EventId = notification.EventId, SignatureValid = notification.SignatureValid, Payload = notification.RawPayload, ProcessedAt = DateTimeOffset.UtcNow }); await commercePersistence.SaveChangesAsync(cancellationToken); return new PaymentNotificationProcessResult( normalizedProvider, notification.EventId, notification.OrderNo, "processed", false); } }