package com.talentcard.server.business.merchant;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.BusinessIdGenerator;
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.business.payment.huifu.HuifuPaymentProperties;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Single decision point for "may this merchant collect money?".
*
*
A merchant is payable only when the sub-merchant onboarding is approved, its settlement bank
* card carries a provider token, the provider account is in a normal state, automatic settlement is
* confirmed off and the encashment (取现) capability is confirmed available. When the provider
* integration is disabled the network configuration is not required, but the onboarding approval
* and the bound card token still are.
*/
@Service
public class MerchantPaymentReadinessService {
private static final Logger log = LoggerFactory.getLogger(MerchantPaymentReadinessService.class);
private final JdbcTemplate jdbc;
private final HuifuMerchantCapabilityGateway capabilityGateway;
private final HuifuPaymentProperties huifuPaymentProperties;
private final TransactionTemplate refreshTransaction;
private final TransactionTemplate providerTransaction;
public MerchantPaymentReadinessService(
JdbcTemplate jdbc,
HuifuMerchantCapabilityGateway capabilityGateway,
HuifuPaymentProperties huifuPaymentProperties,
TransactionTemplate refreshTransaction
) {
this.jdbc = jdbc;
this.capabilityGateway = capabilityGateway;
this.huifuPaymentProperties = huifuPaymentProperties;
this.refreshTransaction = refreshTransaction;
// The snapshot must survive a failed provider query in the caller's transaction, so it runs
// in its own transaction instead of joining the caller's.
this.refreshTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// The provider is an HTTP call: never make payment or onboarding hold a database transaction
// (and its row locks) while waiting for it.
this.providerTransaction = new TransactionTemplate(refreshTransaction.getTransactionManager());
this.providerTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
}
/** Readiness of the merchant's newest approved onboarding, refreshing the provider view when stale. */
@Transactional
public Readiness readiness(Long merchantId, Long storeId) {
Application application = approvedApplication(merchantId, storeId);
if (application == null) {
return Readiness.blocked("商户进件尚未审核通过,暂时无法收款");
}
if (!approvedBankCardExists(merchantId, application.id())) {
return Readiness.blocked("结算银行卡尚未绑定成功,暂时无法收款");
}
return readiness(application);
}
/**
* Returns the collecting sub-merchant an order must be bound to, or throws with the first unmet
* precondition. Callers persist the returned application, store and Huifu id on the order so the
* payment request cannot drift to another sub-merchant later.
*/
@Transactional
public CollectingMerchant requireCollectingMerchant(Long merchantId, Long storeId) {
Readiness readiness = readiness(merchantId, storeId);
if (!readiness.ready()) throw new BusinessException(ErrorCode.CONFLICT, readiness.reason());
return readiness.merchant();
}
/** Re-reads the provider configuration and stores the resulting snapshot. */
public Map refresh(Long applicationId) {
// The snapshot is only updated inside providerTransaction, which needs no row lock.
queryProviderOutsideTransaction(requireApprovedApplication(applicationId));
return capabilityView(requireApprovedApplication(applicationId));
}
/**
* Capability snapshot for the admin settlement page. It queries the provider when the snapshot
* has never been confirmed, because the explicit admin request is the moment to find out.
*/
public Map capability(Long applicationId) {
Application application = requireApprovedApplication(applicationId);
if (application.capabilityVerifiedAt().isBlank()) {
queryProviderOutsideTransaction(application);
application = requireApprovedApplication(applicationId);
}
return capabilityView(application);
}
/**
* Attempts to refresh approved applications whose capability snapshot is missing or older than
* the configured interval. Each application is refreshed in its own transaction, so one failing
* merchant never affects the others or the snapshots already stored.
*/
public int refreshStaleCapabilities(int staleHours, int limit) {
if (!providerVerificationRequired() || !huifuPaymentProperties.isEnabled()) return 0;
List