feat: add payment sdk and tenant secret foundation
This commit is contained in:
66
Tiku.Infrastructure/Commerce/AlipayProvider.cs
Normal file
66
Tiku.Infrastructure/Commerce/AlipayProvider.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Text.Json;
|
||||
using Aop.Api;
|
||||
using Tiku.Application.Commerce;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class AlipayProvider : IPaymentProvider
|
||||
{
|
||||
public string Provider => PaymentProviders.Alipay;
|
||||
|
||||
public Task<CreatePaymentProviderResult> CreatePaymentAsync(
|
||||
PaymentProviderAccount account,
|
||||
CreatePaymentProviderRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = BuildClient(account);
|
||||
throw new PaymentProviderException(
|
||||
"Alipay SDK is configured; checkout call is enabled in the commerce checkout batch.",
|
||||
"alipay_checkout_not_enabled");
|
||||
}
|
||||
|
||||
public Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
|
||||
PaymentProviderAccount account,
|
||||
PaymentNotificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = BuildClient(account);
|
||||
throw new PaymentProviderException(
|
||||
"Alipay SDK is configured; notification parsing is enabled in the notification batch.",
|
||||
"alipay_notification_not_enabled");
|
||||
}
|
||||
|
||||
private static DefaultAopClient BuildClient(PaymentProviderAccount account)
|
||||
{
|
||||
return new DefaultAopClient(
|
||||
Required(account.ConfigPublic, "gatewayUrl", "gateway"),
|
||||
Required(account.ConfigPublic, "appId"),
|
||||
Required(account.SecretPayload, "privateKey", "appPrivateKey"),
|
||||
"json",
|
||||
"1.0",
|
||||
"RSA2",
|
||||
Required(account.ConfigPublic, "alipayPublicKey"),
|
||||
"UTF-8",
|
||||
false);
|
||||
}
|
||||
|
||||
private static string Required(JsonElement element, params string[] keys)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(property.GetString()))
|
||||
{
|
||||
return property.GetString()!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new PaymentProviderException(
|
||||
"Alipay provider configuration is incomplete.",
|
||||
"alipay_config_incomplete");
|
||||
}
|
||||
}
|
||||
41
Tiku.Infrastructure/Commerce/ManualPaymentProvider.cs
Normal file
41
Tiku.Infrastructure/Commerce/ManualPaymentProvider.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.Commerce;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class ManualPaymentProvider : IPaymentProvider
|
||||
{
|
||||
public string Provider => PaymentProviders.Manual;
|
||||
|
||||
public Task<CreatePaymentProviderResult> CreatePaymentAsync(
|
||||
PaymentProviderAccount account,
|
||||
CreatePaymentProviderRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
request.OrderNo,
|
||||
request.AmountCents,
|
||||
request.Subject,
|
||||
Message = "manual_payment_requires_tenant_admin_confirmation"
|
||||
});
|
||||
|
||||
return Task.FromResult(new CreatePaymentProviderResult(
|
||||
Provider,
|
||||
request.Method,
|
||||
"pending",
|
||||
null,
|
||||
payload,
|
||||
payload));
|
||||
}
|
||||
|
||||
public Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
|
||||
PaymentProviderAccount account,
|
||||
PaymentNotificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new PaymentProviderException(
|
||||
"Manual payment does not accept provider notifications.",
|
||||
"manual_payment_notification_not_supported");
|
||||
}
|
||||
}
|
||||
80
Tiku.Infrastructure/Commerce/PaymentProviderConfigService.cs
Normal file
80
Tiku.Infrastructure/Commerce/PaymentProviderConfigService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class PaymentProviderConfigService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantSecretService tenantSecretService) : IPaymentProviderConfigService
|
||||
{
|
||||
public async Task<PaymentProviderAccount> GetActiveAccountAsync(
|
||||
Guid tenantId,
|
||||
string provider,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedProvider = NormalizeProvider(provider);
|
||||
var account = await dbContext.TenantPaymentAccounts
|
||||
.AsNoTracking()
|
||||
.Where(item =>
|
||||
item.TenantId == tenantId &&
|
||||
item.Provider == normalizedProvider &&
|
||||
item.Status == TenantPaymentAccountStatus.Active)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (account is null)
|
||||
{
|
||||
throw new PaymentProviderException(
|
||||
"Payment provider account is not configured.",
|
||||
"payment_provider_not_configured");
|
||||
}
|
||||
|
||||
var secretPayload = normalizedProvider == PaymentProviders.Manual
|
||||
? JsonDocument.Parse("{}").RootElement.Clone()
|
||||
: await tenantSecretService.GetActiveSecretPayloadAsync(
|
||||
tenantId,
|
||||
GetString(account.ConfigPublic, "secretRef", "secret_ref") ?? string.Empty,
|
||||
cancellationToken);
|
||||
|
||||
return new PaymentProviderAccount(
|
||||
account.TenantId,
|
||||
normalizedProvider,
|
||||
account.Mode.ToString(),
|
||||
account.ConfigPublic,
|
||||
secretPayload);
|
||||
}
|
||||
|
||||
private static string NormalizeProvider(string provider)
|
||||
{
|
||||
var normalized = provider.Trim().ToLowerInvariant().Replace("-", "_", StringComparison.Ordinal);
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"wechat" or "wechatpay" or "wxpay" or "wx_pay" => PaymentProviders.WechatPay,
|
||||
"ali_pay" => PaymentProviders.Alipay,
|
||||
"" => throw new PaymentProviderException("Payment provider is required.", "payment_provider_required"),
|
||||
_ => normalized
|
||||
};
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement element, params string[] keys)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return property.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
45
Tiku.Infrastructure/Commerce/PaymentProviderGateway.cs
Normal file
45
Tiku.Infrastructure/Commerce/PaymentProviderGateway.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Tiku.Application.Commerce;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class PaymentProviderGateway(
|
||||
IEnumerable<IPaymentProvider> providers,
|
||||
IPaymentProviderConfigService configService) : IPaymentProviderGateway
|
||||
{
|
||||
public async Task<CreatePaymentProviderResult> CreatePaymentAsync(
|
||||
string provider,
|
||||
CreatePaymentProviderRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var resolvedProvider = ResolveProvider(provider);
|
||||
var account = await configService.GetActiveAccountAsync(
|
||||
request.TenantId,
|
||||
resolvedProvider.Provider,
|
||||
cancellationToken);
|
||||
|
||||
return await resolvedProvider.CreatePaymentAsync(account, request, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
|
||||
string provider,
|
||||
PaymentNotificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var resolvedProvider = ResolveProvider(provider);
|
||||
var account = await configService.GetActiveAccountAsync(
|
||||
request.TenantId,
|
||||
resolvedProvider.Provider,
|
||||
cancellationToken);
|
||||
|
||||
return await resolvedProvider.ParsePaymentNotificationAsync(account, request, cancellationToken);
|
||||
}
|
||||
|
||||
private IPaymentProvider ResolveProvider(string provider)
|
||||
{
|
||||
var normalized = provider.Trim().ToLowerInvariant().Replace("-", "_", StringComparison.Ordinal);
|
||||
return providers.FirstOrDefault(item => item.Provider == normalized)
|
||||
?? throw new PaymentProviderException(
|
||||
"Payment provider is not supported.",
|
||||
"payment_provider_not_supported");
|
||||
}
|
||||
}
|
||||
43
Tiku.Infrastructure/Commerce/TenantSecretService.cs
Normal file
43
Tiku.Infrastructure/Commerce/TenantSecretService.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class TenantSecretService(TikuDbContext dbContext) : ITenantSecretService
|
||||
{
|
||||
public async Task<JsonElement> GetActiveSecretPayloadAsync(
|
||||
Guid tenantId,
|
||||
string secretRef,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secretRef))
|
||||
{
|
||||
throw new PaymentProviderException(
|
||||
"Payment provider secret is not configured.",
|
||||
"payment_secret_not_configured");
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var secret = await dbContext.TenantSecrets
|
||||
.AsNoTracking()
|
||||
.Where(item =>
|
||||
item.TenantId == tenantId &&
|
||||
item.SecretRef == secretRef &&
|
||||
item.Status == TenantSecretStatus.Active &&
|
||||
(item.ExpiresAt == null || item.ExpiresAt > now))
|
||||
.Select(item => item.SecretPayload)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (secret.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
|
||||
{
|
||||
throw new PaymentProviderException(
|
||||
"Payment provider secret is not configured.",
|
||||
"payment_secret_not_configured");
|
||||
}
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
68
Tiku.Infrastructure/Commerce/WechatPayProvider.cs
Normal file
68
Tiku.Infrastructure/Commerce/WechatPayProvider.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json;
|
||||
using SKIT.FlurlHttpClient.Wechat.TenpayV3;
|
||||
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings;
|
||||
using Tiku.Application.Commerce;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class WechatPayProvider : IPaymentProvider
|
||||
{
|
||||
public string Provider => PaymentProviders.WechatPay;
|
||||
|
||||
public Task<CreatePaymentProviderResult> CreatePaymentAsync(
|
||||
PaymentProviderAccount account,
|
||||
CreatePaymentProviderRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = BuildClient(account);
|
||||
throw new PaymentProviderException(
|
||||
"WeChat Pay SDK is configured; checkout call is enabled in the commerce checkout batch.",
|
||||
"wechat_pay_checkout_not_enabled");
|
||||
}
|
||||
|
||||
public Task<PaymentNotificationResult> ParsePaymentNotificationAsync(
|
||||
PaymentProviderAccount account,
|
||||
PaymentNotificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = BuildClient(account);
|
||||
throw new PaymentProviderException(
|
||||
"WeChat Pay SDK is configured; notification parsing is enabled in the notification batch.",
|
||||
"wechat_pay_notification_not_enabled");
|
||||
}
|
||||
|
||||
private static WechatTenpayClient BuildClient(PaymentProviderAccount account)
|
||||
{
|
||||
var options = new WechatTenpayClientOptions
|
||||
{
|
||||
MerchantId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"),
|
||||
MerchantV3Secret = Required(account.SecretPayload, "apiV3Key", "merchantV3Secret"),
|
||||
MerchantCertificateSerialNumber = Required(account.ConfigPublic, "certificateSerialNumber", "serialNo"),
|
||||
MerchantCertificatePrivateKey = Required(account.SecretPayload, "privateKey", "merchantCertificatePrivateKey"),
|
||||
PlatformAuthScheme = PlatformAuthScheme.PublicKey,
|
||||
PlatformPublicKeyManager = new InMemoryPublicKeyManager()
|
||||
};
|
||||
|
||||
return WechatTenpayClientBuilder.Create(options).Build();
|
||||
}
|
||||
|
||||
private static string Required(JsonElement element, params string[] keys)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(property.GetString()))
|
||||
{
|
||||
return property.GetString()!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new PaymentProviderException(
|
||||
"WeChat Pay provider configuration is incomplete.",
|
||||
"wechat_pay_config_incomplete");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user