1
0
mirror of synced 2026-10-19 22:26:48 +08:00

🆕 #4070【微信支付】新增 V3服务商电子发票能力封装,覆盖邀约、抬头、开票、冲红、文件上传/下载等核心接口

This commit is contained in:
Binary Wang
2026-07-18 20:54:53 +08:00
committed by GitHub
parent 6d5843489d
commit 2d496ed309
30 changed files with 1183 additions and 4 deletions

View File

@@ -0,0 +1,59 @@
package com.github.binarywang.wxpay.bean.invoice;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.testng.annotations.Test;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
/**
* {@link GeneralInvoiceRequest} 单元测试。
*/
public class GeneralInvoiceRequestTest {
@Test
public void shouldSerializeGeneralInvoiceRequestUsingWechatPayFieldNames() {
GeneralInvoiceRequest request = new GeneralInvoiceRequest();
request.setSubMchid("1900000109");
request.setFapiaoApplyId("apply_20260718_001");
BuyerInformation buyer = new BuyerInformation();
buyer.setType("ORGANIZATION");
buyer.setName("示例企业");
buyer.setTaxpayerId("91440300TEST00001");
request.setBuyerInformation(buyer);
GeneralInvoiceRequest.InvoiceItem item = new GeneralInvoiceRequest.InvoiceItem();
item.setTaxCode("3010101020203000000");
item.setGoodsName("技术服务");
item.setTotalAmount(100);
item.setTaxRate(600);
item.setDiscount(false);
GeneralInvoiceRequest.FapiaoInformation invoice = new GeneralInvoiceRequest.FapiaoInformation();
invoice.setFapiaoId("invoice_20260718_001");
invoice.setTotalAmount(100);
invoice.setItems(Collections.singletonList(item));
invoice.setBillingPersonId("billing_person_001");
invoice.setFapiaoBillType("COMM_FAPIAO");
GeneralInvoiceRequest.TransactionInformation transaction = new GeneralInvoiceRequest.TransactionInformation();
transaction.setPayChannel("WECHAT_PAY");
transaction.setOutTradeNo("order_20260718_001");
transaction.setAmount(100);
invoice.setTransactionInformation(Collections.singletonList(transaction));
request.setFapiaoInformation(invoice);
JsonObject json = new Gson().toJsonTree(request).getAsJsonObject();
assertThat(json.get("sub_mchid").getAsString()).isEqualTo("1900000109");
assertThat(json.get("fapiao_apply_id").getAsString()).isEqualTo("apply_20260718_001");
assertThat(json.getAsJsonObject("buyer_information").get("taxpayer_id").getAsString())
.isEqualTo("91440300TEST00001");
assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("items").get(0)
.getAsJsonObject().get("tax_code").getAsString()).isEqualTo("3010101020203000000");
assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("transaction_information").get(0)
.getAsJsonObject().get("out_trade_no").getAsString()).isEqualTo("order_20260718_001");
}
}

View File

