| | |
| | | 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), |
| | | mock(GuideNotificationService.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")); |
| | | |
| | | assertThat(order.getRefundAmount()).isEqualByComparingTo("100.00"); |
| | | assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.REFUNDED.getCode()); |
| | | assertThat(order.getStatus()).isEqualTo(OrderStatus.REFUNDED.getCode()); |
| | | verify(orderMapper).updateById(order); |
| | | verify(refundService).requestRefund(order, new BigDecimal("100.00")); |
| | | } |
| | | |
| | | @Test |
| | |
| | | .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<LambdaQueryWrapper<TourOrder>> 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<LambdaQueryWrapper<OrderReview>> 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.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; |
| | | } |