🆕 #4116 【微信支付】增加开具旅客运输行业电子发票接口
This commit is contained in:
@@ -62,5 +62,5 @@ V3 服务商电子发票产品文档:前者是 API 列表中的具体接口,
|
||||
| 身份/鉴权上下文 | 公众号 access token、用户授权页/授权数据 | 微信支付服务商号、子商户号、V3 签名与敏感字段加密 |
|
||||
| 覆盖范围 | 授权、开票、冲红、查询及公众号商户配置 | 子商户邀约/状态、模板/开发配置、行业开票、文件和卡包、通知 |
|
||||
|
||||
因此,两个 Issue 都是同一个微信支付 V3 服务商电子发票接入需求,当前代码库检索未
|
||||
发现对应的 `new-tax-control-fapiao` API 实现。
|
||||
因此,两个 Issue 都是同一个微信支付 V3 服务商电子发票接入需求,当前由
|
||||
`weixin-java-pay` 的 `PartnerInvoiceService` 提供对应的 `new-tax-control-fapiao` API 实现。
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.github.binarywang.wxpay.bean.invoice;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 服务商开具旅客运输行业电子发票请求。
|
||||
*
|
||||
* <p>购买方手机号、邮箱以及出行人证件号码由调用方按微信支付文档加密。</p>
|
||||
*
|
||||
* @see <a href="https://pay.weixin.qq.com/doc/v3/partner/4025863376">开具旅客运输行业电子发票</a>
|
||||
*/
|
||||
@Data
|
||||
public class PassengerTransportInvoiceRequest implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 子商户号。微信支付分配的子商户号,必填,最长32个字符。
|
||||
*/
|
||||
@SerializedName("sub_mchid")
|
||||
private String subMchid;
|
||||
|
||||
/**
|
||||
* 发票申请单号。唯一标识一次开票行为并关联唯一的购买方信息,必填,最长32个字符;
|
||||
* 微信支付账单开票场景下填写微信支付交易单号。
|
||||
*/
|
||||
@SerializedName("fapiao_apply_id")
|
||||
private String fapiaoApplyId;
|
||||
|
||||
/**
|
||||
* 购买方信息,即发票抬头,必填。
|
||||
*/
|
||||
@SerializedName("buyer_information")
|
||||
private BuyerInformation buyerInformation;
|
||||
|
||||
/**
|
||||
* 需要开具的旅客运输行业数电发票信息,必填。
|
||||
*/
|
||||
@SerializedName("fapiao_information")
|
||||
private FapiaoInformation fapiaoInformation;
|
||||
|
||||
/**
|
||||
* 需要开具的旅客运输行业数电发票信息。
|
||||
*/
|
||||
@Data
|
||||
public static class FapiaoInformation implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 商户发票单号,必填,最长32个字符,在每个商户下必须唯一。
|
||||
* 仅支持字母、数字、中划线、下划线、竖线和星号;开票失败或发票已冲红时可更换后重试,
|
||||
* 同一发票申请单最多支持5个商户发票单号。
|
||||
*/
|
||||
@SerializedName("fapiao_id")
|
||||
private String fapiaoId;
|
||||
|
||||
/**
|
||||
* 总价税合计,必填,单位:分。值为所有发票行单行金额合计之和,且全部发票的总价税合计
|
||||
* 不能超过交易总金额。
|
||||
*/
|
||||
@SerializedName("total_amount")
|
||||
private Long totalAmount;
|
||||
|
||||
/**
|
||||
* 发票行信息,必填;单张发票最多包含8行。
|
||||
*/
|
||||
private List<InvoiceItem> items;
|
||||
|
||||
/**
|
||||
* 出口业务适用政策代码。可选值:1(退税政策)、2(免税政策)、3(征税政策)。
|
||||
*/
|
||||
@SerializedName("export_business_policy_code")
|
||||
private Long exportBusinessPolicyCode;
|
||||
|
||||
/**
|
||||
* 增值税即征即退代码。可选值:1(软件产品)、2(资源综合利用产品)、3(管道运输服务)、
|
||||
* 4(有形动产融资租赁服务)、5(有形动产融资性售后回租服务)、6(新型墙体材料)、
|
||||
* 7(风力发电产品)、8(光伏发电产品)、9(动漫软件产品)、10(飞机维修劳务)、
|
||||
* 11(黄金)、12(铂金)。
|
||||
*/
|
||||
@SerializedName("vat_refund_levy_code")
|
||||
private Long vatRefundLevyCode;
|
||||
|
||||
/**
|
||||
* 开票人ID,必填,最长64个字符,为税局乐企系统登记的开票人ID。
|
||||
*/
|
||||
@SerializedName("billing_person_id")
|
||||
private String billingPersonId;
|
||||
|
||||
/**
|
||||
* 开票人名称,最长64个字符,为税局乐企系统登记的脱敏后名称;格式为脱敏姓名、空格和身份证后四位。
|
||||
*/
|
||||
@SerializedName("billing_person")
|
||||
private String billingPerson;
|
||||
|
||||
/**
|
||||
* 发票类型,必填。可选值:COMM_FAPIAO(增值税普通发票)、VAT_FAPIAO(增值税专用发票)。
|
||||
*/
|
||||
@SerializedName("fapiao_bill_type")
|
||||
private String fapiaoBillType;
|
||||
|
||||
/**
|
||||
* 发票对应的交易信息,必填,最多支持10条。
|
||||
*/
|
||||
@SerializedName("transaction_information")
|
||||
private List<TransactionInformation> transactionInformation;
|
||||
|
||||
/**
|
||||
* 发票备注。
|
||||
*/
|
||||
private String remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发票行信息。
|
||||
*/
|
||||
@Data
|
||||
public static class InvoiceItem implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 税局侧规定的货物或应税劳务、服务税收分类编码,必填,长度为19个字符;
|
||||
* 旅客运输业务仅支持以301开头的税收分类编码。
|
||||
*/
|
||||
@SerializedName("tax_code")
|
||||
private String taxCode;
|
||||
|
||||
/**
|
||||
* 货物或应税劳务、服务名称,必填,由商户自定义,最长128个字符。
|
||||
*/
|
||||
@SerializedName("goods_name")
|
||||
private String goodsName;
|
||||
|
||||
/**
|
||||
* 规格型号,展示在发票规格型号列,最长20个字符。
|
||||
*/
|
||||
private String specification;
|
||||
|
||||
/**
|
||||
* 单位,展示在发票单位列;折扣行不填写。
|
||||
*/
|
||||
private String unit;
|
||||
|
||||
/**
|
||||
* 数量,单位为10的负8次方,100000000表示数量1。非折扣行不填写时默认为100000000;
|
||||
* 折扣行不填写。
|
||||
*/
|
||||
private Long quantity;
|
||||
|
||||
/**
|
||||
* 单行金额合计,必填,单位:分。折扣行为负数,非折扣行为正数。
|
||||
*/
|
||||
@SerializedName("total_amount")
|
||||
private Long totalAmount;
|
||||
|
||||
/**
|
||||
* 税率,必填,单位为万分之一,例如1300表示13%。当前支持0、1%、1.5%、3%、5%、6%、
|
||||
* 9%、10%、11%、13%、16%和17%。
|
||||
*/
|
||||
@SerializedName("tax_rate")
|
||||
private Long taxRate;
|
||||
|
||||
/**
|
||||
* 是否为折扣行,必填;折扣行必须紧跟在被折扣行之后。
|
||||
*/
|
||||
private Boolean discount;
|
||||
|
||||
/**
|
||||
* 优惠政策标识。可选值:1(简易征收)、2(稀土产品)、3(免税)、4(不征税)、
|
||||
* 5(先征后退)、6(100%先征后退)、7(50%先征后退)、8(按3%简易征收)、
|
||||
* 9(按5%简易征收)、10(按5%简易征收减按1.5%计征)、11(即征即退30%)、
|
||||
* 12(即征即退50%)、13(即征即退70%)、14(即征即退100%)、
|
||||
* 15(超税负3%即征即退)、16(超税负8%即征即退)、17(超税负12%即征即退)、
|
||||
* 18(超税负6%即征即退)。
|
||||
*/
|
||||
@SerializedName("preferential_policy_code")
|
||||
private Long preferentialPolicyCode;
|
||||
|
||||
/**
|
||||
* 出行人额外信息,可选;传入时其内部必填字段必须完整填写。
|
||||
*/
|
||||
@SerializedName("passenger_information")
|
||||
private PassengerInformation passengerInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出行人额外信息。证件号码由调用方按微信支付文档加密。
|
||||
*/
|
||||
@Data
|
||||
public static class PassengerInformation implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 出行人姓名;填写出行人额外信息时必填,最长20个字符。
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 出行人证件类型;填写出行人额外信息时必填。可选值:IDENTITY_CARD(居民身份证)、
|
||||
* PASSPORT(护照)、HONG_KONG_PERMIT(中国香港居民来往内地通行证)、
|
||||
* MACAO_PERMIT(中国澳门居民来往内地通行证)、TAIWAN_PERMIT(中国台湾居民来往大陆通行证)、
|
||||
* FOREIGNER_RESIDENCE_PERMIT(外国人居留证)、HONG_KONG_RESIDENT_CARD(香港居民证)、
|
||||
* MACAO_RESIDENT_CARD(澳门居民证)、TAIWAN_RESIDENT_CARD(台湾居民证)、
|
||||
* MILITARY_OFFICER_CARD(军官证)、ARMED_POLICE_OFFICER_CARD(武警警官证)、
|
||||
* SOLDIER_CARD(士兵证)、HOMECOMING_CERT(港澳同胞回乡证)、
|
||||
* TAIWAN_COMPATRIOT_CERT(台胞证)。
|
||||
*/
|
||||
@SerializedName("certificate_type")
|
||||
private String certificateType;
|
||||
|
||||
/**
|
||||
* 出行人证件号码;填写出行人额外信息时必填。该字段为密文字段,调用方需使用微信支付公钥
|
||||
* 或微信支付平台证书公钥加密后传入。
|
||||
*/
|
||||
@SerializedName("certificate_number")
|
||||
private String certificateNumber;
|
||||
|
||||
/**
|
||||
* 出行日期;填写出行人额外信息时必填,使用 RFC3339 格式:yyyy-MM-DDTHH:mm:ss+TIMEZONE。
|
||||
*/
|
||||
@SerializedName("departure_date")
|
||||
private String departureDate;
|
||||
|
||||
/**
|
||||
* 出发地详细地址;填写出行人额外信息时必填,最长80个字符。
|
||||
*/
|
||||
@SerializedName("departure_place")
|
||||
private String departurePlace;
|
||||
|
||||
/**
|
||||
* 目的地详细地址;填写出行人额外信息时必填,最长80个字符。
|
||||
*/
|
||||
private String destination;
|
||||
|
||||
/**
|
||||
* 交通工具类型;填写出行人额外信息时必填。可选值:LONG_DISTANCE_BUS(长途汽车)、
|
||||
* PUBLIC_TRANSPORTATION(公共交通)、CAR(汽车)、SHIP(船舶)、
|
||||
* OTHER_TRANSPORTATION(其他交通工具)。
|
||||
*/
|
||||
@SerializedName("transportation_type")
|
||||
private String transportationType;
|
||||
|
||||
/**
|
||||
* 交通工具等级。交通工具为 SHIP 时必填,可选值:SHIP_FIRST_CLASS_CABIN(船舶一等舱)、
|
||||
* SHIP_SECOND_CLASS_CABIN(船舶二等舱)、SHIP_THIRD_CLASS_CABIN(船舶三等舱);
|
||||
* 其他交通工具不填写。
|
||||
*/
|
||||
@SerializedName("transportation_classes")
|
||||
private String transportationClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发票对应的交易信息。
|
||||
*/
|
||||
@Data
|
||||
public static class TransactionInformation implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 支付渠道,必填。当前可选值:WECHAT_PAY(微信支付)。
|
||||
*/
|
||||
@SerializedName("pay_channel")
|
||||
private String payChannel;
|
||||
|
||||
/**
|
||||
* 支付订单号,最长64个字符。支付渠道为 WECHAT_PAY 时,本字段与商户订单号至少填写一个。
|
||||
*/
|
||||
@SerializedName("transaction_id")
|
||||
private String transactionId;
|
||||
|
||||
/**
|
||||
* 支付商户订单号,最长64个字符。支付渠道为 WECHAT_PAY 时,本字段与支付订单号至少填写一个。
|
||||
*/
|
||||
@SerializedName("out_trade_no")
|
||||
private String outTradeNo;
|
||||
|
||||
/**
|
||||
* 交易金额,必填,单位:分。
|
||||
*/
|
||||
private Long amount;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.github.binarywang.wxpay.service;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InviteUrlResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InviteUrlRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.PassengerTransportInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InvoiceResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest;
|
||||
@@ -51,6 +52,21 @@ public interface PartnerInvoiceService {
|
||||
*/
|
||||
void issueGeneralInvoice(GeneralInvoiceRequest request) throws WxPayException;
|
||||
|
||||
/**
|
||||
* 开具旅客运输行业电子发票。
|
||||
*
|
||||
* <p>接口受理成功时返回 HTTP 202 Accepted,无应答包体。受理成功不代表开票完成,
|
||||
* 请通过开票结果回调或查询电子发票接口获取处理结果。</p>
|
||||
*
|
||||
* @param request 开票申请
|
||||
* @throws WxPayException 微信支付异常
|
||||
* @throws UnsupportedOperationException 当前实现不支持开具旅客运输行业电子发票
|
||||
* @see <a href="https://pay.weixin.qq.com/doc/v3/partner/4025863376">官方文档</a>
|
||||
*/
|
||||
default void issuePassengerTransportInvoice(PassengerTransportInvoiceRequest request) throws WxPayException {
|
||||
throw new UnsupportedOperationException("当前实现不支持开具旅客运输行业电子发票");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询电子发票。
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.binarywang.wxpay.service.impl;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InviteUrlResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InviteUrlRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.PassengerTransportInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InvoiceResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest;
|
||||
@@ -45,6 +46,7 @@ public class PartnerInvoiceServiceImpl implements PartnerInvoiceService {
|
||||
private static final com.google.gson.Gson GSON = WxGsonBuilder.create();
|
||||
private static final String INVITE_URL_PATH = "/v3/new-tax-control-fapiao/fapiaomerchant/getspinviteurl";
|
||||
private static final String ISSUE_GENERAL_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/issue-general";
|
||||
private static final String ISSUE_PASSENGER_TRANSPORT_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/issue-passenger-transport";
|
||||
private static final String FAPIAO_APPLICATIONS_PATH = "/v3/new-tax-control-fapiao/fapiao-applications/";
|
||||
|
||||
private final WxPayService payService;
|
||||
@@ -80,6 +82,12 @@ public class PartnerInvoiceServiceImpl implements PartnerInvoiceService {
|
||||
this.payService.postV3(url, GSON.toJson(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issuePassengerTransportInvoice(PassengerTransportInvoiceRequest request) throws WxPayException {
|
||||
String url = this.payService.getPayBaseUrl() + ISSUE_PASSENGER_TRANSPORT_PATH;
|
||||
this.payService.postV3(url, GSON.toJson(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvoiceResult getInvoice(String fapiaoApplyId, String subMchId, String fapiaoId) throws WxPayException {
|
||||
String url = this.payService.getPayBaseUrl() + FAPIAO_APPLICATIONS_PATH + encode(fapiaoApplyId) + "?sub_mchid=" + encode(subMchId);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
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 PassengerTransportInvoiceRequest} 单元测试。
|
||||
*/
|
||||
public class PassengerTransportInvoiceRequestTest {
|
||||
|
||||
@Test
|
||||
public void shouldSerializePassengerTransportInvoiceRequestUsingWechatPayFieldNames() {
|
||||
PassengerTransportInvoiceRequest request = new PassengerTransportInvoiceRequest();
|
||||
request.setSubMchid("1900000109");
|
||||
request.setFapiaoApplyId("apply_20260827_001");
|
||||
|
||||
BuyerInformation buyer = new BuyerInformation();
|
||||
buyer.setType("INDIVIDUAL");
|
||||
buyer.setName("示例旅客");
|
||||
buyer.setPhone("encrypted-phone");
|
||||
buyer.setEmail("encrypted-email");
|
||||
buyer.setAmount(1000);
|
||||
request.setBuyerInformation(buyer);
|
||||
|
||||
PassengerTransportInvoiceRequest.PassengerInformation passenger =
|
||||
new PassengerTransportInvoiceRequest.PassengerInformation();
|
||||
passenger.setName("张**");
|
||||
passenger.setCertificateType("IDENTITY_CARD");
|
||||
passenger.setCertificateNumber("encrypted-certificate-number");
|
||||
passenger.setDepartureDate("2026-08-27T10:00:00+08:00");
|
||||
passenger.setDeparturePlace("重庆市渝北区示例出发地");
|
||||
passenger.setDestination("重庆市两江新区示例目的地");
|
||||
passenger.setTransportationType("SHIP");
|
||||
passenger.setTransportationClasses("SHIP_FIRST_CLASS_CABIN");
|
||||
|
||||
PassengerTransportInvoiceRequest.InvoiceItem item = new PassengerTransportInvoiceRequest.InvoiceItem();
|
||||
item.setTaxCode("3010101020100000000");
|
||||
item.setGoodsName("旅客运输服务");
|
||||
item.setQuantity(2200000000L);
|
||||
item.setTotalAmount(1000L);
|
||||
item.setTaxRate(300L);
|
||||
item.setDiscount(false);
|
||||
item.setPassengerInformation(passenger);
|
||||
|
||||
PassengerTransportInvoiceRequest.TransactionInformation transaction =
|
||||
new PassengerTransportInvoiceRequest.TransactionInformation();
|
||||
transaction.setPayChannel("WECHAT_PAY");
|
||||
transaction.setOutTradeNo("order_20260827_001");
|
||||
transaction.setAmount(1000L);
|
||||
|
||||
PassengerTransportInvoiceRequest.FapiaoInformation fapiao =
|
||||
new PassengerTransportInvoiceRequest.FapiaoInformation();
|
||||
fapiao.setFapiaoId("invoice_20260827_001");
|
||||
fapiao.setTotalAmount(1000L);
|
||||
fapiao.setItems(Collections.singletonList(item));
|
||||
fapiao.setBillingPersonId("billing_person_001");
|
||||
fapiao.setFapiaoBillType("COMM_FAPIAO");
|
||||
fapiao.setTransactionInformation(Collections.singletonList(transaction));
|
||||
request.setFapiaoInformation(fapiao);
|
||||
|
||||
JsonObject json = new Gson().toJsonTree(request).getAsJsonObject();
|
||||
JsonObject passengerJson = json.getAsJsonObject("fapiao_information")
|
||||
.getAsJsonArray("items").get(0).getAsJsonObject()
|
||||
.getAsJsonObject("passenger_information");
|
||||
|
||||
assertThat(json.get("sub_mchid").getAsString()).isEqualTo("1900000109");
|
||||
assertThat(json.get("fapiao_apply_id").getAsString()).isEqualTo("apply_20260827_001");
|
||||
assertThat(json.getAsJsonObject("buyer_information").get("phone").getAsString())
|
||||
.isEqualTo("encrypted-phone");
|
||||
assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("items").get(0)
|
||||
.getAsJsonObject().get("quantity").getAsLong()).isEqualTo(2200000000L);
|
||||
assertThat(passengerJson.get("name").getAsString()).isEqualTo("张**");
|
||||
assertThat(passengerJson.get("certificate_type").getAsString()).isEqualTo("IDENTITY_CARD");
|
||||
assertThat(passengerJson.get("certificate_number").getAsString())
|
||||
.isEqualTo("encrypted-certificate-number");
|
||||
assertThat(passengerJson.get("departure_date").getAsString())
|
||||
.isEqualTo("2026-08-27T10:00:00+08:00");
|
||||
assertThat(passengerJson.get("departure_place").getAsString())
|
||||
.isEqualTo("重庆市渝北区示例出发地");
|
||||
assertThat(passengerJson.get("destination").getAsString())
|
||||
.isEqualTo("重庆市两江新区示例目的地");
|
||||
assertThat(passengerJson.get("transportation_type").getAsString()).isEqualTo("SHIP");
|
||||
assertThat(passengerJson.get("transportation_classes").getAsString())
|
||||
.isEqualTo("SHIP_FIRST_CLASS_CABIN");
|
||||
assertThat(json.getAsJsonObject("fapiao_information").getAsJsonArray("transaction_information").get(0)
|
||||
.getAsJsonObject().get("out_trade_no").getAsString()).isEqualTo("order_20260827_001");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.github.binarywang.wxpay.service.impl;
|
||||
|
||||
import com.github.binarywang.wxpay.service.PartnerInvoiceService;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import com.github.binarywang.wxpay.bean.invoice.GeneralInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.PassengerTransportInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InvoiceResult;
|
||||
import com.github.binarywang.wxpay.bean.invoice.ReverseInvoiceRequest;
|
||||
import com.github.binarywang.wxpay.bean.invoice.InvoiceFileResult;
|
||||
@@ -18,6 +20,12 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
*/
|
||||
public class PartnerInvoiceServiceImplTest {
|
||||
|
||||
@Test
|
||||
public void shouldKeepPassengerTransportInvoiceMethodSourceCompatible() throws Exception {
|
||||
Assert.assertTrue(PartnerInvoiceService.class
|
||||
.getMethod("issuePassengerTransportInvoice", PassengerTransportInvoiceRequest.class).isDefault());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRequestInviteUrlWithOptionalSubMchId() throws Exception {
|
||||
AtomicReference<String> requestedUrl = new AtomicReference<>();
|
||||
@@ -67,6 +75,44 @@ public class PartnerInvoiceServiceImplTest {
|
||||
Assert.assertTrue(requestedBody.get().contains("\"fapiao_apply_id\":\"invoice-001\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPostPassengerTransportInvoiceToV3Endpoint() 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 null;
|
||||
}
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
PassengerTransportInvoiceRequest request = new PassengerTransportInvoiceRequest();
|
||||
request.setSubMchid("1900000109");
|
||||
request.setFapiaoApplyId("invoice-002");
|
||||
PassengerTransportInvoiceRequest.PassengerInformation passenger =
|
||||
new PassengerTransportInvoiceRequest.PassengerInformation();
|
||||
passenger.setCertificateNumber("encrypted-certificate-number");
|
||||
PassengerTransportInvoiceRequest.InvoiceItem item = new PassengerTransportInvoiceRequest.InvoiceItem();
|
||||
item.setPassengerInformation(passenger);
|
||||
PassengerTransportInvoiceRequest.FapiaoInformation fapiao =
|
||||
new PassengerTransportInvoiceRequest.FapiaoInformation();
|
||||
fapiao.setItems(java.util.Collections.singletonList(item));
|
||||
request.setFapiaoInformation(fapiao);
|
||||
|
||||
new PartnerInvoiceServiceImpl(payService).issuePassengerTransportInvoice(request);
|
||||
|
||||
Assert.assertEquals(requestedUrl.get(),
|
||||
"https://api.mch.weixin.qq.com/v3/new-tax-control-fapiao/fapiao-applications/issue-passenger-transport");
|
||||
Assert.assertTrue(requestedBody.get().contains("\"fapiao_apply_id\":\"invoice-002\""));
|
||||
Assert.assertTrue(requestedBody.get().contains("\"passenger_information\""));
|
||||
Assert.assertTrue(requestedBody.get().contains("\"certificate_number\":\"encrypted-certificate-number\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldQueryInvoiceWithRequiredSubMchId() throws Exception {
|
||||
AtomicReference<String> requestedUrl = new AtomicReference<>();
|
||||
|
||||
Reference in New Issue
Block a user