Files
tiku-backend.net/Tiku.Infrastructure/Commerce/PaymentCallbacks/CommercePaymentNotificationService.cs
xiong 33375a38d7
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(architecture): harden module boundaries
2026-08-04 12:10:36 +08:00

127 lines
4.8 KiB
C#

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<PaymentNotificationProcessResult> ProcessPaymentNotificationAsync(
Guid tenantId,
string provider,
IReadOnlyDictionary<string, string> 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);
}
}