package com.talentcard.server.business.merchant;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
import java.util.Map;
|
|
import com.talentcard.server.business.payment.huifu.HuifuMerchantCapabilityGateway;
|
import com.talentcard.server.business.payment.huifu.HuifuMerchantCapabilityQueryCommand;
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
|
import com.talentcard.server.common.BusinessException;
|
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.TestPropertySource;
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
/**
|
* Verifies the merchant payment readiness rules with the provider confirmation switched on and a
|
* stubbed Huifu capability query.
|
*/
|
@SpringBootTest
|
@ActiveProfiles("test")
|
@TestPropertySource(properties = {
|
"app.payment.huifu.enabled=true",
|
"app.payment.huifu.merchant-capability-required=true"
|
})
|
class MerchantPaymentReadinessServiceTests {
|
|
@Autowired
|
private JdbcTemplate jdbc;
|
|
@Autowired
|
private MerchantPaymentReadinessService readinessService;
|
|
@MockitoBean
|
private HuifuMerchantCapabilityGateway capabilityGateway;
|
|
private Long merchantId;
|
private Long storeId;
|
private Long applicationId;
|
private Long bankCardId;
|
private String suffix;
|
|
@BeforeEach
|
void prepareMerchant() {
|
suffix = String.valueOf(System.nanoTime());
|
jdbc.update("insert into merchant(merchant_name, status, deleted) values (?, 1, 0)", "Readiness 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, "Readiness store " + suffix);
|
storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
|
jdbc.update("""
|
insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name,
|
contact_name, contact_mobile, idempotency_key, status, provider_huifu_id)
|
values (?, ?, ?, 'individual', 'Readiness owner', 'Readiness owner', '13800000000', ?, 'approved', ?)
|
""", merchantId, storeId, "MO-RDY-" + suffix, "readiness-onboarding-" + suffix, shortReference("H", suffix));
|
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_token_no)
|
values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '6666', '**** **** **** 6666', ?, 'approved', ?, ?)
|
""", merchantId, storeId, "hash-readiness-" + suffix, "readiness-card-" + suffix, shortReference("T", suffix));
|
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);
|
}
|
|
@Test
|
void allowsPaymentOnceAutoSettlementIsOffAndEncashmentIsConfirmed() {
|
stubProvider("NORMAL", "0", "1");
|
|
MerchantPaymentReadinessService.Readiness readiness = readinessService.readiness(merchantId, storeId);
|
|
assertThat(readiness.ready()).isTrue();
|
assertThat(readiness.reason()).isEmpty();
|
assertThat(jdbc.queryForMap("select auto_settle_status as autoSettle, cash_switch_status as cashSwitch, cash_type as cashType, merchant_stat as merchantStat, capability_verified_at as verifiedAt from merchant_onboarding_application where id=?", applicationId))
|
.containsEntry("AUTOSETTLE", "0")
|
.containsEntry("CASHSWITCH", "1")
|
.containsEntry("CASHTYPE", "DM");
|
assertThat(jdbc.queryForObject("select capability_verified_at from merchant_onboarding_application where id=?", Object.class, applicationId)).isNotNull();
|
}
|
|
@Test
|
void blocksPaymentWhileAutoSettlementIsStillOn() {
|
stubProvider("NORMAL", "1", "1");
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("自动结算");
|
}
|
|
@Test
|
void blocksPaymentWhenEncashmentCapabilityIsUnavailable() {
|
stubProvider("NORMAL", "0", "0");
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("取现");
|
}
|
|
@Test
|
void blocksPaymentWhenTheSubMerchantIsFrozen() {
|
stubProvider("FREEZEWITHDRAWAL", "0", "1");
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("FREEZEWITHDRAWAL");
|
}
|
|
@Test
|
void blocksPaymentWhenTheProviderRejectsTheCapabilityQuery() {
|
when(capabilityGateway.query(any(HuifuMerchantCapabilityQueryCommand.class)))
|
.thenReturn(HuifuMerchantCapabilityGateway.CapabilityResult.failed("99999999", "商户不存在"));
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("待确认");
|
}
|
|
@Test
|
void blocksPaymentWhenTheProviderQueryTimesOut() {
|
when(capabilityGateway.query(any(HuifuMerchantCapabilityQueryCommand.class)))
|
.thenThrow(new HuifuPaymentGatewayException("timeout", true, null));
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class);
|
}
|
|
@Test
|
void blocksPaymentWhenOnboardingIsNotApproved() {
|
jdbc.update("update merchant_onboarding_application set status='pending' where id=?", applicationId);
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("进件");
|
verify(capabilityGateway, never()).query(any(HuifuMerchantCapabilityQueryCommand.class));
|
}
|
|
@Test
|
void blocksPaymentWhenTheSettlementCardHasNoToken() {
|
jdbc.update("update merchant_bank_card set provider_token_no=null where id=?", bankCardId);
|
|
assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("银行卡");
|
verify(capabilityGateway, never()).query(any(HuifuMerchantCapabilityQueryCommand.class));
|
}
|
|
@Test
|
void reusesTheConfirmedSnapshotInsteadOfQueryingTheProviderAgain() {
|
stubProvider("NORMAL", "0", "1");
|
readinessService.requirePayable(merchantId, storeId);
|
|
readinessService.requirePayable(merchantId, storeId);
|
|
verify(capabilityGateway, times(1)).query(any(HuifuMerchantCapabilityQueryCommand.class));
|
}
|
|
@Test
|
void refreshesTheSnapshotOnDemandForTheAdminView() {
|
stubProvider("NORMAL", "1", "1");
|
Map<String, Object> capability = readinessService.capability(applicationId);
|
|
assertThat(capability).containsEntry("autoSettleDisabled", false).containsEntry("cashAvailable", true);
|
|
stubProvider("NORMAL", "0", "1");
|
Map<String, Object> refreshed = readinessService.refresh(applicationId);
|
|
assertThat(refreshed).containsEntry("autoSettleDisabled", true).containsEntry("cashAvailable", true);
|
}
|
|
/** Provider identifiers are limited to 32 characters and huifu ids to 18, so tests keep them short. */
|
private String shortReference(String prefix, String suffix) {
|
return prefix + suffix.substring(Math.max(0, suffix.length() - 8));
|
}
|
|
private void stubProvider(String merchantStatus, String settleStatus, String cashSwitch) {
|
when(capabilityGateway.query(any(HuifuMerchantCapabilityQueryCommand.class)))
|
.thenReturn(new HuifuMerchantCapabilityGateway.CapabilityResult(true, "00000000", "成功", merchantStatus, settleStatus, "D1", cashSwitch, "DM"));
|
}
|
}
|