import java.util.Map;
|
import java.util.UUID;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
import org.springframework.transaction.support.TransactionTemplate;
|
import static org.mockito.Mockito.*;
|
import com.talentcard.server.business.merchant.*;
|
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
|
import com.talentcard.server.business.admin.MerchantSettlementAdminService;
|
import com.talentcard.server.business.payment.*;
|
import com.talentcard.server.business.payment.huifu.*;
|
|
/** Audit-only characterization probe. REPRODUCED means an unsafe behavior exists, not a safety test passing.
|
* Uses actual service code and SQL in isolated H2; only provider calls/signature verification are mocked.
|
* Does not load application configuration, start Spring, or contact any external service.
|
*/
|
public class PaymentFlowAuditProbe {
|
private JdbcTemplate jdbc;
|
private TransactionTemplate tx;
|
private MerchantSettlementService settlement;
|
private HuifuMerchantWithdrawalService withdrawals;
|
|
public static void main(String[] args) throws Exception {
|
PaymentFlowAuditProbe p = new PaymentFlowAuditProbe();
|
p.multipleStoresCountSameOrder();
|
p.partialRefundDoesNotReduceAvailable();
|
p.adminFailureReleasesUnconfirmedWithdrawal();
|
p.differentKeysCreateTwoActivePayments();
|
p.expiredOrderAcceptsMoneyButRemainsCancelled();
|
p.callbackDoesNotValidateAmount();
|
p.withdrawalOverwritesPaymentSdkConfiguration();
|
System.out.println("REPRODUCED 7/7 audit scenarios (not evidence of financial safety).");
|
}
|
|
private void setup() {
|
DriverManagerDataSource ds = new DriverManagerDataSource(
|
"jdbc:h2:mem:audit_" + UUID.randomUUID() + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
|
jdbc = new JdbcTemplate(ds);
|
tx = new TransactionTemplate(new DataSourceTransactionManager(ds));
|
jdbc.execute("create table merchant_store(id bigint primary key, merchant_id bigint, status int, deleted int)");
|
jdbc.execute("create table talent_user(id bigint primary key, openid varchar(64), deleted int)");
|
jdbc.execute("""
|
create table benefit_order(id bigint primary key, order_no varchar(64), talent_id bigint,
|
merchant_id bigint, store_id bigint, pay_amount bigint, order_status int, verify_status int,
|
deleted int default 0, price_version int default 1, expires_at timestamp,
|
product_name_snapshot varchar(64), order_type varchar(32))
|
""");
|
jdbc.execute("""
|
create table merchant_withdrawal(id bigint primary key, merchant_id bigint, store_id bigint,
|
withdrawal_no varchar(64), amount_cents bigint, status varchar(32), failure_reason varchar(255),
|
reviewed_at timestamp, paid_at timestamp, provider_trade_no varchar(64),
|
provider_response_code varchar(64), provider_response_message varchar(255))
|
""");
|
jdbc.execute("""
|
create table payment_transaction(id bigint auto_increment primary key, payment_no varchar(64) unique,
|
provider varchar(32), order_id bigint, order_no varchar(64), talent_id bigint, merchant_id bigint,
|
amount bigint, status varchar(32), idempotency_key varchar(128), attempt_no int,
|
client_payment_payload varchar(2000), provider_transaction_no varchar(64),
|
provider_response_code varchar(64), provider_response_message varchar(255), paid_at timestamp,
|
unique(idempotency_key, attempt_no))
|
""");
|
jdbc.execute("""
|
create table order_refund(id bigint primary key, order_id bigint, refund_no varchar(64),
|
refund_amount bigint, refund_status int, deleted int default 0)
|
""");
|
jdbc.execute("""
|
create table payment_callback_event(provider varchar(32), provider_request_no varchar(64),
|
payload_sha256 varchar(64), signature_valid int, process_status varchar(32),
|
received_at timestamp, processed_at timestamp, unique(provider, provider_request_no))
|
""");
|
jdbc.update("insert into merchant_store values(10,1,1,0),(20,1,1,0)");
|
jdbc.update("insert into talent_user values(1,'audit-open-id',0)");
|
withdrawals = new HuifuMerchantWithdrawalService(jdbc, mock(HuifuWithdrawalGateway.class));
|
settlement = new MerchantSettlementService(jdbc, null, null, null, null, null, withdrawals, null, null, null);
|
}
|
|
private void order(Integer storeId, int state, int verified) {
|
jdbc.update("""
|
insert into benefit_order(id,order_no,talent_id,merchant_id,store_id,pay_amount,order_status,
|
verify_status,expires_at,product_name_snapshot,order_type)
|
values(1,'AUDIT-O',1,1,?,10000,?,?,dateadd('MINUTE',15,current_timestamp),'Audit product','STORE_CONSUME')
|
""", storeId, state, verified);
|
}
|
private long available(long storeId) {
|
return ((Number) settlement.withdrawalSummary(new MerchantPrincipal(1L, 1L, storeId, "OWNER"))
|
.get("availableAmountCents")).longValue();
|
}
|
private void payment(String state) {
|
jdbc.update("""
|
insert into payment_transaction(payment_no,provider,order_id,order_no,talent_id,merchant_id,
|
amount,status,idempotency_key,attempt_no)
|
values('AUDIT-P','HUIFU',1,'AUDIT-O',1,1,10000,?,'audit-key',1)
|
""", state);
|
}
|
private void paidCallback(String amount) {
|
HuifuSignatureService signature = mock(HuifuSignatureService.class);
|
when(signature.verifyCallbackPayload(anyString(), anyString())).thenReturn(true);
|
HuifuPaymentCallbackService callback = new HuifuPaymentCallbackService(jdbc, signature);
|
tx.executeWithoutResult(s -> callback.handle("{\"req_seq_id\":\"AUDIT-P\",\"trans_stat\":\"S\","
|
+ "\"resp_code\":\"00000000\",\"hf_seq_id\":\"AUDIT-HF\",\"trans_amt\":\"" + amount + "\"}", "mocked-valid-signature"));
|
}
|
private void multipleStoresCountSameOrder() {
|
setup(); order(null, 1, 1);
|
require(available(10) == 10000 && available(20) == 10000, "multi-store duplication");
|
System.out.println("REPRODUCED: one 100.00 order -> store A 100.00 + store B 100.00 available.");
|
}
|
private void partialRefundDoesNotReduceAvailable() {
|
setup(); order(10, 1, 1);
|
jdbc.update("insert into order_refund values(1,1,'AUDIT-R',4000,2,0)");
|
require(available(10) == 10000, "refund not deducted");
|
System.out.println("REPRODUCED: paid 100.00, refunded 40.00 -> still 100.00 withdrawable.");
|
}
|
private void adminFailureReleasesUnconfirmedWithdrawal() {
|
setup(); order(10, 1, 1);
|
jdbc.update("insert into merchant_withdrawal(id,merchant_id,store_id,withdrawal_no,amount_cents,status) values(1,1,10,'AUDIT-W',10000,'processing')");
|
require(available(10) == 0, "initial reservation");
|
new MerchantSettlementAdminService(jdbc, withdrawals, null).reviewWithdrawal(1L, "failed", "audit manual action");
|
withdrawals.handleCallback("AUDIT-W", Map.of("resp_code", "00000000", "trans_stat", "S", "hf_seq_id", "AUDIT-PAID"));
|
require(available(10) == 10000, "released balance after provider success");
|
require("failed".equals(jdbc.queryForObject("select status from merchant_withdrawal where id=1", String.class)), "late success ignored");
|
System.out.println("REPRODUCED: admin fails processing withdrawal; provider success ignored; 100.00 released.");
|
}
|
private void differentKeysCreateTwoActivePayments() {
|
setup(); order(10, 0, 0);
|
HuifuMiniProgramPaymentGateway gateway = mock(HuifuMiniProgramPaymentGateway.class);
|
when(gateway.createMiniProgramPayment(any())).thenReturn(Map.of("data", Map.of("trans_stat", "P", "resp_code", "00000000", "pay_info", Map.of("package", "audit"))));
|
HuifuPaymentService service = new HuifuPaymentService(jdbc, tx, gateway);
|
service.beginMiniProgramPayment("AUDIT-O", 1L, 1, "device-a");
|
service.beginMiniProgramPayment("AUDIT-O", 1L, 1, "device-b");
|
require(jdbc.queryForObject("select count(*) from payment_transaction where status='PREPAY_CREATED'", Integer.class) == 2, "two live payment attempts");
|
verify(gateway, times(2)).createMiniProgramPayment(any());
|
System.out.println("REPRODUCED: one order + two request keys -> two provider prepayments.");
|
}
|
private void expiredOrderAcceptsMoneyButRemainsCancelled() {
|
setup(); order(10, 2, 0); payment("PREPAY_CREATED"); paidCallback("100.00");
|
require("SUCCEEDED".equals(jdbc.queryForObject("select status from payment_transaction", String.class)), "payment success");
|
require(jdbc.queryForObject("select order_status from benefit_order", Integer.class) == 2, "order remains cancelled");
|
require(jdbc.queryForObject("select count(*) from order_refund", Integer.class) == 0, "no compensating refund");
|
System.out.println("REPRODUCED: expired order + valid payment success -> SUCCEEDED payment, cancelled order, no refund.");
|
}
|
private void callbackDoesNotValidateAmount() {
|
setup(); order(10, 0, 0); payment("PREPAY_CREATED"); paidCallback("0.01");
|
require(jdbc.queryForObject("select order_status from benefit_order", Integer.class) == 1, "amount mismatch accepted");
|
System.out.println("REPRODUCED: authenticated callback for 0.01 marks a 100.00 order paid (signature mocked, not bypassed).");
|
}
|
private void withdrawalOverwritesPaymentSdkConfiguration() throws Exception {
|
HuifuPaymentProperties properties = new HuifuPaymentProperties();
|
properties.setSystemId("AUDIT-SYSTEM");
|
properties.setProductId("AUDIT-PAYMENT-PRODUCT");
|
properties.setWithdrawalProductId("AUDIT-WITHDRAWAL-PRODUCT");
|
HuifuMiniProgramPaymentGateway payment = new HuifuMiniProgramPaymentGateway(properties);
|
HuifuWithdrawalGateway withdrawal = new HuifuWithdrawalGateway(properties);
|
var paymentInit = HuifuMiniProgramPaymentGateway.class.getDeclaredMethod("initializeSdk");
|
var withdrawalInit = HuifuWithdrawalGateway.class.getDeclaredMethod("initializeSdk");
|
paymentInit.setAccessible(true);
|
withdrawalInit.setAccessible(true);
|
paymentInit.invoke(payment);
|
require("AUDIT-PAYMENT-PRODUCT".equals(com.huifu.bspay.sdk.opps.core.BasePay.getConfig(null).getProductId()), "payment SDK initialized");
|
withdrawalInit.invoke(withdrawal);
|
paymentInit.invoke(payment);
|
require("AUDIT-WITHDRAWAL-PRODUCT".equals(com.huifu.bspay.sdk.opps.core.BasePay.getConfig(null).getProductId()), "payment initialization skips restoring overwritten config");
|
System.out.println("REPRODUCED: payment -> withdrawal -> payment initialization retains WITHDRAWAL product in real SDK default config.");
|
}
|
private void require(boolean value, String name) {
|
if (!value) throw new AssertionError("Observed behavior changed or probe failed: " + name);
|
}
|
}
|