package com.tourguide.modules.finance.service; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.mapper.TourOrderMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class GuideBalanceSettlementServiceTest { private TourOrderMapper orderMapper; private GuideWithdrawableBalanceService balanceService; private GuideBalanceSettlementService service; @BeforeEach void setUp() { orderMapper = mock(TourOrderMapper.class); balanceService = mock(GuideWithdrawableBalanceService.class); service = new GuideBalanceSettlementService(orderMapper, balanceService); } @Test void shouldCreditDueOrderAndMarkSettled() { LocalDateTime now = LocalDateTime.of(2026, 8, 17, 12, 0); TourOrder order = dueOrder(now.minusMinutes(1)); when(orderMapper.selectForUpdate(10L)).thenReturn(order); when(orderMapper.updateById(order)).thenReturn(1); assertThat(service.settle(10L, now)).isTrue(); verify(balanceService).credit(8L, new BigDecimal("90.00")); verify(orderMapper).updateById(order); assertThat(order.getSettledAt()).isEqualTo(now); } @Test void shouldNotCreditOrderTwice() { LocalDateTime now = LocalDateTime.of(2026, 8, 17, 12, 0); TourOrder order = dueOrder(now.minusMinutes(1)); order.setSettledAt(now.minusSeconds(1)); when(orderMapper.selectForUpdate(10L)).thenReturn(order); assertThat(service.settle(10L, now)).isFalse(); verify(balanceService, never()).credit(8L, new BigDecimal("90.00")); verify(orderMapper, never()).updateById(order); } @Test void shouldCreditTeamBalanceForTeamOrder() { LocalDateTime now = LocalDateTime.of(2026, 9, 8, 12, 0); TourOrder order = dueOrder(now.minusMinutes(1)); order.setCommissionTeamId(6L); when(orderMapper.selectForUpdate(10L)).thenReturn(order); when(orderMapper.updateById(order)).thenReturn(1); assertThat(service.settle(10L, now)).isTrue(); verify(balanceService).creditTeam(6L, new BigDecimal("90.00")); verify(balanceService, never()).credit(8L, new BigDecimal("90.00")); } @Test void shouldSettleRemainingEarningForPartialRefund() { LocalDateTime now = LocalDateTime.of(2026, 9, 8, 12, 0); TourOrder order = dueOrder(now.minusMinutes(1)); order.setAmount(new BigDecimal("100.00")); order.setPaymentAmount(new BigDecimal("100.00")); order.setRefundStatus("PARTIAL"); order.setRefundedAmount(new BigDecimal("50.00")); when(orderMapper.selectForUpdate(10L)).thenReturn(order); when(orderMapper.updateById(order)).thenReturn(1); assertThat(service.settle(10L, now)).isTrue(); verify(balanceService).credit(8L, new BigDecimal("45.00")); assertThat(order.getSettledAt()).isEqualTo(now); } @Test void shouldReverseSettledGuideEarningForRefund() { TourOrder order = dueOrder(LocalDateTime.now()); order.setSettledAt(LocalDateTime.now()); order.setAmount(new BigDecimal("100.00")); order.setPaymentAmount(new BigDecimal("100.00")); when(balanceService.debitForRefund(8L, new BigDecimal("90.00"))).thenReturn(true); service.reverseRefund(order, BigDecimal.ZERO, new BigDecimal("100.00")); verify(balanceService).debitForRefund(8L, new BigDecimal("90.00")); } @Test void shouldReverseOnlyIncrementalAmountForPartialRefund() { TourOrder order = dueOrder(LocalDateTime.now()); order.setSettledAt(LocalDateTime.now()); order.setAmount(new BigDecimal("100.00")); order.setPaymentAmount(new BigDecimal("100.00")); when(balanceService.debitForRefund(8L, new BigDecimal("27.00"))).thenReturn(true); service.reverseRefund(order, new BigDecimal("20.00"), new BigDecimal("50.00")); verify(balanceService).debitForRefund(8L, new BigDecimal("27.00")); } @Test void shouldReverseSettledTeamEarningForRefund() { TourOrder order = dueOrder(LocalDateTime.now()); order.setSettledAt(LocalDateTime.now()); order.setCommissionTeamId(6L); order.setAmount(new BigDecimal("100.00")); order.setPaymentAmount(new BigDecimal("100.00")); when(balanceService.debitTeamForRefund(6L, new BigDecimal("90.00"))).thenReturn(true); service.reverseRefund(order, BigDecimal.ZERO, new BigDecimal("100.00")); verify(balanceService).debitTeamForRefund(6L, new BigDecimal("90.00")); } @Test void shouldNotSettleOrderWithRefundInProgress() { LocalDateTime now = LocalDateTime.of(2026, 8, 17, 12, 0); TourOrder order = dueOrder(now.minusMinutes(1)); order.setRefundStatus("REFUNDING"); when(orderMapper.selectForUpdate(10L)).thenReturn(order); assertThat(service.settle(10L, now)).isFalse(); verify(balanceService, never()).credit(8L, new BigDecimal("90.00")); verify(orderMapper, never()).updateById(order); } @Test void shouldFailRefundWhenBalanceAdjustmentFails() { TourOrder order = dueOrder(LocalDateTime.now()); order.setSettledAt(LocalDateTime.now()); order.setAmount(new BigDecimal("100.00")); order.setPaymentAmount(new BigDecimal("100.00")); assertThatThrownBy(() -> service.reverseRefund(order, BigDecimal.ZERO, new BigDecimal("100.00"))) .isInstanceOf(BusinessException.class) .hasMessage("退款后导游收益冲正失败"); } private TourOrder dueOrder(LocalDateTime dueAt) { TourOrder order = new TourOrder(); order.setId(10L); order.setGuideId(8L); order.setStatus("COMPLETED"); order.setRefundStatus("NONE"); order.setGuideEarningAmount(new BigDecimal("90.00")); order.setSettlementDueAt(dueAt); return order; } }