forked from xiongyuxing/tiku-backend.net
feat: add payment notifications and entitlement fulfillment
This commit is contained in:
@@ -187,7 +187,17 @@ public sealed class CommerceService(
|
||||
|
||||
if (IsPaid(result.Status))
|
||||
{
|
||||
await MarkPaidAsync(actor, order, payment, result.ProviderTradeNo, result.RawPayload, cancellationToken);
|
||||
await MarkPaidAsync(
|
||||
actor,
|
||||
order,
|
||||
payment,
|
||||
result.ProviderTradeNo,
|
||||
result.RawPayload,
|
||||
"payment_paid",
|
||||
result.ProviderTradeNo,
|
||||
true,
|
||||
null,
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,15 +248,146 @@ public sealed class CommerceService(
|
||||
: null);
|
||||
}
|
||||
|
||||
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 dbContext.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 dbContext.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)
|
||||
{
|
||||
dbContext.PaymentEvents.Add(new PaymentEvent
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Provider = normalizedProvider,
|
||||
EventType = notification.EventType,
|
||||
EventId = notification.EventId,
|
||||
SignatureValid = true,
|
||||
Payload = notification.RawPayload,
|
||||
Error = "payment_amount_mismatch"
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
throw new CommerceException("Payment amount does not match order amount.", "payment_amount_mismatch");
|
||||
}
|
||||
|
||||
var payment = await dbContext.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
|
||||
};
|
||||
dbContext.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
|
||||
{
|
||||
dbContext.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 dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new PaymentNotificationProcessResult(
|
||||
normalizedProvider,
|
||||
notification.EventId,
|
||||
notification.OrderNo,
|
||||
"processed",
|
||||
false);
|
||||
}
|
||||
|
||||
private async Task MarkPaidAsync(
|
||||
CommerceActor actor,
|
||||
Order order,
|
||||
Payment payment,
|
||||
string? providerTradeNo,
|
||||
JsonElement rawPayload,
|
||||
string eventType,
|
||||
string? eventId,
|
||||
bool signatureValid,
|
||||
DateTimeOffset? paidAtOverride,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var paidAt = DateTimeOffset.UtcNow;
|
||||
var paidAt = paidAtOverride ?? DateTimeOffset.UtcNow;
|
||||
payment.Status = PaymentStatus.Paid;
|
||||
payment.ProviderTradeNo = providerTradeNo ?? payment.ProviderTradeNo;
|
||||
payment.PaidAt = paidAt;
|
||||
@@ -293,9 +434,9 @@ public sealed class CommerceService(
|
||||
TenantId = actor.TenantId,
|
||||
PaymentId = payment.Id,
|
||||
Provider = payment.Provider,
|
||||
EventType = "payment_paid",
|
||||
EventId = payment.ProviderTradeNo,
|
||||
SignatureValid = true,
|
||||
EventType = eventType,
|
||||
EventId = eventId,
|
||||
SignatureValid = signatureValid,
|
||||
Payload = rawPayload,
|
||||
ProcessedAt = paidAt
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user