@@ -0,0 +1,195 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest;
import com.github.binarywang.wxpay.bean.invoice.InvoiceResult;
import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest;
import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult;
import com.github.binarywang.wxpay.bean.invoice.SubMerchantInvoiceStatus;
import com.github.binarywang.wxpay.bean.invoice.TitleUrlRequest;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicReference;
/**
* {@link PartnerInvoiceServiceImpl} 测试。
*/
public class PartnerInvoiceServiceImplTest {
@Test
public void shouldRequestInviteUrlWithOptionalSubMchId() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("getV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
return "{\"invite_url\":\"https://wxacurl.cn?xxx=\"}";
}
throw new UnsupportedOperationException(method.getName());
});
PartnerInvoiceServiceImpl service = new PartnerInvoiceServiceImpl(payService);
Assert.assertEquals(service.getInviteUrl("19998278783").getInviteUrl(), "https://wxacurl.cn?xxx=");
Assert.assertEquals(requestedUrl.get(),
"https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiaomerchant/getspinviteurl?sub_mchid=19998278783");
}
@Test
public void shouldPostGeneralInvoiceToV3Endpoint() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
AtomicReference<String> requestedBody = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("postV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
requestedBody.set((String) args[1]);
return "";
}
throw new UnsupportedOperationException(method.getName());
});
GeneralInvoiceRequest request = new GeneralInvoiceRequest();
request.setSubMchid("1900000109");
request.setFapiaoApplyId("invoice-001");
new PartnerInvoiceServiceImpl(payService).issueGeneralInvoice(request);
Assert.assertEquals(requestedUrl.get(),
"https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/issue-general");
Assert.assertTrue(requestedBody.get().contains("\"fapiao_apply_id\":\"invoice-001\""));
}
@Test
public void shouldQueryInvoiceWithRequiredSubMchId() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("getV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
return "{\"total_count\":1,\"fapiao_information\":[{\"fapiao_id\":\"fapiao-001\",\"status\":\"ISSUED\"}]}";
}
throw new UnsupportedOperationException(method.getName());
});
InvoiceResult result = new PartnerInvoiceServiceImpl(payService)
.getInvoice("apply-001", "1900000109", "fapiao-001");
Assert.assertEquals(requestedUrl.get(), "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/apply-001?sub_mchid=1900000109&fapiao_id=fapiao-001");
Assert.assertEquals(result.getTotalCount(), Integer.valueOf(1));
Assert.assertEquals(result.getFapiaoInformation().get(0).getStatus(), "ISSUED");
}
@Test
public void shouldReverseInvoiceUsingPathApplyId() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
AtomicReference<String> requestedBody = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("postV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
requestedBody.set((String) args[1]);
return "";
}
throw new UnsupportedOperationException(method.getName());
});
ReverseInvoiceRequest request = new ReverseInvoiceRequest();
request.setFapiaoApplyId("apply-001");
request.setSubMchid("1900000109");
request.setReverseReason("SALES_RETURN");
new PartnerInvoiceServiceImpl(payService).reverseInvoice(request);
Assert.assertEquals(requestedUrl.get(),
"https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/apply-001/reverse");
Assert.assertTrue(requestedBody.get().contains("\"sub_mchid\":\"1900000109\""));
Assert.assertFalse(requestedBody.get().contains("fapiao_apply_id"));
}
@Test
public void shouldGetInvoiceFileDownloadInfo() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("getV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
return "{\"fapiao_download_info_list\":[{\"fapiao_id\":\"fapiao-001\",\"download_url\":\"https://download.example.com/file\",\"status\":\"ISSUED\"}]}";
}
throw new UnsupportedOperationException(method.getName());
});
InvoiceFileResult result = new PartnerInvoiceServiceImpl(payService)
.getInvoiceFileDownloadInfo("apply-001", "1900000109", "fapiao-001");
Assert.assertEquals(requestedUrl.get(), "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/apply-001/fapiao-files?sub_mchid=1900000109&fapiao_id=fapiao-001");
Assert.assertEquals(result.getFapiaoDownloadInfoList().get(0).getDownloadUrl(), "https://download.example.com/file");
}
@Test
public void shouldCheckSubMerchantInvoiceStatus() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("getV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
return "{\"sub_mchid\":\"1900000109\",\"third_mode\":{\"status\":\"ENABLED\"}}";
}
throw new UnsupportedOperationException(method.getName());
});
SubMerchantInvoiceStatus result = new PartnerInvoiceServiceImpl(payService).getSubMerchantInvoiceStatus("1900000109");
Assert.assertEquals(requestedUrl.get(), "https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/merchant/1900000109/check-status");
Assert.assertEquals(result.getThirdMode().getStatus(), "ENABLED");
}
@Test
public void shouldEncodeTitleUrlParametersAndIncludeOptionalFields() throws Exception {
AtomicReference<String> requestedUrl = new AtomicReference<>();
WxPayService payService = (WxPayService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{WxPayService.class}, (proxy, method, args) -> {
if ("getPayBaseUrl".equals(method.getName())) {
return "https://api.mch.weixin.qq.com";
}
if ("getV3".equals(method.getName())) {
requestedUrl.set((String) args[0]);
return "{}";
}
throw new UnsupportedOperationException(method.getName());
});
TitleUrlRequest request = new TitleUrlRequest();
request.setSubMchid("1900000109");
request.setFapiaoApplyId("apply|001");
request.setSource("WEB");
request.setAppid("wx123");
request.setOpenid("openid");
request.setTotalAmount(100);
request.setSellerName("测试 商户");
request.setShowPhoneCell(true);
new PartnerInvoiceServiceImpl(payService).getUserTitleUrl(request);
Assert.assertTrue(requestedUrl.get().contains("fapiao_apply_id=apply%7C001"));
Assert.assertTrue(requestedUrl.get().contains("seller_name=%E6%B5%8B%E8%AF%95+%E5%95%86%E6%88%B7"));
Assert.assertTrue(requestedUrl.get().contains("show_phone_cell=true"));
}
}