package com.talentcard.server.business.merchant;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.mockito.ArgumentMatchers.*;
|
import static org.mockito.Mockito.*;
|
import java.util.Map;
|
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
|
import com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway;
|
import org.junit.jupiter.api.BeforeEach;
|
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>原先的做法是把进件和银行卡一起标成 rejected,好让商户用同一张卡整单重提;规格已明确改成
|
* 「基本进件成功后的局部失败保留原商户号和银行卡」。这类申请现在走分阶段模型,
|
* {@code closeRetryableApplication} 对它只刷新进度、不释放任何东西。
|
*
|
* <p>例外是被通道**明确驳回基本资料**的申请:它交回旧的单状态模型({@code supports()} 对
|
* {@code rejected} 返回 false),商户端才拿得到「重新提交」入口。
|
*/
|
@SpringBootTest
|
@ActiveProfiles("test")
|
class MerchantOnboardingRetryReleaseTests {
|
|
@Autowired
|
private JdbcTemplate jdbc;
|
|
@Autowired
|
private MerchantSettlementService settlementService;
|
|
// 分阶段刷新会先推进开通步骤,这几个网关都要挡住,否则测试会真的打到汇付。
|
@MockitoBean private HuifuOnboardingProgressGateway progressGateway;
|
@MockitoBean private com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway wechatConfigGateway;
|
@MockitoBean private com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway businessOpenGateway;
|
|
private Long merchantId;
|
private Long storeId;
|
private Long applicationId;
|
private Long bankCardId;
|
private String suffix;
|
|
@BeforeEach
|
void prepare() {
|
suffix = String.valueOf(System.nanoTime());
|
jdbc.update("insert into merchant(merchant_name, status, deleted) values (?, 1, 0)", "Retry merchant " + suffix);
|
merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class);
|
jdbc.update("insert into merchant_store(merchant_id, store_name, status, deleted) values (?, ?, 1, 0)", merchantId, "Retry store " + suffix);
|
storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
|
}
|
|
@Test
|
void aFailedProvisioningKeepsTheApplicationAndItsBankCard() {
|
seedApplication("failed", "not_started");
|
stubProvider();
|
|
settlementService.closeRetryableApplication(principal());
|
|
// 关键变化:不再释放。进件与银行卡都留着,失败的开通步骤由刷新去重试——
|
// 释放掉就等于让商户整单重来,与「保留原商户号和银行卡」相悖。
|
assertThat(status("merchant_onboarding_application", applicationId)).isEqualTo("pending");
|
assertThat(status("merchant_bank_card", bankCardId)).isEqualTo("pending");
|
}
|
|
@Test
|
void aFailedBusinessOpenKeepsTheCardForRetry() {
|
seedApplication("approved", "failed");
|
stubProvider();
|
|
settlementService.closeRetryableApplication(principal());
|
|
assertThat(status("merchant_onboarding_application", applicationId)).isEqualTo("pending");
|
assertThat(status("merchant_bank_card", bankCardId)).isEqualTo("pending");
|
}
|
|
@Test
|
void provisioningStillInFlightIsLeftAlone() {
|
seedApplication("processing", "not_started");
|
|
settlementService.closeRetryableApplication(principal());
|
|
assertThat(status("merchant_onboarding_application", applicationId)).isEqualTo("pending");
|
assertThat(status("merchant_bank_card", bankCardId)).isEqualTo("pending");
|
}
|
|
/** 被驳回的申请回到旧的单状态模型,刷新不再接管它——否则会被翻回 pending,重提入口消失。 */
|
@Test
|
void aRejectedApplicationIsLeftToTheSingleStatusModel() {
|
seedApplication("failed", "failed");
|
jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason='基本资料审核未通过' where id=?", applicationId);
|
|
Map<String,Object> view = settlementService.refreshProgress(applicationId, false);
|
|
assertThat(view).containsEntry("status", "rejected");
|
verifyNoInteractions(progressGateway);
|
}
|
|
private void stubProvider() {
|
when(progressGateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
|
when(progressGateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"));
|
when(progressGateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000"));
|
}
|
|
private void seedApplication(String wechatConfigStatus, String businessOpenStatus) {
|
String applicationNo = "MO-RT-" + suffix + "-" + wechatConfigStatus;
|
jdbc.update("""
|
insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name,
|
contact_name, contact_mobile, idempotency_key, status,
|
wechat_config_status, business_open_status)
|
values (?, ?, ?, 'individual', 'Retry owner', 'Retry owner', '13700000001', ?, 'pending', ?, ?)
|
""", merchantId, storeId, applicationNo, "retry-onboarding-" + suffix, wechatConfigStatus, businessOpenStatus);
|
applicationId = jdbc.queryForObject("select max(id) from merchant_onboarding_application", Long.class);
|
jdbc.update("""
|
insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, city_code, card_tail, card_masked,
|
card_hash, status, idempotency_key, provider_request_no)
|
values (?, ?, '中国建设银行', '广安人民路支行', '511600', '0924', '**** **** **** 0924', ?, 'pending', ?, ?)
|
""", merchantId, storeId, "hash-retry-" + suffix, "retry-card-" + suffix, applicationNo);
|
bankCardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
|
jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", bankCardId, applicationId);
|
}
|
|
private MerchantPrincipal principal() { return new MerchantPrincipal(0L, merchantId, storeId, "operator"); }
|
|
private String status(String table, Long id) {
|
return jdbc.queryForObject("select status from " + table + " where id = ?", String.class, id);
|
}
|
}
|