125 lines
4.6 KiB
C#
125 lines
4.6 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Commerce;
|
|
using Tiku.Domain.Commerce;
|
|
|
|
namespace Tiku.Infrastructure.Commerce;
|
|
|
|
public sealed partial class CommerceService
|
|
{
|
|
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);
|
|
}
|
|
} |