package com.tourguide.modules.order.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.domain.RefundStatus; import com.tourguide.modules.order.dto.TourOrderResponse; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.mapper.TourOrderMapper; import com.tourguide.modules.review.mapper.OrderReviewMapper; import com.tourguide.modules.review.entity.OrderReview; import com.tourguide.modules.system.service.GuideNotificationService; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; 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.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.ArgumentMatchers.any; class TourOrderServiceTest { private TourOrderMapper orderMapper; private TourOrderService orderService; private OrderRefundService refundService; private GuideNotificationService notificationService; private static boolean tableInfoInitialized; private static boolean reviewTableInfoInitialized; @BeforeEach void setUp() { orderMapper = mock(TourOrderMapper.class); refundService = mock(OrderRefundService.class); notificationService = mock(GuideNotificationService.class); orderService = new TourOrderService(orderMapper, mock(OrderReviewMapper.class), notificationService, refundService); } @Test void shouldRefundRequestedAmount() { TourOrder order = refundableOrder(); when(orderMapper.selectForUpdate(1L)).thenReturn(order); when(refundService.requestRefund(order, new BigDecimal("100.00"))).thenReturn(true); orderService.refund(1L, new BigDecimal("100.00")); verify(refundService).requestRefund(order, new BigDecimal("100.00")); } @Test void shouldRejectAmountGreaterThanPayment() { TourOrder order = refundableOrder(); when(orderMapper.selectForUpdate(1L)).thenReturn(order); assertThatThrownBy(() -> orderService.refund(1L, new BigDecimal("156.01"))) .isInstanceOf(BusinessException.class) .hasMessage("退款金额不能超过当前可退金额"); } @Test void shouldAllowFollowUpRefundForPartialOrder() { TourOrder order = refundableOrder(); order.setStatus(OrderStatus.CANCELLED.getCode()); order.setRefundStatus(RefundStatus.PARTIAL.getCode()); order.setRefundedAmount(new BigDecimal("100.00")); when(orderMapper.selectForUpdate(1L)).thenReturn(order); when(refundService.requestRefund(order, new BigDecimal("56.00"))).thenReturn(true); TourOrderResponse response = orderService.refund(1L, new BigDecimal("56.00")); assertThat(response.getRefundableAmount()).isEqualByComparingTo("56.00"); verify(refundService).requestRefund(order, new BigDecimal("56.00")); } @Test void shouldRejectFollowUpRefundAboveRemainingAmount() { TourOrder order = refundableOrder(); order.setStatus(OrderStatus.CANCELLED.getCode()); order.setRefundStatus(RefundStatus.PARTIAL.getCode()); order.setRefundedAmount(new BigDecimal("100.00")); when(orderMapper.selectForUpdate(1L)).thenReturn(order); assertThatThrownBy(() -> orderService.refund(1L, new BigDecimal("56.01"))) .isInstanceOf(BusinessException.class) .hasMessage("退款金额不能超过当前可退金额"); } @Test void shouldReportProviderFailureInsteadOfSuccess() { TourOrder order = refundableOrder(); when(orderMapper.selectForUpdate(1L)).thenReturn(order); when(refundService.requestRefund(order, new BigDecimal("100.00"))).thenReturn(false); assertThatThrownBy(() -> orderService.refund(1L, new BigDecimal("100.00"))) .isInstanceOf(BusinessException.class) .hasMessage("支付渠道退款失败,请稍后重试"); } @Test void notificationFailureShouldNotFailAcceptedRefund() { TourOrder order = refundableOrder(); order.setStatus(OrderStatus.PENDING_MEETING.getCode()); order.setGuideId(42L); when(orderMapper.selectForUpdate(1L)).thenReturn(order); when(refundService.requestRefund(order, new BigDecimal("100.00"))).thenReturn(true); doThrow(new IllegalStateException("notification unavailable")) .when(notificationService).notifyOrderCancelled(order); TourOrderResponse response = orderService.refund(1L, new BigDecimal("100.00")); assertThat(response).isNotNull(); verify(notificationService).notifyOrderCancelled(order); } @Test void shouldFilterOrdersByGuideId() { initializeTableInfo(); when(orderMapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0)); orderService.page(1, 10, null, null, 42L, null, null, null, null); ArgumentCaptor> queryCaptor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); verify(orderMapper).selectPage(any(Page.class), queryCaptor.capture()); queryCaptor.getValue().getSqlSegment(); assertThat(queryCaptor.getValue().getParamNameValuePairs().values()).contains(42L); } @Test void detailShouldOnlyTreatVisibleReviewAsReviewed() { initializeTableInfo(); initializeReviewTableInfo(); OrderReviewMapper reviewMapper = mock(OrderReviewMapper.class); orderService = new TourOrderService(orderMapper, reviewMapper, notificationService, refundService); TourOrder order = new TourOrder(); order.setId(1L); when(orderMapper.selectById(1L)).thenReturn(order); orderService.detail(1L); ArgumentCaptor> queryCaptor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); verify(reviewMapper).selectOne(queryCaptor.capture()); assertThat(queryCaptor.getValue().getSqlSegment()).contains("status"); } private static void initializeTableInfo() { if (!tableInfoInitialized) { TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TourOrder.class); tableInfoInitialized = true; } } private static void initializeReviewTableInfo() { if (!reviewTableInfoInitialized) { TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), OrderReview.class); reviewTableInfoInitialized = true; } } private TourOrder refundableOrder() { TourOrder order = new TourOrder(); order.setId(1L); order.setOrderNo("123465"); order.setStatus(OrderStatus.COMPLETED.getCode()); order.setRefundStatus(RefundStatus.NONE.getCode()); order.setAmount(new BigDecimal("256.10")); order.setPaymentAmount(new BigDecimal("156.00")); order.setPaymentTime(LocalDateTime.now()); order.setPaymentMethod("WECHAT"); return order; } }