forked from gongxuegit/tiku-backend.net
feat: add payment notifications and entitlement fulfillment
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user