forked from gongxuegit/tiku-backend.net
feat: modularize external providers
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Tenancy;
|
||||
@@ -12,55 +13,44 @@ namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class CommerceAdminService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantSecretProtector tenantSecretProtector) : ICommerceAdminService
|
||||
ITenantSecretProtector tenantSecretProtector,
|
||||
ITenantExternalProviderConfigService providerConfigService) : ICommerceAdminService
|
||||
{
|
||||
public async Task<IReadOnlyCollection<TenantPaymentAccountItem>> GetPaymentAccountsAsync(
|
||||
public async Task<IReadOnlyCollection<TenantPaymentProviderItem>> GetPaymentAccountsAsync(
|
||||
CommerceAdminActor actor,
|
||||
CommerceAdminQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var accounts = dbContext.TenantPaymentAccounts.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Provider))
|
||||
{
|
||||
var provider = NormalizeProvider(query.Provider);
|
||||
accounts = accounts.Where(item => item.Provider == provider);
|
||||
}
|
||||
var accounts = await providerConfigService.GetProvidersAsync(
|
||||
actor.TenantId,
|
||||
TenantExternalProviderCapability.Payment,
|
||||
string.IsNullOrWhiteSpace(query.Provider) ? null : NormalizeProvider(query.Provider),
|
||||
query.Limit,
|
||||
cancellationToken);
|
||||
|
||||
return await accounts
|
||||
.OrderBy(item => item.Provider)
|
||||
.Take(Math.Clamp(query.Limit ?? 50, 1, 100))
|
||||
.Select(item => ToPaymentAccountItem(item))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return accounts.Select(ToPaymentAccountItem).ToArray();
|
||||
}
|
||||
|
||||
public async Task<TenantPaymentAccountItem> UpsertPaymentAccountAsync(
|
||||
public async Task<TenantPaymentProviderItem> UpsertPaymentAccountAsync(
|
||||
CommerceAdminActor actor,
|
||||
UpsertPaymentAccountCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
AssertNoSecrets(command.ConfigPublic, "config_public");
|
||||
var provider = NormalizeProvider(command.Provider);
|
||||
var account = await dbContext.TenantPaymentAccounts
|
||||
.SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Provider == provider, cancellationToken);
|
||||
if (account is null)
|
||||
{
|
||||
account = new TenantPaymentAccount
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
Provider = provider
|
||||
};
|
||||
dbContext.TenantPaymentAccounts.Add(account);
|
||||
}
|
||||
var account = await providerConfigService.UpsertProviderAsync(
|
||||
actor.TenantId,
|
||||
new UpsertTenantExternalProviderCommand(
|
||||
TenantExternalProviderCapability.Payment,
|
||||
command.Provider,
|
||||
command.Status,
|
||||
command.DisplayName,
|
||||
command.SecretRef,
|
||||
command.Priority,
|
||||
WithPaymentMode(command.ConfigPublic, command.Mode),
|
||||
JsonObjectOrDefault(default)),
|
||||
cancellationToken);
|
||||
|
||||
account.Mode = command.Mode;
|
||||
account.DisplayName = command.DisplayName?.Trim();
|
||||
account.Status = command.Status;
|
||||
account.ConfigPublic = JsonObjectOrDefault(command.ConfigPublic);
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return ToPaymentAccountItem(account);
|
||||
}
|
||||
|
||||
@@ -588,8 +578,18 @@ internal sealed class CommerceAdminService(
|
||||
}
|
||||
}
|
||||
|
||||
private static TenantPaymentAccountItem ToPaymentAccountItem(TenantPaymentAccount item) =>
|
||||
new(item.Id, item.Provider, item.Mode.ToString(), item.DisplayName, item.Status.ToString(), item.ConfigPublic, item.CreatedAt, item.UpdatedAt);
|
||||
private static TenantPaymentProviderItem ToPaymentAccountItem(TenantExternalProviderItem item) =>
|
||||
new(
|
||||
item.Id,
|
||||
item.Provider,
|
||||
GetJsonString(item.ConfigPublic, "mode") ?? "TenantCollect",
|
||||
item.DisplayName,
|
||||
item.Status,
|
||||
item.SecretRef,
|
||||
item.Priority,
|
||||
item.ConfigPublic,
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt);
|
||||
|
||||
private static TenantSecretItem ToSecretItem(TenantSecret item) =>
|
||||
new(item.Id, item.Purpose, item.Provider, item.SecretKey, item.SecretRef, item.Status.ToString(), item.RotatedAt, item.ExpiresAt, item.UpdatedAt);
|
||||
@@ -651,6 +651,40 @@ internal sealed class CommerceAdminService(
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonElement WithPaymentMode(JsonElement element, string? mode)
|
||||
{
|
||||
var values = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
values[property.Name] = property.Value.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
values["mode"] = JsonSerializer.SerializeToElement(
|
||||
string.IsNullOrWhiteSpace(mode) ? "TenantCollect" : mode.Trim());
|
||||
return JsonSerializer.SerializeToElement(values);
|
||||
}
|
||||
|
||||
private static string? GetJsonString(JsonElement element, params string[] keys)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return value.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonElement JsonObjectOrDefault(JsonElement element) =>
|
||||
element.ValueKind == JsonValueKind.Object
|
||||
? element.Clone()
|
||||
|
||||
@@ -1,49 +1,41 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class PaymentProviderConfigService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantSecretService tenantSecretService) : IPaymentProviderConfigService
|
||||
ITenantExternalProviderConfigService providerConfigService) : 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)
|
||||
TenantExternalProviderAccount account;
|
||||
try
|
||||
{
|
||||
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(
|
||||
account = await providerConfigService.GetActiveProviderAsync(
|
||||
tenantId,
|
||||
GetString(account.ConfigPublic, "secretRef", "secret_ref") ?? string.Empty,
|
||||
TenantExternalProviderCapability.Payment,
|
||||
NormalizeProvider(provider),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (TenantExternalProviderException exception)
|
||||
{
|
||||
throw new PaymentProviderException(exception.Message, "payment_provider_not_configured");
|
||||
}
|
||||
|
||||
return new PaymentProviderAccount(
|
||||
account.TenantId,
|
||||
normalizedProvider,
|
||||
account.Mode.ToString(),
|
||||
account.Provider,
|
||||
GetString(account.ConfigPublic, "mode") ?? "TenantCollect",
|
||||
account.ConfigPublic,
|
||||
secretPayload);
|
||||
account.SecretPayload);
|
||||
}
|
||||
|
||||
private static string NormalizeProvider(string provider)
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class TenantSecretEncryptionOptions
|
||||
string.Equals(options.MasterKey, DevelopmentMasterKey, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
internal interface ITenantSecretProtector
|
||||
public interface ITenantSecretProtector
|
||||
{
|
||||
ProtectedTenantSecret Protect(Guid tenantId, string secretRef, JsonElement payload);
|
||||
|
||||
@@ -47,13 +47,13 @@ internal interface ITenantSecretProtector
|
||||
byte[] tag);
|
||||
}
|
||||
|
||||
internal sealed record ProtectedTenantSecret(
|
||||
public sealed record ProtectedTenantSecret(
|
||||
string KeyId,
|
||||
byte[] Ciphertext,
|
||||
byte[] Nonce,
|
||||
byte[] Tag);
|
||||
|
||||
internal sealed class TenantSecretProtector(
|
||||
public sealed class TenantSecretProtector(
|
||||
Microsoft.Extensions.Options.IOptions<TenantSecretEncryptionOptions> options) : ITenantSecretProtector
|
||||
{
|
||||
private readonly TenantSecretEncryptionOptions options = options.Value;
|
||||
|
||||
Reference in New Issue
Block a user