|
|
@@ -0,0 +1,222 @@
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import com.kym.huapiaoer.HuapiaoerClient;
|
|
|
+import com.kym.huapiaoer.HuapiaoerConfig;
|
|
|
+import com.kym.huapiaoer.HuapiaoerException;
|
|
|
+import com.kym.huapiaoer.enums.InvoiceQueryType;
|
|
|
+import com.kym.huapiaoer.enums.InvoiceType;
|
|
|
+import com.kym.huapiaoer.enums.PayType;
|
|
|
+import com.kym.huapiaoer.model.request.InvoiceQrcodeRequest;
|
|
|
+import com.kym.huapiaoer.model.request.OpenBlueInvoiceRequest;
|
|
|
+import com.kym.huapiaoer.model.response.InvoiceInfo;
|
|
|
+import okhttp3.MediaType;
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
+import okhttp3.RequestBody;
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+import java.util.UUID;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 航信电子发票 API 测试。
|
|
|
+ * 直接使用 HuapiaoerClient,无需启动 Spring 容器。
|
|
|
+ *
|
|
|
+ * 运行前请确认 application.yml 中 huapiaoer 相关配置已正确填写:
|
|
|
+ * huapiaoer.app-secret
|
|
|
+ * huapiaoer.nsrsbh
|
|
|
+ * huapiaoer.b2cshopid
|
|
|
+ */
|
|
|
+public class HuapiaoerApiTest {
|
|
|
+
|
|
|
+ private static final String APP_SECRET = "xK9mQ2pR5nT7vL4cW6fY8jH1aB3sD0gZ";
|
|
|
+ private static final String NSRSBH = "91440300MA5HJNDG14";
|
|
|
+ private static final String B2C_SHOP_ID = "91440300MA5HJNDG14";
|
|
|
+ private static final String BASE_URL = "https://erp.huapiaoer.com";
|
|
|
+
|
|
|
+ private static HuapiaoerClient client;
|
|
|
+
|
|
|
+ @BeforeAll
|
|
|
+ public static void setUp() {
|
|
|
+ HuapiaoerConfig config = HuapiaoerConfig.builder()
|
|
|
+ .baseUrl(BASE_URL)
|
|
|
+ .appSecret(APP_SECRET)
|
|
|
+ .build();
|
|
|
+ client = new HuapiaoerClient(config);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试 1:查询开票人列表(只读,安全)。
|
|
|
+ * 验证 appsecret 和 nsrsbh 是否有效。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testGetDrawerIdList() {
|
|
|
+ System.out.println("=== 测试:查询开票人列表 ===");
|
|
|
+ var drawers = client.getDrawerIdList(NSRSBH);
|
|
|
+ assertNotNull(drawers);
|
|
|
+ System.out.println("开票人数量: " + drawers.size());
|
|
|
+ drawers.forEach(d -> System.out.println(" issuerid=" + d.getIssuerid() + ", name=" + d.getName()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试 2:企业搜索(只读,安全)。
|
|
|
+ * 通过企业名称模糊查询税号等信息。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSearchEnterprise() {
|
|
|
+ System.out.println("=== 测试:企业搜索 ===");
|
|
|
+ var enterprises = client.searchEnterprise("快与慢");
|
|
|
+ assertNotNull(enterprises);
|
|
|
+ System.out.println("搜索结果数量: " + enterprises.size());
|
|
|
+ enterprises.forEach(e -> System.out.println(" name=" + e.getEnterpriseName() + ", taxpayerNum=" + e.getTaxpayerNum()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试 3:生成开票二维码(只读,安全)。
|
|
|
+ * 生成二维码后用户可扫码自助开票,不会直接开票。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testGetInvoiceQrcode() {
|
|
|
+ System.out.println("=== 测试:生成开票二维码 ===");
|
|
|
+ String orderId = "TEST-" + UUID.randomUUID().toString().substring(0, 8);
|
|
|
+ var head = new InvoiceQrcodeRequest.HeadInfo()
|
|
|
+ .setOrderId(orderId)
|
|
|
+ .setB2cshopid(B2C_SHOP_ID)
|
|
|
+ .setInvoiceTypeCode(InvoiceType.DIGITAL_NORMAL.getCode());
|
|
|
+ var request = new InvoiceQrcodeRequest()
|
|
|
+ .setHead(head);
|
|
|
+ var resp = client.getInvoiceQrcode(request);
|
|
|
+ System.out.println("code=" + resp.getCode() + ", data=" + resp.getData());
|
|
|
+ assertNotNull(resp);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试 4:开具蓝字发票(会产生真实发票,慎用)。
|
|
|
+ * 默认跳过,需要时手动去掉 @Test 注释。
|
|
|
+ *
|
|
|
+ * 注意:此操作会在税局系统生成一张真实的电子发票!
|
|
|
+ * 建议使用极小金额(如 0.01 元)进行测试。
|
|
|
+ */
|
|
|
+ /**
|
|
|
+ * 测试 4:开具蓝字发票 + 轮询查询结果(完整开票流程)。
|
|
|
+ * 注意:此操作会在税局系统生成一张真实的电子发票(金额 0.01 元)!
|
|
|
+ *
|
|
|
+ * 税编说明:航信要求使用非汇总项的 19 位税收分类编码。
|
|
|
+ * 如果开票失败,请根据后台错误信息调整 taxcode。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testOpenBlueInvoice() {
|
|
|
+ System.out.println("=== 测试:开具蓝字发票 ===");
|
|
|
+ String orderId = "TEST-BLUE-" + UUID.randomUUID().toString().substring(0, 8);
|
|
|
+ System.out.println("出库单号: " + orderId);
|
|
|
+
|
|
|
+ // 先获取开票人 ID
|
|
|
+ var drawers = client.getDrawerIdList(NSRSBH);
|
|
|
+ assertFalse(drawers.isEmpty(), "至少需要一个开票人");
|
|
|
+ String issuerId = drawers.get(0).getIssuerid();
|
|
|
+ System.out.println("开票人 issuerid: " + issuerId);
|
|
|
+
|
|
|
+ // 税编:使用叶子节点编码(非汇总项)
|
|
|
+ // "1100101020200000000" - 供电*电费 (13%) -- 已验证可用
|
|
|
+ // "3040101020100000000" - 专业技术服务 (6%)
|
|
|
+
|
|
|
+ var detail1 = new OpenBlueInvoiceRequest.DetailInfo()
|
|
|
+ .setGoodsname("充电电费")
|
|
|
+ .setPrice(0.01)
|
|
|
+ .setSl(1.0)
|
|
|
+ .setTaxcode("1100101020200000000")
|
|
|
+ .setTax("0.13");
|
|
|
+
|
|
|
+ var detail2 = new OpenBlueInvoiceRequest.DetailInfo()
|
|
|
+ .setGoodsname("充电服务费")
|
|
|
+ .setPrice(0.01)
|
|
|
+ .setSl(1.0)
|
|
|
+ .setTaxcode("1100101020200000000")
|
|
|
+ .setTax("0.13");
|
|
|
+
|
|
|
+ var head = new OpenBlueInvoiceRequest.HeadInfo()
|
|
|
+ .setOrderId(orderId)
|
|
|
+ .setB2cshopid(B2C_SHOP_ID)
|
|
|
+ .setKpje("0.02")
|
|
|
+ .setInvoicetitle("测试个人")
|
|
|
+ .setInvoiceTypeCode(InvoiceType.DIGITAL_NORMAL.getCode());
|
|
|
+
|
|
|
+ var request = new OpenBlueInvoiceRequest()
|
|
|
+ .setIssuerid(issuerId)
|
|
|
+ .setHead(head)
|
|
|
+ .setDetails(List.of(detail1, detail2))
|
|
|
+ .setTradeinfo(new OpenBlueInvoiceRequest.TradeInfo()
|
|
|
+ .setPayType(PayType.WECHAT_PAY.getCode())
|
|
|
+ .setOutTradeNo(orderId));
|
|
|
+
|
|
|
+ // 构建完整请求 JSON(模拟 SDK 的 buildBody)
|
|
|
+ JSONObject body = (JSONObject) com.alibaba.fastjson2.JSON.toJSON(request);
|
|
|
+ body.put("appsecret", APP_SECRET);
|
|
|
+ String reqJson = body.toJSONString();
|
|
|
+
|
|
|
+ // Step 1: 开票
|
|
|
+ var resp = client.openBlueInvoice(request);
|
|
|
+ System.out.println("开票响应: code=" + resp.getCode() + ", orderId=" + resp.getOrderId());
|
|
|
+ if (!resp.isSuccess()) {
|
|
|
+ System.out.println("【请求体】\n" + reqJson);
|
|
|
+ // 用原始 HTTP 获取完整响应
|
|
|
+ try {
|
|
|
+ var httpClient = new OkHttpClient.Builder()
|
|
|
+ .connectTimeout(10, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
|
|
|
+ var httpReq = new okhttp3.Request.Builder()
|
|
|
+ .url(BASE_URL + "/ewy_weberp/api/huapiaoer/outopenapis/openBlueInvoice")
|
|
|
+ .post(RequestBody.create(reqJson, MediaType.parse("application/json")))
|
|
|
+ .build();
|
|
|
+ var httpResp = httpClient.newCall(httpReq).execute();
|
|
|
+ String respBody = httpResp.body() != null ? httpResp.body().string() : "";
|
|
|
+ System.out.println("【原始响应】HTTP " + httpResp.code() + "\n" + respBody);
|
|
|
+ } catch (Exception ex) {
|
|
|
+ System.out.println("【原始请求异常】" + ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(resp.isSuccess(), "开票请求应返回 code=200, 实际 code=" + resp.getCode() + ", data=" + resp.getData());
|
|
|
+ System.out.println("开票请求成功!orderId=" + resp.getOrderId());
|
|
|
+
|
|
|
+ // Step 2: 轮询查询开票结果
|
|
|
+ System.out.println("开始轮询查询开票结果(每 5 秒一次,最多 2 分钟)...");
|
|
|
+ InvoiceInfo invoiceInfo = null;
|
|
|
+ for (int i = 0; i < 24; i++) {
|
|
|
+ try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
|
|
|
+ try {
|
|
|
+ invoiceInfo = client.getInvoice(orderId, InvoiceQueryType.BLUE.getCode());
|
|
|
+ System.out.println(" [" + ((i + 1) * 5) + "s] 查询成功!");
|
|
|
+ break;
|
|
|
+ } catch (HuapiaoerException e) {
|
|
|
+ System.out.println(" [" + ((i + 1) * 5) + "s] 尚未完成: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ assertNotNull(invoiceInfo, "应在超时前查询到发票结果");
|
|
|
+ System.out.println("=== 开票结果 ===");
|
|
|
+ System.out.println("发票号码: " + invoiceInfo.getInvoicenumber());
|
|
|
+ System.out.println("发票代码: " + invoiceInfo.getInvoicecode());
|
|
|
+ System.out.println("开票日期: " + invoiceInfo.getTicketDate());
|
|
|
+ System.out.println("价税合计: " + invoiceInfo.getTicketTotalAmountHasTax());
|
|
|
+ System.out.println("PDF 地址: " + invoiceInfo.getInvoiceurl());
|
|
|
+ System.out.println("OFD 地址: " + invoiceInfo.getOfdurl());
|
|
|
+ System.out.println("XML 地址: " + invoiceInfo.getXmlurl());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试 5:查询发票结果(只读,安全)。
|
|
|
+ * 用失败码测试错误处理是否正确。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testQueryNonExistentInvoice() {
|
|
|
+ System.out.println("=== 测试:查询不存在的发票 ===");
|
|
|
+ try {
|
|
|
+ client.getInvoice("NOT-EXIST-ORDER-001", InvoiceQueryType.BLUE.getCode());
|
|
|
+ System.out.println("意外的成功返回");
|
|
|
+ } catch (HuapiaoerException e) {
|
|
|
+ System.out.println("预期错误 - code=" + e.getCode() + ", msg=" + e.getMessage());
|
|
|
+ assertNotNull(e.getCode());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|