package com.tourguide.modules.order.service;
|
|
import com.tourguide.common.exception.BusinessException;
|
import com.tourguide.modules.order.domain.OrderStatus;
|
import com.tourguide.modules.order.entity.TourOrder;
|
import com.tourguide.modules.order.entity.TourOrderPaymentAttempt;
|
import com.tourguide.modules.order.mapper.TourOrderMapper;
|
import com.tourguide.modules.order.mapper.TourOrderPaymentAttemptMapper;
|
import com.tourguide.modules.order.payment.HuifuPaymentNotice;
|
import com.tourguide.modules.order.payment.OrderPaymentPreparation;
|
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.never;
|
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
class TourOrderPaymentServiceTest {
|
private TourOrderMapper orderMapper;
|
private TourOrderPaymentAttemptMapper paymentAttemptMapper;
|
private GuideOrderMatchingService matchingService;
|
private TourOrderPaymentService paymentService;
|
|
@BeforeEach
|
void setUp() {
|
orderMapper = mock(TourOrderMapper.class);
|
paymentAttemptMapper = mock(TourOrderPaymentAttemptMapper.class);
|
matchingService = mock(GuideOrderMatchingService.class);
|
paymentService = new TourOrderPaymentService(orderMapper, paymentAttemptMapper, matchingService);
|
}
|
|
@Test
|
void repeatedPaymentShouldCreateNewRequestSequenceAndSyncOrder() {
|
TourOrder order = pendingOrder();
|
when(orderMapper.selectForUpdate(99L)).thenReturn(order);
|
|
OrderPaymentPreparation first = paymentService.prepare(7L, 99L);
|
OrderPaymentPreparation second = paymentService.prepare(7L, 99L);
|
|
assertThat(first.getReqSeqId()).startsWith("PY").hasSize(32);
|
assertThat(second.getReqSeqId()).startsWith("PY").hasSize(32);
|
assertThat(second.getReqSeqId()).isNotEqualTo(first.getReqSeqId());
|
assertThat(order.getPaymentOrderNo()).isEqualTo(second.getReqSeqId());
|
assertThat(order.getPaymentMethod()).isEqualTo("WECHAT");
|
|
ArgumentCaptor<TourOrderPaymentAttempt> captor = ArgumentCaptor.forClass(TourOrderPaymentAttempt.class);
|
verify(paymentAttemptMapper, times(2)).insert(captor.capture());
|
assertThat(captor.getAllValues()).extracting(TourOrderPaymentAttempt::getReqSeqId)
|
.containsExactly(first.getReqSeqId(), second.getReqSeqId());
|
assertThat(captor.getAllValues()).allSatisfy(attempt -> {
|
assertThat(attempt.getOrderId()).isEqualTo(99L);
|
assertThat(attempt.getAmount()).isEqualByComparingTo("36.00");
|
assertThat(attempt.getStatus()).isEqualTo("CREATED");
|
});
|
verify(orderMapper, times(2)).updateById(order);
|
}
|
|
@Test
|
void successCallbackShouldConfirmAttemptOnceAndStartMatching() {
|
TourOrder order = pendingOrder();
|
order.setPaymentOrderNo("PY-newer");
|
TourOrderPaymentAttempt attempt = attempt("PY-paid", "CREATED");
|
when(paymentAttemptMapper.selectByReqSeqIdForUpdate("PY-paid")).thenReturn(attempt);
|
when(orderMapper.selectForUpdate(99L)).thenReturn(order);
|
HuifuPaymentNotice notice = notice("PY-paid", new BigDecimal("36.00"));
|
|
boolean confirmed = paymentService.confirm(notice);
|
|
assertThat(confirmed).isTrue();
|
assertThat(attempt.getStatus()).isEqualTo("SUCCEEDED");
|
assertThat(attempt.getProviderTransactionId()).isEqualTo("hf-seq-1");
|
assertThat(order.getPaymentOrderNo()).isEqualTo("PY-paid");
|
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING_ACCEPT.getCode());
|
assertThat(order.getPaymentAmount()).isEqualByComparingTo("36.00");
|
assertThat(order.getPaymentTime()).isEqualTo(notice.getPaidAt());
|
verify(paymentAttemptMapper).updateById(attempt);
|
verify(orderMapper).updateById(order);
|
verify(matchingService).match(99L);
|
}
|
|
@Test
|
void duplicateCallbackShouldNotMatchAgain() {
|
TourOrder order = pendingOrder();
|
order.setPaymentTime(LocalDateTime.now());
|
order.setStatus(OrderStatus.PENDING_ACCEPT.getCode());
|
TourOrderPaymentAttempt attempt = attempt("PY-paid", "SUCCEEDED");
|
when(paymentAttemptMapper.selectByReqSeqIdForUpdate("PY-paid")).thenReturn(attempt);
|
when(orderMapper.selectForUpdate(99L)).thenReturn(order);
|
|
boolean confirmed = paymentService.confirm(notice("PY-paid", new BigDecimal("36.00")));
|
|
assertThat(confirmed).isFalse();
|
verify(paymentAttemptMapper, never()).updateById(attempt);
|
verify(orderMapper, never()).updateById(order);
|
verify(matchingService, never()).match(99L);
|
}
|
|
@Test
|
void laterSuccessfulAttemptShouldBeRecordedWithoutDuplicateMatching() {
|
TourOrder order = pendingOrder();
|
order.setPaymentTime(LocalDateTime.now());
|
order.setStatus(OrderStatus.PENDING_ACCEPT.getCode());
|
TourOrderPaymentAttempt attempt = attempt("PY-late", "CREATED");
|
when(paymentAttemptMapper.selectByReqSeqIdForUpdate("PY-late")).thenReturn(attempt);
|
when(orderMapper.selectForUpdate(99L)).thenReturn(order);
|
|
boolean confirmed = paymentService.confirm(notice("PY-late", new BigDecimal("36.00")));
|
|
assertThat(confirmed).isFalse();
|
assertThat(attempt.getStatus()).isEqualTo("SUCCEEDED");
|
verify(paymentAttemptMapper).updateById(attempt);
|
verify(orderMapper, never()).updateById(order);
|
verify(matchingService, never()).match(99L);
|
}
|
|
@Test
|
void callbackShouldRejectMismatchedAmount() {
|
TourOrder order = pendingOrder();
|
TourOrderPaymentAttempt attempt = attempt("PY-paid", "CREATED");
|
when(paymentAttemptMapper.selectByReqSeqIdForUpdate("PY-paid")).thenReturn(attempt);
|
when(orderMapper.selectForUpdate(99L)).thenReturn(order);
|
|
assertThatThrownBy(() -> paymentService.confirm(notice("PY-paid", new BigDecimal("35.99"))))
|
.isInstanceOf(BusinessException.class)
|
.extracting("code")
|
.isEqualTo(409);
|
|
verify(paymentAttemptMapper, never()).updateById(attempt);
|
verify(orderMapper, never()).updateById(order);
|
verify(matchingService, never()).match(99L);
|
}
|
|
private TourOrder pendingOrder() {
|
TourOrder order = new TourOrder();
|
order.setId(99L);
|
order.setOrderNo("MJ202608030001");
|
order.setVisitorId(7L);
|
order.setScenicName("Qingcheng Mountain");
|
order.setPackageName("Group tour");
|
order.setAmount(new BigDecimal("36.00"));
|
order.setStatus(OrderStatus.PENDING_PAYMENT.getCode());
|
return order;
|
}
|
|
private TourOrderPaymentAttempt attempt(String reqSeqId, String status) {
|
TourOrderPaymentAttempt attempt = new TourOrderPaymentAttempt();
|
attempt.setId(1L);
|
attempt.setOrderId(99L);
|
attempt.setReqSeqId(reqSeqId);
|
attempt.setReqDate("20260803");
|
attempt.setAmount(new BigDecimal("36.00"));
|
attempt.setStatus(status);
|
return attempt;
|
}
|
|
private HuifuPaymentNotice notice(String reqSeqId, BigDecimal amount) {
|
return new HuifuPaymentNotice(reqSeqId, "merchant-1", "S", amount,
|
"hf-seq-1", LocalDateTime.of(2026, 8, 3, 16, 0));
|
}
|
}
|