From 1ce5ee4f6f646a64ad4fbe2d53dd6f064a268a9f Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 14 Oct 2024 10:51:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenpayv3):=20=E6=96=B0=E5=A2=9E=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=94=B6=E4=BB=98=E9=80=9A=E5=8F=91=E8=B5=B7=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E9=80=80=E6=AC=BE=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/WechatTenpayV3/Basic_ModelDefinition.md | 2 + ...tTenpayClientExecuteEcommerceExtensions.cs | 22 +++++ ...eateEcommerceAbnormalRefundApplyRequest.cs | 97 +++++++++++++++++++ ...ateEcommerceAbnormalRefundApplyResponse.cs | 9 ++ ...commerceRefundByOutRefundNumberResponse.cs | 36 ++++++- ...teEcommerceAbnormalRefundApplyRequest.json | 9 ++ ...eEcommerceAbnormalRefundApplyResponse.json | 35 +++++++ .../TestCase_RequestEncryptionTests.cs | 56 +++++++++++ 8 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.json diff --git a/docs/WechatTenpayV3/Basic_ModelDefinition.md b/docs/WechatTenpayV3/Basic_ModelDefinition.md index 22a86107..3adb4bf0 100644 --- a/docs/WechatTenpayV3/Basic_ModelDefinition.md +++ b/docs/WechatTenpayV3/Basic_ModelDefinition.md @@ -927,6 +927,8 @@ - 查询垫付回补结果:`GetEcommerceRefundReturnAdvance` + - 发起异常退款:`CreateEcommerceAbnormalRefundApply` + - 平台收付通(余额查询) - 查询二级商户账户实时余额:`GetEcommerceFundBalance` diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientExecuteEcommerceExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientExecuteEcommerceExtensions.cs index 5d3fedc9..abcca423 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientExecuteEcommerceExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientExecuteEcommerceExtensions.cs @@ -804,6 +804,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 return await client.SendFlurlRequestAsJsonAsync(flurlReq, data: request, cancellationToken: cancellationToken).ConfigureAwait(false); } + + /// + /// 异步调用 [POST] /ecommerce/refunds/{refund_id}/apply-abnormal-refund 接口。 + /// + /// REF:
+ /// + ///
+ ///
+ /// + /// + /// + /// + public static async Task ExecuteCreateEcommerceAbnormalRefundApplyAsync(this WechatTenpayClient client, Models.CreateEcommerceAbnormalRefundApplyRequest request, CancellationToken cancellationToken = default) + { + if (client is null) throw new ArgumentNullException(nameof(client)); + if (request is null) throw new ArgumentNullException(nameof(request)); + + IFlurlRequest flurlReq = client + .CreateFlurlRequest(request, HttpMethod.Post, "ecommerce", "refunds", request.RefundId, "apply-abnormal-refund"); + + return await client.SendFlurlRequestAsJsonAsync(flurlReq, data: request, cancellationToken: cancellationToken).ConfigureAwait(false); + } #endregion #region Subsidies diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.cs new file mode 100644 index 00000000..40547db1 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.cs @@ -0,0 +1,97 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models +{ + /// + /// 表示 [POST] /ecommerce/refunds/{refund_id}/apply-abnormal-refund 接口的请求。 + /// + [WechatTenpaySensitive] + public class CreateEcommerceAbnormalRefundApplyRequest : WechatTenpayRequest + { + public static class Types + { + public class Amount + { + /// + /// 获取或设置原订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("total")] + [System.Text.Json.Serialization.JsonPropertyName("total")] + public int Total { get; set; } + + /// + /// 获取或设置退款金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("refund")] + [System.Text.Json.Serialization.JsonPropertyName("refund")] + public int Refund { get; set; } + + /// + /// 获取或设置退款币种。 + /// 默认值:"CNY" + /// + [Newtonsoft.Json.JsonProperty("currency")] + [System.Text.Json.Serialization.JsonPropertyName("currency")] + public string Currency { get; set; } = "CNY"; + } + } + + /// + /// 获取或设置微信二级商户号。 + /// + [Newtonsoft.Json.JsonProperty("sub_mchid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_mchid")] + public string SubMerchantId { get; set; } = string.Empty; + + /// + /// 获取或设置微信退款单号。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string RefundId { get; set; } = string.Empty; + + /// + /// 获取或设置商户退款单号。 + /// + [Newtonsoft.Json.JsonProperty("out_refund_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_refund_no")] + public string OutRefundNumber { get; set; } = string.Empty; + + /// + /// 获取或设置个人收款方受理授权 ID。 + /// + [Newtonsoft.Json.JsonProperty("individual_auth_id")] + [System.Text.Json.Serialization.JsonPropertyName("individual_auth_id")] + public string? IndividualAuthId { get; set; } + + /// + /// 获取或设置异常退款处理方式。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// + /// 获取或设置收款开户银行。 + /// + [Newtonsoft.Json.JsonProperty("bank_type")] + [System.Text.Json.Serialization.JsonPropertyName("bank_type")] + public string? BankName { get; set; } + + /// + /// 获取或设置收款银行卡号(需使用平台公钥/证书加密)。 + /// + [Newtonsoft.Json.JsonProperty("bank_account")] + [System.Text.Json.Serialization.JsonPropertyName("bank_account")] + [WechatTenpaySensitiveProperty(scheme: Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256, algorithm: Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS8_OAEP_WITH_SHA1_AND_MGF1)] + [WechatTenpaySensitiveProperty(scheme: Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3, algorithm: Constants.EncryptionAlgorithms.SM2_C1C3C2_ASN1)] + public string? BankAccountNumber { get; set; } + + /// + /// 获取或设置收款用户姓名(需使用平台公钥/证书加密)。 + /// + [Newtonsoft.Json.JsonProperty("real_name")] + [System.Text.Json.Serialization.JsonPropertyName("real_name")] + [WechatTenpaySensitiveProperty(scheme: Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256, algorithm: Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS8_OAEP_WITH_SHA1_AND_MGF1)] + [WechatTenpaySensitiveProperty(scheme: Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3, algorithm: Constants.EncryptionAlgorithms.SM2_C1C3C2_ASN1)] + public string? RealName { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.cs new file mode 100644 index 00000000..9c781879 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.cs @@ -0,0 +1,9 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models +{ + /// + /// 表示 [POST] /ecommerce/refunds/{refund_id}/apply-abnormal-refund 接口的响应。 + /// + public class CreateEcommerceAbnormalRefundApplyResponse : GetEcommerceRefundByOutRefundNumberResponse + { + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/GetEcommerceRefundByOutRefundNumberResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/GetEcommerceRefundByOutRefundNumberResponse.cs index 0e5fcec7..f48b49bb 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/GetEcommerceRefundByOutRefundNumberResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/_Partner/Ecommerce/Refunds/GetEcommerceRefundByOutRefundNumberResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models { @@ -11,6 +11,26 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models { public class Amount { + public static class Types + { + public class From + { + /// + /// 获取或设置出资账户类型。 + /// + [Newtonsoft.Json.JsonProperty("account")] + [System.Text.Json.Serialization.JsonPropertyName("account")] + public string Account { get; set; } = default!; + + /// + /// 获取或设置出资金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("amount")] + [System.Text.Json.Serialization.JsonPropertyName("amount")] + public int Amount { get; set; } + } + } + /// /// 获取或设置退款金额(单位:分)。 /// @@ -32,12 +52,26 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models [System.Text.Json.Serialization.JsonPropertyName("discount_refund")] public int DiscountRefund { get; set; } + /// + /// 获取或设置电商平台垫付金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("advance")] + [System.Text.Json.Serialization.JsonPropertyName("advance")] + public int? Advance { get; set; } + /// /// 获取或设置退款币种。 /// [Newtonsoft.Json.JsonProperty("currency")] [System.Text.Json.Serialization.JsonPropertyName("currency")] public string Currency { get; set; } = default!; + + /// + /// 获取或设置退款出资账户及金额信息。 + /// + [Newtonsoft.Json.JsonProperty("from")] + [System.Text.Json.Serialization.JsonPropertyName("from")] + public Types.From[]? From { get; set; } } public class Promotion diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.json new file mode 100644 index 00000000..1d24ad61 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyRequest.json @@ -0,0 +1,9 @@ +{ + "sub_mchid": "1900000109", + "individual_auth_id": "1900000110", + "out_refund_no": "1217752501201407033233368018", + "type": "USER_BANK_CARD", + "bank_type": "ICBC_DEBIT", + "bank_account": "d+xT+MQCvrLHUVDWv/8MR/dB7TkXLVfSrUxMPZy6jWWYzpRrEEaYQE8ZRGYoeorwC+w==", + "real_name": "UPgQcZSdq3zOayJwZ5XLrHY2dZU1W2Cd" +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.json new file mode 100644 index 00000000..642e646e --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/ModelSamples/_/_Partner/Ecommerce/Refunds/CreateEcommerceAbnormalRefundApplyResponse.json @@ -0,0 +1,35 @@ +{ + "refund_id": "1217752501201407033233368018", + "out_refund_no": "1217752501201407033233368018", + "transaction_id": "1217752501201407033233368018", + "out_trade_no": "1217752501201407033233368018", + "channel": "ORIGINAL", + "user_received_account": "招商银行信用卡0403", + "success_time": "2018-06-08T10:34:56+08:00", + "create_time": "2018-06-08T10:34:56+08:00", + "status": "SUCCESS", + "amount": { + "refund": 888, + "from": [ + { + "account": "AVAILABLE", + "amount": 444 + } + ], + "payer_refund": 888, + "discount_refund": 888, + "currency": "CNY", + "advance": 888 + }, + "promotion_detail": [ + { + "promotion_id": "109519", + "scope": "SINGLE", + "type": "DISCOUNT", + "amount": 5, + "refund_amount": 100 + } + ], + "refund_account": "REFUND_SOURCE_SUB_MERCHANT", + "funds_account": "UNSETTLED" +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_RequestEncryptionTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_RequestEncryptionTests.cs index 69ba447b..17ec4d45 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_RequestEncryptionTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_RequestEncryptionTests.cs @@ -677,6 +677,62 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests } } + [Fact(DisplayName = "测试用例:加密请求中的敏感数据([POST] /ecommerce/refunds/{refund_id}/apply-abnormal-refund)")] + public async Task TestEncryptRequestSensitiveProperty_CreateEcommerceAbnormalRefundApplyRequest() + { + static Models.CreateEcommerceAbnormalRefundApplyRequest GenerateMockRequestModel() + { + return new Models.CreateEcommerceAbnormalRefundApplyRequest() + { + BankAccountNumber = MOCK_PLAIN_STR, + RealName = MOCK_PLAIN_STR + }; + } + + static void AssertMockRequestModel(Models.CreateEcommerceAbnormalRefundApplyRequest request, Func decryptor) + { + Assert.NotEqual(MOCK_PLAIN_STR, request.BankAccountNumber!); + Assert.NotEqual(MOCK_PLAIN_STR, request.RealName!); + Assert.Equal(MOCK_PLAIN_STR, decryptor.Invoke(request.BankAccountNumber!)); + Assert.Equal(MOCK_PLAIN_STR, decryptor.Invoke(request.RealName!)); + Assert.Equal(MOCK_CERT_SN, request.WechatpayCertificateSerialNumber!, ignoreCase: true); + } + + if (!string.IsNullOrEmpty(TestConfigs.WechatMerchantRSACertificatePrivateKey)) + { + using (var client = CreateMockClientUseRSA(autoEncrypt: false)) + { + var request = GenerateMockRequestModel(); + client.EncryptRequestSensitiveProperty(request); + AssertMockRequestModel(request, (cipher) => Utilities.RSAUtility.DecryptWithECB(RSA_PEM_PRIVATE_KEY, (EncodedString)cipher)!); + } + + using (var client = CreateMockClientUseRSA(autoEncrypt: true)) + { + var request = GenerateMockRequestModel(); + await client.ExecuteCreateEcommerceAbnormalRefundApplyAsync(request); + AssertMockRequestModel(request, (cipher) => Utilities.RSAUtility.DecryptWithECB(RSA_PEM_PRIVATE_KEY, (EncodedString)cipher)!); + } + } + + if (!string.IsNullOrEmpty(TestConfigs.WechatMerchantSM2CertificatePrivateKey)) + { + using (var client = CreateMockClientUseSM2(autoEncrypt: false)) + { + var request = GenerateMockRequestModel(); + client.EncryptRequestSensitiveProperty(request); + AssertMockRequestModel(request, (cipher) => Utilities.SM2Utility.Decrypt(SM2_PEM_PRIVATE_KEY, (EncodedString)cipher)!); + } + + using (var client = CreateMockClientUseSM2(autoEncrypt: true)) + { + var request = GenerateMockRequestModel(); + await client.ExecuteCreateEcommerceAbnormalRefundApplyAsync(request); + AssertMockRequestModel(request, (cipher) => Utilities.SM2Utility.Decrypt(SM2_PEM_PRIVATE_KEY, (EncodedString)cipher)!); + } + } + } + [Fact(DisplayName = "测试用例:加密请求中的敏感数据([POST] /eduschoolpay/contracts/presign)")] public async Task TestEncryptRequestSensitiveProperty_PresignEducationSchoolPayContractRequest() {