feat: add payment notifications and entitlement fulfillment

This commit is contained in:
xiong
2026-07-26 19:38:53 +08:00
parent 494039973d
commit 36b64dcf70
6 changed files with 570 additions and 13 deletions

View File

@@ -164,6 +164,81 @@ public sealed class CommerceEndpointTests
Assert.Equal(firstPayment!.Id, secondPayment!.Id);
}
[Fact]
public async Task Payment_notification_marks_order_paid_once()
{
await using var factory = new ApiTestFactory(paymentProviderGateway: new FakePaymentGateway());
var seed = await SeedLoginUserAsync(factory);
using var client = factory.CreateClient();
await LoginAsync(client, seed);
var order = await CreateOrderAsync(client, seed.PlanId);
var request = new CreateCommercePaymentDto
{
OrderNo = order.OrderNo,
Provider = "manual",
Method = "manual"
};
await client.PostAsJsonAsync("/api/commerce/payments", request);
client.DefaultRequestHeaders.Authorization = null;
var payload = new
{
eventId = "notify-001",
orderNo = order.OrderNo,
providerTradeNo = "trade-notify-001",
amountCents = order.AmountCents,
paid = true,
signatureValid = true
};
var firstNotify = await client.PostAsJsonAsync(
$"/api/commerce/payments/notify/wechat-pay?tenantId={seed.TenantId}",
payload);
var secondNotify = await client.PostAsJsonAsync(
$"/api/commerce/payments/notify/wechat-pay?tenantId={seed.TenantId}",
payload);
await LoginAsync(client, seed);
var entitlement = await (await client.GetAsync("/api/commerce/entitlements/current"))
.Content
.ReadFromJsonAsync<CurrentEntitlementItem>();
using var scope = factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
var events = dbContext.PaymentEvents.Count(item => item.EventId == "notify-001");
Assert.Equal(HttpStatusCode.OK, firstNotify.StatusCode);
Assert.Equal(HttpStatusCode.OK, secondNotify.StatusCode);
Assert.True(entitlement!.IsActive);
Assert.Equal(1, events);
}
[Fact]
public async Task Invalid_payment_notification_signature_does_not_pay_order()
{
await using var factory = new ApiTestFactory(paymentProviderGateway: new FakePaymentGateway());
var seed = await SeedLoginUserAsync(factory);
using var client = factory.CreateClient();
await LoginAsync(client, seed);
var order = await CreateOrderAsync(client, seed.PlanId);
client.DefaultRequestHeaders.Authorization = null;
var notifyResponse = await client.PostAsJsonAsync(
$"/api/commerce/payments/notify/wechat-pay?tenantId={seed.TenantId}",
new
{
eventId = "notify-invalid",
orderNo = order.OrderNo,
providerTradeNo = "trade-invalid",
amountCents = order.AmountCents,
paid = true,
signatureValid = false
});
await LoginAsync(client, seed);
var orderResponse = await client.GetAsync($"/api/commerce/orders/{order.OrderNo}");
var currentOrder = await orderResponse.Content.ReadFromJsonAsync<CommerceOrderItem>();
Assert.Equal(HttpStatusCode.BadRequest, notifyResponse.StatusCode);
Assert.Equal("Pending", currentOrder!.Status);
}
private static async Task<CommerceOrderItem> CreateOrderAsync(HttpClient client, Guid planId)
{
var response = await client.PostAsJsonAsync(
@@ -312,7 +387,49 @@ public sealed class CommerceEndpointTests
PaymentNotificationRequest request,
CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
var body = request.Body;
var eventId = GetString(body, "eventId") ?? Guid.NewGuid().ToString("N");
var orderNo = GetString(body, "orderNo") ?? throw new InvalidOperationException("orderNo missing");
var tradeNo = GetString(body, "providerTradeNo");
var amount = GetInt(body, "amountCents");
var paid = GetBoolean(body, "paid");
var signatureValid = GetBoolean(body, "signatureValid");
return Task.FromResult(new PaymentNotificationResult(
provider,
"payment_notify",
eventId,
orderNo,
tradeNo,
amount,
paid,
signatureValid,
DateTimeOffset.UtcNow,
body));
}
private static string? GetString(JsonElement element, string key)
{
return element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(key, out var property) &&
property.ValueKind == JsonValueKind.String
? property.GetString()
: null;
}
private static int GetInt(JsonElement element, string key)
{
return element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(key, out var property) &&
property.TryGetInt32(out var value)
? value
: 0;
}
private static bool GetBoolean(JsonElement element, string key)
{
return element.ValueKind == JsonValueKind.Object &&
element.TryGetProperty(key, out var property) &&
property.ValueKind == JsonValueKind.True;
}
}
}