package com.tourguide.modules.finance.service; import com.alibaba.fastjson.JSONObject; import com.tourguide.modules.finance.dto.GuideWithdrawalCreateRequest; import com.tourguide.modules.finance.dto.WithdrawalResponse; import com.tourguide.modules.finance.entity.GuideWithdrawal; import com.tourguide.modules.finance.entity.GuideWithdrawalAccount; import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.service.GuideUserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.never; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class GuideWithdrawalServiceTest { private GuideWithdrawalMapper withdrawalMapper; private GuideSettlementService settlementService; private GuideUserService guideUserService; private GuideWithdrawableBalanceService balanceService; private GuideWithdrawalStateService stateService; private GuideWithdrawalPayoutService payoutService; private GuideWithdrawalService service; @BeforeEach void setUp() { withdrawalMapper = mock(GuideWithdrawalMapper.class); settlementService = mock(GuideSettlementService.class); guideUserService = mock(GuideUserService.class); balanceService = mock(GuideWithdrawableBalanceService.class); stateService = mock(GuideWithdrawalStateService.class); payoutService = mock(GuideWithdrawalPayoutService.class); service = new GuideWithdrawalService(withdrawalMapper, settlementService, guideUserService, balanceService, stateService, payoutService); } @Test void createShouldReserveAvailableBalanceAndPersistCardSnapshot() throws Exception { GuideUser guide = new GuideUser(); guide.setRealName("张三"); guide.setMobile("13800138000"); GuideWithdrawalAccount account = new GuideWithdrawalAccount(); account.setId(10); account.setHuifuId("HF100"); account.setBankName("招商银行"); account.setBankCardCode("6225888888885678"); when(guideUserService.findEnabledById(8L)).thenReturn(guide); when(settlementService.requireApprovedCardForUpdate(8L, 10)).thenReturn(account); when(balanceService.debit(8L, new BigDecimal("200.00"))).thenReturn(true); doAnswer(invocation -> { GuideWithdrawal withdrawal = invocation.getArgument(0); withdrawal.setId(20L); return 1; }).when(withdrawalMapper).insert(any(GuideWithdrawal.class)); GuideWithdrawalCreateRequest request = new GuideWithdrawalCreateRequest(); request.setAmount(new BigDecimal("200.00")); request.setBankCardId(10); WithdrawalResponse response = service.create(8L, request); assertThat(response.getAmount()).isEqualByComparingTo("200.00"); assertThat(response.getBankName()).isEqualTo("招商银行"); assertThat(response.getCardMasked()).isEqualTo("****5678"); assertThat(response.getStatus()).isEqualTo("PENDING"); verify(balanceService).debit(8L, new BigDecimal("200.00")); } @Test void rejectShouldReturnReservedBalance() { GuideWithdrawal withdrawal = new GuideWithdrawal(); withdrawal.setId(20L); withdrawal.setGuideId(8L); withdrawal.setAmount(new BigDecimal("200.00")); withdrawal.setStatus("REJECTED"); when(stateService.reject(20L, "资料不完整", "admin")) .thenReturn(new WithdrawalResponse(withdrawal)); WithdrawalResponse response = service.process(20L, "REJECTED", "资料不完整", "admin"); assertThat(response.getStatus()).isEqualTo("REJECTED"); verify(stateService).reject(20L, "资料不完整", "admin"); } @Test void failedCashCallbackShouldReturnToTransferredWithoutRefundingLocalBalance() { GuideWithdrawal withdrawal = new GuideWithdrawal(); withdrawal.setId(20L); withdrawal.setGuideId(8L); withdrawal.setAmount(new BigDecimal("200.00")); withdrawal.setCashReqSeqId("CW001"); withdrawal.setCashReqDate("20260817"); withdrawal.setStatus("PROCESSING"); GuideWithdrawalAccount account = new GuideWithdrawalAccount(); account.setHuifuId("HF100"); when(withdrawalMapper.selectByPayoutSequenceForUpdate("CW001")).thenReturn(withdrawal); when(settlementService.findCurrent(8L)).thenReturn(account); when(withdrawalMapper.resetCashForRetry(any(), any(), any(), any())).thenReturn(1); JSONObject body = new JSONObject(); body.put("req_seq_id", "CW001"); body.put("huifu_id", "HF100"); body.put("cash_amt", "200.00"); body.put("trans_stat", "F"); body.put("resp_desc", "银行处理中断"); assertThat(service.handleCallback(body)).isTrue(); assertThat(withdrawal.getStatus()).isEqualTo("TRANSFERRED"); assertThat(withdrawal.getCashReqSeqId()).isNull(); verify(balanceService, org.mockito.Mockito.never()).credit(8L, new BigDecimal("200.00")); } @Test void nonTerminalCashCallbackShouldRemainProcessing() { GuideWithdrawal withdrawal = processingWithdrawal(); GuideWithdrawalAccount account = new GuideWithdrawalAccount(); account.setHuifuId("HF100"); when(withdrawalMapper.selectByPayoutSequenceForUpdate("CW001")).thenReturn(withdrawal); when(settlementService.findCurrent(8L)).thenReturn(account); when(withdrawalMapper.updateById(withdrawal)).thenReturn(1); JSONObject body = new JSONObject(); body.put("req_seq_id", "CW001"); body.put("huifu_id", "HF100"); body.put("cash_amt", "200.00"); body.put("hf_seq_id", "HF-CASH-1"); assertThat(service.handleCallback(body)).isTrue(); assertThat(withdrawal.getStatus()).isEqualTo("PROCESSING"); assertThat(withdrawal.getCashReqSeqId()).isEqualTo("CW001"); assertThat(withdrawal.getCashHfSeqId()).isEqualTo("HF-CASH-1"); verify(withdrawalMapper, never()).resetCashForRetry(any(), any(), any(), any()); } @Test void successCallbackShouldRecoverTransferredRecordWithSameCashSequence() { GuideWithdrawal withdrawal = processingWithdrawal(); withdrawal.setStatus("TRANSFERRED"); GuideWithdrawalAccount account = new GuideWithdrawalAccount(); account.setHuifuId("HF100"); when(withdrawalMapper.selectByPayoutSequenceForUpdate("CW001")).thenReturn(withdrawal); when(settlementService.findCurrent(8L)).thenReturn(account); when(withdrawalMapper.updateById(withdrawal)).thenReturn(1); JSONObject body = new JSONObject(); body.put("req_seq_id", "CW001"); body.put("huifu_id", "HF100"); body.put("cash_amt", "200.00"); body.put("trans_stat", "S"); assertThat(service.handleCallback(body)).isTrue(); assertThat(withdrawal.getStatus()).isEqualTo("APPROVED"); assertThat(withdrawal.getPayoutMessage()).isEqualTo("取现到账成功"); } private GuideWithdrawal processingWithdrawal() { GuideWithdrawal withdrawal = new GuideWithdrawal(); withdrawal.setId(20L); withdrawal.setGuideId(8L); withdrawal.setAmount(new BigDecimal("200.00")); withdrawal.setCashReqSeqId("CW001"); withdrawal.setCashReqDate("20260817"); withdrawal.setStatus("PROCESSING"); return withdrawal; } }