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 GuideSessionTransitionService sessionTransitionService;
|
private static boolean tableInfoInitialized;
|
private static boolean reviewTableInfoInitialized;
|
|
@BeforeEach
|
void setUp() {
|
orderMapper = mock(TourOrderMapper.class);
|
refundService = mock(OrderRefundService.class);
|
notificationService = mock(GuideNotificationService.class);
|
sessionTransitionService = mock(GuideSessionTransitionService.class);
|
orderService = new TourOrderService(orderMapper, mock(OrderReviewMapper.class),
|
notificationService, refundService, null, sessionTransitionService);
|
}
|
|
@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);
|
order.setReceivingSessionId(18L);
|
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(sessionTransitionService).restoreAfterOrderCancellation(order);
|
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,
|
null, sessionTransitionService);
|
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.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;
|
}
|
}
|