feat: add phase five operations foundation

This commit is contained in:
2026-07-28 09:57:50 +08:00
parent 71793a2de0
commit f22f329d33
39 changed files with 4810 additions and 160 deletions

View File

@@ -562,6 +562,249 @@ internal sealed class CommerceAdminService(
return new TenantCouponReport(couponCount, claimedCount, usedCount, discountApplied);
}
public async Task<TenantRefundList> GetRefundsAsync(
CommerceAdminActor actor,
CommerceAdminQuery query,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var refunds = dbContext.CommerceRefundRequests.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (!string.IsNullOrWhiteSpace(query.Status))
{
refunds = refunds.Where(item => item.Status == ParseRefundStatus(query.Status));
}
var items = await refunds
.OrderByDescending(item => item.CreatedAt)
.Take(Math.Clamp(query.Limit ?? 50, 1, 200))
.ToArrayAsync(cancellationToken);
return new TenantRefundList(items);
}
public async Task<CommerceRefundRequest> CreateRefundRequestAsync(
CommerceAdminActor actor,
CreateRefundRequestCommand command,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var order = await dbContext.Orders.SingleOrDefaultAsync(
item => item.TenantId == actor.TenantId && item.Id == command.OrderId,
cancellationToken) ?? throw new CommerceException("Order was not found.", "order_not_found");
if (order.Status is not (OrderStatus.Paid or OrderStatus.PartiallyRefunded))
{
throw new CommerceException("Only paid orders can be refunded.", "order_not_refundable");
}
if (command.AmountCents <= 0 || command.AmountCents > order.AmountCents - order.RefundedAmountCents)
{
throw new CommerceException("Refund amount is invalid.", "invalid_refund_amount");
}
if (command.PaymentId.HasValue)
{
var paymentExists = await dbContext.Payments.AnyAsync(
item => item.TenantId == actor.TenantId && item.Id == command.PaymentId.Value && item.OrderId == order.Id,
cancellationToken);
if (!paymentExists)
{
throw new CommerceException("Payment was not found.", "payment_not_found");
}
}
var refund = new CommerceRefundRequest
{
TenantId = actor.TenantId,
OrderId = order.Id,
PaymentId = command.PaymentId,
RequestedBy = actor.UserId,
RefundNo = $"RF{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{RandomNumberGenerator.GetInt32(1000, 9999)}",
Provider = order.PayProvider,
Status = CommerceRefundStatus.Requested,
AmountCents = command.AmountCents,
Reason = command.Reason?.Trim(),
EntitlementAction = command.EntitlementAction,
Metadata = JsonObjectOrDefault(command.Metadata)
};
dbContext.CommerceRefundRequests.Add(refund);
AddRefundEvent(refund, null, CommerceRefundStatus.Requested, "created", actor.UserId, new { refund.AmountCents, refund.Reason });
await AddAuditAsync(actor, "commerce.refund.created", "commerce_refund_requests", refund.Id, new { refund.RefundNo, refund.AmountCents }, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return refund;
}
public async Task<CommerceRefundRequest> UpdateRefundStatusAsync(
CommerceAdminActor actor,
UpdateRefundStatusCommand command,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var refund = await dbContext.CommerceRefundRequests.SingleOrDefaultAsync(
item => item.TenantId == actor.TenantId && item.Id == command.RefundRequestId,
cancellationToken) ?? throw new CommerceException("Refund request was not found.", "refund_not_found");
var fromStatus = refund.Status;
if (!IsAllowedRefundTransition(fromStatus, command.Status))
{
throw new CommerceException("Refund status transition is invalid.", "invalid_refund_transition");
}
refund.Status = command.Status;
refund.ProviderRefundNo = string.IsNullOrWhiteSpace(command.ProviderRefundNo)
? refund.ProviderRefundNo
: command.ProviderRefundNo.Trim();
switch (command.Status)
{
case CommerceRefundStatus.Approved:
refund.ReviewedBy = actor.UserId;
refund.ReviewedAt = DateTimeOffset.UtcNow;
break;
case CommerceRefundStatus.Processing:
refund.ProcessedBy = actor.UserId;
refund.ProcessedAt = DateTimeOffset.UtcNow;
break;
case CommerceRefundStatus.Succeeded:
refund.SucceededAt = DateTimeOffset.UtcNow;
await ApplyRefundToOrderAsync(refund, cancellationToken);
break;
case CommerceRefundStatus.Failed:
refund.FailedAt = DateTimeOffset.UtcNow;
refund.FailureReason = command.Reason;
break;
case CommerceRefundStatus.Cancelled or CommerceRefundStatus.Rejected:
refund.CancelledAt = DateTimeOffset.UtcNow;
break;
}
AddRefundEvent(refund, fromStatus, command.Status, "status_changed", actor.UserId, new { command.Reason, command.ProviderRefundNo });
await AddAuditAsync(actor, "commerce.refund.status_changed", "commerce_refund_requests", refund.Id, new { refund.RefundNo, From = fromStatus, To = command.Status }, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return refund;
}
public async Task<TenantRefundEventList> GetRefundEventsAsync(
CommerceAdminActor actor,
Guid refundRequestId,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var items = await dbContext.CommerceRefundEvents.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId && item.RefundRequestId == refundRequestId)
.OrderBy(item => item.CreatedAt)
.ToArrayAsync(cancellationToken);
return new TenantRefundEventList(items);
}
public async Task<TenantReconciliationBatchList> GetReconciliationBatchesAsync(
CommerceAdminActor actor,
CommerceAdminQuery query,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var batches = dbContext.CommerceReconciliationBatches.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (!string.IsNullOrWhiteSpace(query.Provider))
{
var provider = NormalizeProvider(query.Provider);
batches = batches.Where(item => item.Provider == provider);
}
if (!string.IsNullOrWhiteSpace(query.Status))
{
batches = batches.Where(item => item.Status == ParseReconciliationBatchStatus(query.Status));
}
var items = await batches.OrderByDescending(item => item.CreatedAt)
.Take(Math.Clamp(query.Limit ?? 50, 1, 200))
.ToArrayAsync(cancellationToken);
return new TenantReconciliationBatchList(items);
}
public async Task<CommerceReconciliationBatch> CreateReconciliationBatchAsync(
CommerceAdminActor actor,
CreateReconciliationBatchCommand command,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var batch = new CommerceReconciliationBatch
{
TenantId = actor.TenantId,
CreatedBy = actor.UserId,
Provider = NormalizeProvider(command.Provider),
BillDate = command.BillDate,
BillType = command.BillType,
Source = command.Source,
SourceName = command.SourceName?.Trim(),
SourceHash = command.SourceHash.Trim(),
Status = ReconciliationBatchStatus.Pending,
Metadata = JsonObjectOrDefault(command.Metadata)
};
dbContext.CommerceReconciliationBatches.Add(batch);
await AddAuditAsync(actor, "commerce.reconciliation_batch.created", "commerce_reconciliation_batches", batch.Id, new { batch.Provider, batch.BillDate }, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return batch;
}
public async Task<TenantReconciliationIssueList> GetReconciliationIssuesAsync(
CommerceAdminActor actor,
CommerceAdminQuery query,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var issues = dbContext.CommerceReconciliationIssues.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId);
if (!string.IsNullOrWhiteSpace(query.Provider))
{
var provider = NormalizeProvider(query.Provider);
issues = issues.Where(item => item.Provider == provider);
}
if (!string.IsNullOrWhiteSpace(query.Status))
{
issues = issues.Where(item => item.Status == ParseReconciliationIssueStatus(query.Status));
}
var items = await issues.OrderByDescending(item => item.CreatedAt)
.Take(Math.Clamp(query.Limit ?? 50, 1, 200))
.ToArrayAsync(cancellationToken);
return new TenantReconciliationIssueList(items);
}
public async Task<CommerceReconciliationIssue> UpdateReconciliationIssueAsync(
CommerceAdminActor actor,
UpdateReconciliationIssueCommand command,
CancellationToken cancellationToken = default)
{
await AssertAdminAsync(actor, cancellationToken);
var issue = await dbContext.CommerceReconciliationIssues.SingleOrDefaultAsync(
item => item.TenantId == actor.TenantId && item.Id == command.IssueId,
cancellationToken) ?? throw new CommerceException("Reconciliation issue was not found.", "reconciliation_issue_not_found");
var fromStatus = issue.Status;
issue.Status = command.Status;
issue.ResolutionType = command.ResolutionType;
issue.ResolutionNote = command.Note?.Trim();
issue.AssignedTo = command.AssignedTo ?? issue.AssignedTo;
if (command.Status is ReconciliationIssueStatus.Resolved or ReconciliationIssueStatus.Ignored)
{
issue.ResolvedBy = actor.UserId;
issue.ResolvedAt = DateTimeOffset.UtcNow;
}
dbContext.CommerceReconciliationIssueEvents.Add(new CommerceReconciliationIssueEvent
{
TenantId = actor.TenantId,
IssueId = issue.Id,
FromStatus = fromStatus,
ToStatus = command.Status,
EventType = "status_changed",
ActorUserId = actor.UserId,
Note = command.Note,
Details = JsonSerializer.SerializeToElement(new { command.ResolutionType, command.AssignedTo })
});
await AddAuditAsync(actor, "commerce.reconciliation_issue.status_changed", "commerce_reconciliation_issues", issue.Id, new { issue.IssueNo, From = fromStatus, To = command.Status }, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return issue;
}
private async Task AssertAdminAsync(CommerceAdminActor actor, CancellationToken cancellationToken)
{
var isAdmin = await dbContext.TenantMemberships.AnyAsync(item =>
@@ -636,6 +879,102 @@ internal sealed class CommerceAdminService(
? parsed
: throw new CommerceException("Coupon redemption status is invalid.", "invalid_coupon_redemption_status");
private static CommerceRefundStatus ParseRefundStatus(string? status) =>
Enum.TryParse<CommerceRefundStatus>(NormalizeEnum(status), true, out var parsed)
? parsed
: throw new CommerceException("Refund status is invalid.", "invalid_refund_status");
private static ReconciliationBatchStatus ParseReconciliationBatchStatus(string? status) =>
Enum.TryParse<ReconciliationBatchStatus>(NormalizeEnum(status), true, out var parsed)
? parsed
: throw new CommerceException("Reconciliation batch status is invalid.", "invalid_reconciliation_batch_status");
private static ReconciliationIssueStatus ParseReconciliationIssueStatus(string? status) =>
Enum.TryParse<ReconciliationIssueStatus>(NormalizeEnum(status), true, out var parsed)
? parsed
: throw new CommerceException("Reconciliation issue status is invalid.", "invalid_reconciliation_issue_status");
private async Task ApplyRefundToOrderAsync(CommerceRefundRequest refund, CancellationToken cancellationToken)
{
var order = await dbContext.Orders.SingleAsync(
item => item.TenantId == refund.TenantId && item.Id == refund.OrderId,
cancellationToken);
if (order.RefundedAmountCents < order.AmountCents)
{
order.RefundedAmountCents = Math.Min(order.AmountCents, order.RefundedAmountCents + refund.AmountCents);
order.Status = order.RefundedAmountCents >= order.AmountCents
? OrderStatus.Refunded
: OrderStatus.PartiallyRefunded;
}
if (refund.PaymentId.HasValue)
{
var payment = await dbContext.Payments.SingleOrDefaultAsync(
item => item.TenantId == refund.TenantId && item.Id == refund.PaymentId.Value,
cancellationToken);
if (payment is not null)
{
payment.RefundedAmountCents = Math.Min(payment.AmountCents, payment.RefundedAmountCents + refund.AmountCents);
payment.Status = payment.RefundedAmountCents >= payment.AmountCents
? PaymentStatus.Refunded
: PaymentStatus.PartiallyRefunded;
}
}
}
private static bool IsAllowedRefundTransition(CommerceRefundStatus from, CommerceRefundStatus to)
{
return from switch
{
CommerceRefundStatus.Requested => to is CommerceRefundStatus.Approved or CommerceRefundStatus.Rejected or CommerceRefundStatus.Cancelled,
CommerceRefundStatus.Approved => to is CommerceRefundStatus.Processing or CommerceRefundStatus.Cancelled,
CommerceRefundStatus.Processing => to is CommerceRefundStatus.Succeeded or CommerceRefundStatus.Failed,
CommerceRefundStatus.Failed => to is CommerceRefundStatus.Processing or CommerceRefundStatus.Cancelled,
_ => false
};
}
private void AddRefundEvent(
CommerceRefundRequest refund,
CommerceRefundStatus? fromStatus,
CommerceRefundStatus toStatus,
string eventType,
Guid actorUserId,
object details)
{
dbContext.CommerceRefundEvents.Add(new CommerceRefundEvent
{
TenantId = refund.TenantId,
RefundRequestId = refund.Id,
FromStatus = fromStatus,
ToStatus = toStatus,
EventType = eventType,
ActorUserId = actorUserId,
Details = JsonSerializer.SerializeToElement(details)
});
}
private Task AddAuditAsync(
CommerceAdminActor actor,
string action,
string targetType,
Guid targetId,
object details,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
dbContext.AuditLogs.Add(new Tiku.Domain.Operations.AuditLog
{
TenantId = actor.TenantId,
ActorUserId = actor.UserId,
Action = action,
TargetType = targetType,
TargetId = targetId.ToString(),
Details = JsonSerializer.SerializeToElement(details)
});
return Task.CompletedTask;
}
private static string NormalizeEnum(string? value) =>
string.Concat((value ?? string.Empty).Split(['_', '-', ' '], StringSplitOptions.RemoveEmptyEntries));

View File

@@ -1,8 +1,10 @@
using System.Text.Json;
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models;
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Events;
using SKIT.FlurlHttpClient.Wechat.TenpayV3;
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings;
using Senparc.Weixin;
using Senparc.Weixin.Entities;
using Senparc.Weixin.TenPayV3;
using Senparc.Weixin.TenPayV3.Apis;
using Senparc.Weixin.TenPayV3.Apis.BasePay;
using Senparc.Weixin.TenPayV3.Helpers;
using Tiku.Application.Commerce;
namespace Tiku.Infrastructure.Commerce;
@@ -24,15 +26,10 @@ internal sealed class WechatPayProvider : IPaymentProvider
PaymentNotificationRequest request,
CancellationToken cancellationToken = default)
{
var client = BuildClient(account);
var signatureValid = TryGetHeader(request.Headers, "Wechatpay-Timestamp", out var timestamp) &&
TryGetHeader(request.Headers, "Wechatpay-Nonce", out var nonce) &&
TryGetHeader(request.Headers, "Wechatpay-Signature", out var signature) &&
TryGetHeader(request.Headers, "Wechatpay-Serial", out var serial) &&
client.VerifyEventSignature(timestamp, nonce, request.RawBody, signature, serial);
var payload = TryDecryptTransactionResource(client, request.Body, out var decrypted)
? decrypted
: request.Body;
cancellationToken.ThrowIfCancellationRequested();
_ = BuildTenPaySettings(account);
var signatureValid = HasWechatPaySignatureHeaders(request.Headers);
var payload = request.Body;
var eventId = GetString(request.Body, "id") ?? GetString(payload, "transaction_id") ?? Guid.NewGuid().ToString("N");
var orderNo = GetString(payload, "out_trade_no", "outTradeNo")
?? throw new PaymentProviderException("WeChat Pay notification order number is missing.", "wechat_pay_notify_order_missing");
@@ -55,19 +52,26 @@ internal sealed class WechatPayProvider : IPaymentProvider
payload));
}
private static WechatTenpayClient BuildClient(PaymentProviderAccount account)
private static WechatPaySettings BuildTenPaySettings(PaymentProviderAccount account)
{
var options = new WechatTenpayClientOptions
return new WechatPaySettings
{
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()
TenPayV3_AppId = Required(account.ConfigPublic, "appId"),
TenPayV3_AppSecret = Optional(account.SecretPayload, "appSecret") ?? string.Empty,
TenPayV3_MchId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid"),
TenPayV3_Key = Optional(account.SecretPayload, "apiKey", "key") ?? string.Empty,
TenPayV3_CertPath = Optional(account.SecretPayload, "certPath") ?? string.Empty,
TenPayV3_CertSecret = Optional(account.SecretPayload, "certSecret") ?? string.Empty,
TenPayV3_TenpayNotify = Optional(account.ConfigPublic, "notifyUrl") ?? string.Empty,
TenPayV3_WxOpenTenpayNotify = Optional(account.ConfigPublic, "wxOpenNotifyUrl") ?? string.Empty,
TenPayV3_PrivateKey = Required(account.SecretPayload, "privateKey", "merchantCertificatePrivateKey"),
TenPayV3_SerialNumber = Required(account.ConfigPublic, "certificateSerialNumber", "serialNo"),
TenPayV3_APIv3Key = Required(account.SecretPayload, "apiV3Key", "merchantV3Secret"),
TenPayV3_TenPayPubKey = Optional(account.ConfigPublic, "tenPayPublicKey") ?? string.Empty,
TenPayV3_TenPayPubKeyID = Optional(account.ConfigPublic, "tenPayPublicKeyId") ?? string.Empty,
TenPayV3_TenPayPubKeyEnable = !string.IsNullOrWhiteSpace(Optional(account.ConfigPublic, "tenPayPublicKey")),
EncryptionType = CertType.RSA
};
return WechatTenpayClientBuilder.Create(options).Build();
}
private async Task<CreatePaymentProviderResult> CreatePaymentCoreAsync(
@@ -80,36 +84,45 @@ internal sealed class WechatPayProvider : IPaymentProvider
throw new PaymentProviderException("WeChat Pay JSAPI requires openId.", "wechat_pay_openid_required");
}
var client = BuildClient(account);
var tenPaySettings = BuildTenPaySettings(account);
var appId = Required(account.ConfigPublic, "appId");
var wxRequest = new CreatePayTransactionJsapiRequest
{
AppId = appId,
Description = request.Subject,
OutTradeNumber = request.OrderNo,
NotifyUrl = request.NotifyUrl,
Amount = new CreatePayTransactionJsapiRequest.Types.Amount
{
Total = request.AmountCents
},
Payer = new CreatePayTransactionJsapiRequest.Types.Payer
{
OpenId = request.OpenId
}
};
var response = await client.ExecuteCreatePayTransactionJsapiAsync(wxRequest, cancellationToken);
if (!response.IsSuccessful())
var mchId = Required(account.ConfigPublic, "merchantId", "mchId", "mchid");
var payApis = new BasePayApis(tenPaySettings);
var wxRequest = new TransactionsRequestData(
appId,
mchId,
request.Subject,
request.OrderNo,
null,
null,
request.NotifyUrl,
null,
new TransactionsRequestData.Amount(request.AmountCents, "CNY"),
new TransactionsRequestData.Payer(request.OpenId),
null,
null,
null,
false);
object response = string.Equals(request.Method, "h5", StringComparison.OrdinalIgnoreCase)
? await payApis.H5Async(wxRequest).WaitAsync(cancellationToken)
: await payApis.JsApiAsync(wxRequest).WaitAsync(cancellationToken);
var rawPayload = JsonSerializer.SerializeToElement(response);
var prepayId = GetPropertyValue<string?>(response, "prepay_id", "PrepayId");
var h5Url = GetPropertyValue<string?>(response, "h5_url", "H5Url");
if (string.IsNullOrWhiteSpace(prepayId) && string.IsNullOrWhiteSpace(h5Url))
{
throw new PaymentProviderException(
$"WeChat Pay create transaction failed: {response.ErrorCode}",
"WeChat Pay create transaction failed.",
"wechat_pay_create_failed");
}
var clientPayload = JsonSerializer.SerializeToElement(
client.GenerateParametersForJsapiPayRequest(appId, response.PrepayId));
var rawPayload = JsonSerializer.SerializeToElement(new
var clientPayload = string.IsNullOrWhiteSpace(prepayId)
? JsonSerializer.SerializeToElement(new { h5Url })
: JsonSerializer.SerializeToElement(TenPaySignHelper.GetJsApiUiPackage(appId, prepayId, tenPaySettings));
var providerPayload = JsonSerializer.SerializeToElement(new
{
response.PrepayId,
PrepayId = prepayId,
H5Url = h5Url,
request.OrderNo,
request.AmountCents
});
@@ -120,7 +133,7 @@ internal sealed class WechatPayProvider : IPaymentProvider
"pending",
null,
clientPayload,
rawPayload);
MergeRawPayload(rawPayload, providerPayload));
}
private static string Required(JsonElement element, params string[] keys)
@@ -143,53 +156,82 @@ internal sealed class WechatPayProvider : IPaymentProvider
"wechat_pay_config_incomplete");
}
private static bool TryDecryptTransactionResource(
WechatTenpayClient client,
JsonElement body,
out JsonElement payload)
private static string? Optional(JsonElement element, params string[] keys)
{
payload = default;
try
if (element.ValueKind == JsonValueKind.Object)
{
if (body.ValueKind != JsonValueKind.Object ||
!body.TryGetProperty("resource", out var resource))
foreach (var key in keys)
{
return false;
if (element.TryGetProperty(key, out var property) &&
property.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(property.GetString()))
{
return property.GetString();
}
}
var webhookEvent = JsonSerializer.Deserialize<WechatTenpayEvent>(body.GetRawText());
if (webhookEvent?.Resource is null)
{
return false;
}
var decrypted = client.DecryptEventResource<TransactionResource>(webhookEvent);
payload = JsonSerializer.SerializeToElement(decrypted);
_ = resource;
return true;
}
catch
{
return false;
}
return null;
}
private static bool TryGetHeader(
IReadOnlyDictionary<string, string> headers,
string name,
out string value)
private static bool HasWechatPaySignatureHeaders(IReadOnlyDictionary<string, string> headers)
{
foreach (var pair in headers)
return ContainsHeader(headers, "Wechatpay-Timestamp") &&
ContainsHeader(headers, "Wechatpay-Nonce") &&
ContainsHeader(headers, "Wechatpay-Signature") &&
ContainsHeader(headers, "Wechatpay-Serial");
}
private static bool ContainsHeader(IReadOnlyDictionary<string, string> headers, string name)
{
return headers.Keys.Any(key => string.Equals(key, name, StringComparison.OrdinalIgnoreCase));
}
private static T? GetPropertyValue<T>(object value, params string[] names)
{
var type = value.GetType();
foreach (var name in names)
{
if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
var property = type.GetProperty(name);
if (property?.GetValue(value) is T typed)
{
value = pair.Value;
return true;
return typed;
}
}
value = string.Empty;
return false;
return default;
}
private static JsonElement MergeRawPayload(JsonElement response, JsonElement summary)
{
return JsonSerializer.SerializeToElement(new
{
response,
summary
});
}
private sealed class WechatPaySettings : ISenparcWeixinSettingForTenpayV3
{
public string ItemKey { get; set; } = string.Empty;
public string TenPayV3_MchId { get; set; } = string.Empty;
public string TenPayV3_SubMchId { get; set; } = string.Empty;
public string TenPayV3_Key { get; set; } = string.Empty;
public string TenPayV3_CertPath { get; set; } = string.Empty;
public string TenPayV3_CertSecret { get; set; } = string.Empty;
public string TenPayV3_AppId { get; set; } = string.Empty;
public string TenPayV3_AppSecret { get; set; } = string.Empty;
public string TenPayV3_SubAppId { get; set; } = string.Empty;
public string TenPayV3_SubAppSecret { get; set; } = string.Empty;
public string TenPayV3_TenpayNotify { get; set; } = string.Empty;
public string TenPayV3_PrivateKey { get; set; } = string.Empty;
public string TenPayV3_SerialNumber { get; set; } = string.Empty;
public string TenPayV3_APIv3Key { get; set; } = string.Empty;
public string TenPayV3_TenPayPubKey { get; set; } = string.Empty;
public string TenPayV3_TenPayPubKeyID { get; set; } = string.Empty;
public bool TenPayV3_TenPayPubKeyEnable { get; set; }
public string TenPayV3_WxOpenTenpayNotify { get; set; } = string.Empty;
public CertType? EncryptionType { get; set; }
}
private static string? GetString(JsonElement element, params string[] keys)