package com.talentcard.server.business.admin;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.mockito.ArgumentMatchers.*;
|
import static org.mockito.Mockito.*;
|
|
import com.talentcard.server.business.merchant.MerchantPaymentReadinessService;
|
import com.talentcard.server.business.merchant.MerchantSettlementService;
|
import com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway;
|
import com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway;
|
import com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway;
|
import com.talentcard.server.business.payment.huifu.HuifuPictureUploadGateway;
|
import com.talentcard.server.common.BusinessException;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
/**
|
* 外部进件商户的纳管与解绑(接口级,真实测试库 + 模拟汇付响应)。
|
*
|
* <p>断言只落在对外可见的行为上:纳管与解绑的输入校验与结果、纳管后该商家能通过下单闸门、
|
* 共用同一商户号的多个商家各自可用、重复纳管幂等、解绑的在途资金拦截,以及「纳管全程只查询不下发」。
|
* 不测内部 SQL 与私有方法调用顺序。
|
*/
|
@SpringBootTest
|
@ActiveProfiles("test")
|
class MerchantManagedOnboardingTests {
|
private static final String APPLY_NO = "HF-EXTERNAL-APPLY-001";
|
|
@Autowired JdbcTemplate jdbc;
|
@Autowired MerchantManagedOnboardingService service;
|
@Autowired MerchantSettlementService settlementService;
|
@Autowired MerchantPaymentReadinessService readinessService;
|
@Autowired MerchantSettlementAdminService adminSettlementService;
|
@MockitoBean HuifuOnboardingProgressGateway gateway;
|
// 纳管全程只查询不下发:这三个下发网关全部挡在这里,任何一次调用都会被断言抓住。
|
@MockitoBean HuifuMerchantWechatConfigGateway wechatConfigGateway;
|
@MockitoBean HuifuMerchantBusinessOpenGateway businessOpenGateway;
|
@MockitoBean HuifuPictureUploadGateway pictureUploadGateway;
|
|
@Test
|
void adoptingAChannelMerchantLetsTheStoreCollectThroughTheOrderGate() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("纳管成功");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
|
Map<String, Object> adopted = service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
|
assertThat(adopted).containsEntry("status", "approved");
|
assertThat(jdbc.queryForObject("""
|
select source from merchant_onboarding_application where merchant_id = ? and provider_huifu_id = ?
|
""", String.class, fixture.merchantId(), huifuId)).isEqualTo("managed");
|
assertThat(jdbc.queryForObject("""
|
select provider_apply_no from merchant_onboarding_application where merchant_id = ? and provider_huifu_id = ?
|
""", String.class, fixture.merchantId(), huifuId)).isEqualTo(APPLY_NO);
|
// 结算卡是硬前提:卡与绑卡序列号都从通道查回来,闸门才放行。
|
assertThat(jdbc.queryForObject("""
|
select provider_token_no from merchant_bank_card where merchant_id = ? and status = 'approved'
|
""", String.class, fixture.merchantId())).isEqualTo("external-card-token");
|
// 与自助进件走完全相同的收款就绪口径。
|
assertThat(readinessService.requireCollectingMerchant(fixture.merchantId(), fixture.storeId()).huifuId())
|
.isEqualTo(huifuId);
|
}
|
|
/**
|
* 进度刷新要能吃下「只有汇付侧入驻申请单号」的进件。
|
*
|
* <p>通道的申请单状态查询把 {@code apply_no} 列为必填,外部进件商户没有我方发起过的申请单号;
|
* 纳管时把汇付侧那个号一起录入,刷新流程无需改动即可跑通,不会永远停在未确认。
|
*/
|
@Test
|
void progressConvergesFromTheChannelApplyNumberAlone() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("通道申请单号");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
|
service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
|
verify(gateway).queryApplication(eq(huifuId), eq(APPLY_NO));
|
assertThat(adoptedApplicationId(fixture.merchantId(), huifuId)).isNotNull();
|
}
|
|
/** 通道查不到 = 该商户不在我们渠道商的管辖范围内,纳管必须明确拒绝并给出原因。 */
|
@Test
|
void aMerchantTheChannelCannotQueryIsRejectedWithItsReason() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("通道查不到");
|
when(gateway.queryCapabilities(huifuId)).thenReturn(Map.of("resp_code", "20002", "resp_desc", "数据权限认证失败"));
|
|
assertThatThrownBy(() -> service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("通道查不到该商户")
|
.hasMessageContaining("数据权限认证失败");
|
assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where merchant_id = ?",
|
Integer.class, fixture.merchantId())).isZero();
|
}
|
|
/** 没勾确认框不受理:名称核对是唯一防绑错的那道人工闸门。 */
|
@Test
|
void adoptingWithoutTheManualConfirmationIsRejected() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("未确认");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
|
assertThatThrownBy(() -> service.adopt(fixture.merchantId(), huifuId, APPLY_NO, false))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("勾选确认");
|
}
|
|
/** 入驻申请单号必填:取不到就不纳管(不做不依赖申请单号的备用判定分支)。 */
|
@Test
|
void adoptingWithoutTheChannelApplyNumberIsRejected() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("缺申请单号");
|
|
assertThatThrownBy(() -> service.adopt(fixture.merchantId(), huifuId, " ", true))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("入驻申请单号");
|
}
|
|
/**
|
* 主体名不符**只警示不硬拒**:我们这边只有一个商家名(常是简称或服务点名),通道那边是工商全称,
|
* 形态不对应,规则化匹配必然误判——误拒会把运营卡死且无路可绕。
|
*/
|
@Test
|
void aMerchantNameSharingNotASingleCharacterWarnsButStillAdopts() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("A甲乙丙丁戊己");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token", "上海汇付支付服务有限公司", "汇付支付");
|
|
Map<String, Object> preview = service.preview(fixture.merchantId(), huifuId);
|
|
assertThat(String.valueOf(preview.get("nameWarning"))).contains("没有任何相同字符");
|
assertThat(preview).containsEntry("channelMerchantName", "上海汇付支付服务有限公司");
|
// 警示不阻挡提交。
|
assertThat(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true)).containsEntry("status", "approved");
|
}
|
|
/** 名称有重合字符时不提示,避免天天喊狼来了。 */
|
@Test
|
void overlappingNamesProduceNoWarning() {
|
assertThat(MerchantManagedOnboardingService.nameWarning("汇付餐饮店", "上海汇付支付服务有限公司", "汇付支付")).isEmpty();
|
assertThat(MerchantManagedOnboardingService.nameWarning("测试商家", "上海汇付支付服务有限公司", "汇付支付")).isNotEmpty();
|
}
|
|
/** 同一个汇付商户号可以被多个商家分别纳管(连锁各门店共用一个收款主体),只提示不拦截。 */
|
@Test
|
void aHuifuIdAlreadyBoundToAnotherMerchantIsReportedButNotBlocked() {
|
String huifuId = uniqueHuifuId();
|
Fixture first = merchantFixture("连锁A店");
|
Fixture second = merchantFixture("连锁B店");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
|
service.adopt(first.merchantId(), huifuId, APPLY_NO, true);
|
// 门店名与商家名一样时不重复印一遍——多数门店就是这样,列表里不该出现「某某商户(某某商户)」。
|
jdbc.update("update merchant_store set store_name = ? where id = ?", first.key(), first.storeId());
|
Map<String, Object> preview = service.preview(second.merchantId(), huifuId);
|
|
assertThat(preview.get("boundMerchants"))
|
.asInstanceOf(org.assertj.core.api.InstanceOfAssertFactories.list(String.class))
|
.containsExactly(first.key());
|
assertThat((List<?>) preview.get("blockers")).as("已被其他商家绑定只是提示,不能拦").isEmpty();
|
assertThat(service.adopt(second.merchantId(), huifuId, APPLY_NO, true)).containsEntry("status", "approved");
|
assertThat(readinessService.requireCollectingMerchant(second.merchantId(), second.storeId()).huifuId()).isEqualTo(huifuId);
|
}
|
|
/** 重复纳管同一商家是幂等的,不会产生第二条进件记录。 */
|
@Test
|
void adoptingTheSameMerchantTwiceIsIdempotent() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("幂等");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
|
Map<String, Object> first = service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
Map<String, Object> second = service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
|
assertThat(second.get("id")).isEqualTo(first.get("id"));
|
assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where merchant_id = ?",
|
Integer.class, fixture.merchantId())).isEqualTo(1);
|
assertThat(jdbc.queryForObject("select count(*) from merchant_bank_card where merchant_id = ?",
|
Integer.class, fixture.merchantId())).isEqualTo(1);
|
// 已经纳管过的商户号在核验里只说明会复用原记录,不能报成「门店已有进件」这种硬拦截。
|
Map<String, Object> preview = service.preview(fixture.merchantId(), huifuId);
|
assertThat(preview).containsEntry("alreadyManaged", true);
|
assertThat((List<?>) preview.get("blockers")).isEmpty();
|
}
|
|
/**
|
* 命中幂等时不能报一句「纳管成功」却什么都没发生。
|
*
|
* <p>被通道驳回的记录刷新救不回来({@code supports()} 对 rejected 返回 false),所以必须明确让运营
|
* 去解绑后重来;否则运营会以为纳管生效了,而这条记录永远收不了款。
|
*/
|
@Test
|
void readoptingAChannelRejectedApplicationAsksTheOperatorToUnbindFirst() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("已驳回重纳管");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
jdbc.update("update merchant_onboarding_application set status = 'rejected' where merchant_id = ?", fixture.merchantId());
|
|
assertThatThrownBy(() -> service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("已被通道驳回")
|
.hasMessageContaining("解绑");
|
assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where merchant_id = ?",
|
Integer.class, fixture.merchantId())).isEqualTo(1);
|
}
|
|
/** 同一门店已经有进行中或已通过的进件时拒绝:两条收款主体会在下单时打架。 */
|
@Test
|
void adoptingASecondSubMerchantForTheSameStoreIsRejected() {
|
String firstHuifuId = uniqueHuifuId();
|
String secondHuifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("门店冲突");
|
stubConvergedChannel(firstHuifuId, "622200001234", "external-card-token");
|
stubConvergedChannel(secondHuifuId, "622200001235", "second-card-token");
|
|
service.adopt(fixture.merchantId(), firstHuifuId, APPLY_NO, true);
|
|
assertThat(service.preview(fixture.merchantId(), secondHuifuId).get("blockers"))
|
.asInstanceOf(org.assertj.core.api.InstanceOfAssertFactories.list(String.class))
|
.anySatisfy(blocker -> assertThat(blocker).contains("同一门店出现两个收款主体"));
|
assertThatThrownBy(() -> service.adopt(fixture.merchantId(), secondHuifuId, APPLY_NO, true))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("同一门店出现两个收款主体");
|
}
|
|
/**
|
* 纳管全程只查询、不下发:通道侧延迟入账没配好时,纳管**不会**替它配一次。
|
*
|
* <p>下发类操作不幂等(改支付场景会重建微信子商户号),且连锁多店共用同一个汇付商户号时各自
|
* 下发会重复下发。缺什么由界面按阶段如实显示,处理另走自助进件的既有路径。
|
*/
|
@Test
|
void adoptionNeverSendsAnyProviderChange() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("只查询不下发");
|
// 通道侧其他能力都正常,只有延迟入账没开——自助进件在这一点上会下发一次 delay 配置。
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
when(gateway.queryCapabilities(huifuId)).thenReturn(channelDetail(huifuId, "622200001234", "external-card-token", "N", false));
|
|
Map<String, Object> adopted = service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
|
verify(gateway, never()).configureDelay(anyString(), anyString(), anyString());
|
verify(wechatConfigGateway, never()).configure(anyString(), anyString());
|
verify(businessOpenGateway, never()).open(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyBoolean(), any(), any(), any());
|
verifyNoInteractions(pictureUploadGateway);
|
// 如实反映「这一步还没配好」,而不是伪造成功状态。
|
assertThat(adopted).containsEntry("status", "pending");
|
assertThat((List<Map<String, Object>>) adopted.get("stages"))
|
.anySatisfy(stage -> assertThat(stage).containsEntry("key", "delay").containsEntry("status", "missing"));
|
}
|
|
/** 后台/商户端点「刷新进度」对纳管记录同样是只查询:不会走开通步骤的那条下发链路。 */
|
@Test
|
void refreshingAManagedApplicationStaysQueryOnly() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("刷新只查询");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
Map<String, Object> adopted = service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
|
settlementService.refreshProgress(Long.valueOf(String.valueOf(adopted.get("id"))), false);
|
|
verify(wechatConfigGateway, never()).configure(anyString(), anyString());
|
verify(businessOpenGateway, never()).open(anyString(), anyString(), anyString(), anyString(), anyBoolean(), anyBoolean(), any(), any(), any());
|
verify(gateway, never()).configureDelay(anyString(), anyString(), anyString());
|
// 通道返回的一切都按原样落库,进度仍是已通过。
|
assertThat(settlementService.refreshProgress(Long.valueOf(String.valueOf(adopted.get("id"))), false))
|
.containsEntry("status", "approved");
|
}
|
|
/** 解绑清理进件记录与结算卡记录,两者都从后台列表消失。 */
|
@Test
|
void unbindingRemovesTheOnboardingAndItsSettlementCard() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("解绑");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
Long applicationId = Long.valueOf(String.valueOf(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true).get("id")));
|
assertThat(adminSettlementService.bankCards(1, 20, fixture.key(), null, null, null, null, null).records()).hasSize(1);
|
|
Map<String, Object> result = service.unbind(applicationId);
|
|
assertThat(result).containsEntry("unbound", true);
|
assertThat(adminSettlementService.onboardings(1, 20, fixture.key(), null, null, null, null, null).records()).isEmpty();
|
assertThat(adminSettlementService.bankCards(1, 20, fixture.key(), null, null, null, null, null).records()).isEmpty();
|
assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_progress where application_id = ?",
|
Integer.class, applicationId)).isZero();
|
}
|
|
/** 该商户号上还有已收款未结算的订单时拒绝解绑:那是正在进行的对账,不能切断。 */
|
@Test
|
void unbindingIsRefusedWhileMoneyIsStillUnsettled() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("待结算");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
Long applicationId = Long.valueOf(String.valueOf(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true).get("id")));
|
insertOrder(fixture, applicationId, huifuId, /*payAmount*/ 1000, /*orderStatus*/ 1, /*refundStatus*/ 0);
|
|
assertThatThrownBy(() -> service.unbind(applicationId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("已收款未结算");
|
assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where id = ?",
|
Integer.class, applicationId)).isEqualTo(1);
|
}
|
|
@Test
|
void unbindingIsRefusedWhileOrdersAreStillInFlight() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("在途订单");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
Long applicationId = Long.valueOf(String.valueOf(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true).get("id")));
|
insertOrder(fixture, applicationId, huifuId, /*payAmount*/ 1000, /*orderStatus*/ 0, /*refundStatus*/ 0);
|
|
assertThatThrownBy(() -> service.unbind(applicationId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("在途订单");
|
}
|
|
/** 已取消/已退款的终态订单不拦解绑:那不是在途资金。 */
|
@Test
|
void settledOrRefundedOrdersDoNotBlockUnbinding() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("终态订单");
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
Long applicationId = Long.valueOf(String.valueOf(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true).get("id")));
|
insertOrder(fixture, applicationId, huifuId, /*payAmount*/ 1000, /*orderStatus*/ 2, /*refundStatus*/ 0);
|
insertOrder(fixture, applicationId, huifuId, /*payAmount*/ 1000, /*orderStatus*/ 1, /*refundStatus*/ 2);
|
|
assertThat(service.unbind(applicationId)).containsEntry("unbound", true);
|
}
|
|
/** 自助进件的记录不支持解绑:那是商户自己提交的资料,删掉它是另一个性质的操作。 */
|
@Test
|
void aSelfOnboardedApplicationCannotBeUnbound() {
|
Fixture fixture = merchantFixture("自助进件");
|
jdbc.update("""
|
insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type,
|
subject_name, contact_name, contact_mobile, idempotency_key, status, source)
|
values (?, ?, ?, 'enterprise', '自助主体', '自助联系人', '13700000000', ?, 'pending', 'self')
|
""", fixture.merchantId(), fixture.storeId(), "MO-SELF-" + fixture.key(), fixture.key());
|
Long applicationId = jdbc.queryForObject("select id from merchant_onboarding_application where merchant_id = ?",
|
Long.class, fixture.merchantId());
|
|
assertThatThrownBy(() -> service.unbind(applicationId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("不支持解绑");
|
}
|
|
/** 解绑只解除我方绑定:商户端登录账号与门店资料都不动,之后可以重新纳管。 */
|
@Test
|
void unbindingKeepsTheMerchantAccountAndAllowsAdoptingAgain() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("可重新纳管");
|
jdbc.update("insert into merchant_operator(merchant_id, account, password_hash, display_name, role_code, status, deleted) values (?, ?, 'x', '店主', 'owner', 1, 0)",
|
fixture.merchantId(), "t01op" + Math.abs(System.nanoTime()));
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
Long applicationId = Long.valueOf(String.valueOf(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true).get("id")));
|
|
service.unbind(applicationId);
|
Map<String, Object> again = service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true);
|
|
assertThat(again).containsEntry("status", "approved");
|
assertThat(again.get("id")).isNotEqualTo(String.valueOf(applicationId));
|
assertThat(jdbc.queryForObject("select count(*) from merchant_operator where merchant_id = ? and status = 1 and deleted = 0",
|
Integer.class, fixture.merchantId())).isEqualTo(1);
|
}
|
|
/** 该商家还没有商户端登录账号时纳管照做,但要提示运营,否则商户登不进来。 */
|
@Test
|
void adoptionWarnsWhenTheMerchantCannotSignInYet() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("未开登录");
|
jdbc.update("update merchant set can_login_merchant_app = 0 where id = ?", fixture.merchantId());
|
stubConvergedChannel(huifuId, "622200001234", "external-card-token");
|
|
Map<String, Object> preview = service.preview(fixture.merchantId(), huifuId);
|
|
assertThat(String.valueOf(preview.get("loginNotice"))).contains("未开启商户端登录");
|
// 提示不阻挡纳管:纳管只管收款绑定,账号由「商家查询」负责。
|
assertThat(service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true)).containsEntry("status", "approved");
|
}
|
|
/** 通道没给出可用结算卡时不纳管:没有结算卡,进度不会推进、闸门也不会放行。 */
|
@Test
|
void adoptingIsRejectedWhenTheChannelReturnsNoUsableCard() {
|
String huifuId = uniqueHuifuId();
|
Fixture fixture = merchantFixture("无结算卡");
|
Map<String, Object> detail = channelDetail(huifuId, "622200001234", "token", "Y", true);
|
detail.put("qry_cash_card_info_list", List.of(Map.of("card_no", "6222****1234", "token_no", "masked-token")));
|
when(gateway.queryCapabilities(huifuId)).thenReturn(detail);
|
|
assertThat(service.preview(fixture.merchantId(), huifuId).get("blockers"))
|
.asInstanceOf(org.assertj.core.api.InstanceOfAssertFactories.list(String.class))
|
.anySatisfy(blocker -> assertThat(blocker).contains("结算银行卡"));
|
assertThatThrownBy(() -> service.adopt(fixture.merchantId(), huifuId, APPLY_NO, true))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("结算银行卡");
|
}
|
|
private Long adoptedApplicationId(Long merchantId, String huifuId) {
|
return jdbc.queryForObject("select id from merchant_onboarding_application where merchant_id = ? and provider_huifu_id = ?",
|
Long.class, merchantId, huifuId);
|
}
|
|
/** 通道侧一切就绪(六阶段 + 结算卡 + 商户状态正常)的商户详情回执。 */
|
private void stubConvergedChannel(String huifuId, String cardNumber, String token) {
|
stubConvergedChannel(huifuId, cardNumber, token, "攀枝花测试餐饮有限公司", "测试餐饮");
|
}
|
|
private void stubConvergedChannel(String huifuId, String cardNumber, String token, String registeredName, String shortName) {
|
when(gateway.queryCapabilities(huifuId)).thenReturn(channelDetail(huifuId, cardNumber, token, "Y", true, registeredName, shortName));
|
when(gateway.queryApplication(anyString(), anyString())).thenReturn(Map.of(
|
"resp_code", "00000000", "apply_status", "Y", "reg_status", "S", "bind_card_status", "S"));
|
when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code", "00000000", "atu_config_bo_list",
|
List.of(Map.of("fee_type", "02", "wx_config_list",
|
List.of(Map.of("config_type", "1", "config_value", "wxcfee477acb54d179"))))));
|
when(gateway.queryRealname(anyString())).thenReturn(Map.of(
|
"resp_code", "00000000", "applyment_stat", "APPLYMENT_STATE_PASSED", "authorize_stat", "1"));
|
}
|
|
/**
|
* 商户详细信息查询的回执:名称、主体类型、结算卡、微信场景、结算配置与延迟配置。
|
*
|
* <p>{@code delayFlag} 传 {@code N} 就是「通道侧还没开延迟入账」——自助进件在这一点上会下发一次
|
* 配置,纳管不允许下发。
|
*/
|
private Map<String, Object> channelDetail(String huifuId, String cardNumber, String token, String delayFlag, boolean settlementConfigured) {
|
return channelDetail(huifuId, cardNumber, token, delayFlag, settlementConfigured, "攀枝花测试餐饮有限公司", "测试餐饮");
|
}
|
|
private Map<String, Object> channelDetail(String huifuId, String cardNumber, String token, String delayFlag,
|
boolean settlementConfigured, String registeredName, String shortName) {
|
Map<String, Object> detail = new LinkedHashMap<>();
|
detail.put("resp_code", "00000000");
|
detail.put("huifu_id", huifuId);
|
detail.put("reg_name", registeredName);
|
detail.put("short_name", shortName);
|
detail.put("cust_type", "1");
|
detail.put("ent_type", "3");
|
detail.put("legal_name", "测试法人");
|
detail.put("legal_mobile_no", "13700000009");
|
detail.put("contact_name", "测试联系人");
|
detail.put("contact_mobile_no", "13700000008");
|
detail.put("mer_stat", "NORMAL");
|
detail.put("delay_flag", delayFlag);
|
detail.put("forced_delay_flag", "N");
|
detail.put("qry_cash_card_info_list", List.of(Map.of(
|
"card_no", cardNumber, "token_no", token, "status", "N", "is_settle_default", "Y",
|
"bank_name", "中国工商银行", "branch_name", "中国工商银行攀枝花东区支行", "branch_code", "102290026507",
|
"area_id", "510400", "card_name", registeredName)));
|
List<Map<String, Object>> settleConfigs = settlementConfigured
|
? List.of(Map.of("settle_status", "1", "settle_cycle", "D1", "settle_pattern", "P0",
|
"min_amt", "0", "remained_amt", "0", "token_no", token))
|
: List.of();
|
detail.put("qry_settle_config_list", settleConfigs);
|
detail.put("qry_wx_conf_list", wechatScenes());
|
return detail;
|
}
|
|
private static List<Map<String, Object>> wechatScenes() {
|
return List.of("1", "2", "3").stream().map(scene -> Map.of(
|
"pay_scene", scene, "fee_rate", "0.30", "fee_min_amt", "0.00",
|
"fee_charge_type", "1", "out_fee_flag", "2",
|
"wx_mer_infos", List.of(Map.of("bank_mer_code", "922609119", "pay_channel_id", "00016904", "is_default", "1"))
|
)).toList();
|
}
|
|
/** 一笔处于某种状态的订单;纳管记录会被订单上的收款绑定指回来。 */
|
private void insertOrder(Fixture fixture, Long applicationId, String huifuId, long payAmount, int orderStatus, int refundStatus) {
|
jdbc.update("""
|
insert into benefit_order(order_no, talent_id, merchant_id, store_id, order_type, pay_amount,
|
order_status, verify_status, refund_status, collect_onboarding_id, collect_huifu_id)
|
values (?, 1, ?, ?, 'STORE_CONSUME', ?, ?, 0, ?, ?, ?)
|
""", "T01O-" + fixture.key() + "-" + payAmount + orderStatus + refundStatus, fixture.merchantId(),
|
fixture.storeId(), payAmount, orderStatus, refundStatus, applicationId, huifuId);
|
}
|
|
private Fixture merchantFixture(String label) {
|
String key = "T01" + label + System.nanoTime();
|
jdbc.update("""
|
insert into merchant(merchant_name, merchant_type, contact_name, contact_mobile, can_login_merchant_app, status, deleted)
|
values (?, '餐饮服务', '联系人', '13700000001', 1, 1, 0)
|
""", key);
|
Long merchantId = jdbc.queryForObject("select id from merchant where merchant_name = ?", Long.class, key);
|
jdbc.update("insert into merchant_store(merchant_id, store_name, status, deleted) values (?, ?, 1, 0)", merchantId, key + "门店");
|
Long storeId = jdbc.queryForObject("select id from merchant_store where merchant_id = ?", Long.class, merchantId);
|
return new Fixture(key, merchantId, storeId);
|
}
|
|
/** 每个用例用自己的商户号,避免共享测试库上互相串。 */
|
private static String uniqueHuifuId() {
|
return "6666" + String.format("%014d", Math.abs(System.nanoTime() % 100_000_000_000_000L));
|
}
|
|
private record Fixture(String key, Long merchantId, Long storeId) { }
|
}
|