package com.tourguide.modules.order.service; import com.alibaba.fastjson.JSONObject; import com.tourguide.common.huifu.properties.HuiFuProperties; import com.tourguide.modules.finance.service.GuideBalanceSettlementService; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.domain.RefundStatus; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.entity.TourOrderRefund; import com.tourguide.modules.order.mapper.TourOrderMapper; import com.tourguide.modules.order.mapper.TourOrderRefundMapper; 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.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class HuifuRefundCallbackServiceTest { private TourOrderMapper orderMapper; private HuifuRefundCallbackService callbackService; private GuideSessionTransitionService sessionTransitionService; private GuideBalanceSettlementService balanceSettlementService; @BeforeEach void setUp() { HuiFuProperties properties = new HuiFuProperties(); properties.setHuifuId("merchant-1"); orderMapper = mock(TourOrderMapper.class); sessionTransitionService = mock(GuideSessionTransitionService.class); balanceSettlementService = mock(GuideBalanceSettlementService.class); callbackService = new HuifuRefundCallbackService(properties, orderMapper, null, balanceSettlementService, sessionTransitionService); } @Test void successCallbackShouldCompleteRefundOnce() { TourOrder order = refundingOrder(); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); boolean first = callbackService.handle(successBody()); boolean duplicate = callbackService.handle(successBody()); assertThat(first).isTrue(); assertThat(duplicate).isFalse(); assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.REFUNDED.getCode()); assertThat(order.getStatus()).isEqualTo(OrderStatus.REFUNDED.getCode()); assertThat(order.getRefundTime()).isNotNull(); verify(orderMapper).updateById(order); } @Test void failedCallbackShouldRecordFailureWithoutCompletingRefund() { TourOrder order = refundingOrder(); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); JSONObject body = successBody(); body.put("refund_stat", "F"); boolean completed = callbackService.handle(body); assertThat(completed).isFalse(); assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.REFUND_FAILED.getCode()); verify(orderMapper).updateById(order); } @Test void duplicateSuccessCallbackShouldNotUpdateAgain() { TourOrder order = refundingOrder(); order.setRefundStatus(RefundStatus.REFUNDED.getCode()); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); boolean completed = callbackService.handle(successBody()); assertThat(completed).isFalse(); verify(orderMapper, never()).updateById(order); } @Test void nestedSuccessCallbackShouldCompleteOrder() { TourOrder order = refundingOrder(); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); JSONObject data = successBody(); JSONObject body = new JSONObject(); body.put("data", data); assertThat(callbackService.handle(body)).isTrue(); assertThat(order.getStatus()).isEqualTo(OrderStatus.REFUNDED.getCode()); } @Test void partialSuccessCallbackShouldKeepRefundingBalance() { TourOrder order = refundingOrder(); order.setRefundAmount(new BigDecimal("20.00")); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); JSONObject body = successBody(); body.put("ord_amt", "20.00"); assertThat(callbackService.handle(body)).isTrue(); assertThat(order.getRefundedAmount()).isEqualByComparingTo("20.00"); assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.PARTIAL.getCode()); assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED.getCode()); } @Test void successCallbackShouldReverseSettledGuideEarning() { TourOrder order = refundingOrder(); order.setGuideId(8L); order.setAmount(new BigDecimal("100.00")); order.setPaymentAmount(new BigDecimal("100.00")); order.setGuideEarningAmount(new BigDecimal("90.00")); order.setSettledAt(LocalDateTime.of(2026, 8, 2, 12, 0)); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); JSONObject body = successBody(); body.put("ord_amt", "36.00"); assertThat(callbackService.handle(body)).isTrue(); verify(balanceSettlementService).reverseRefund(order, BigDecimal.ZERO, new BigDecimal("36.00")); } @Test void pendingMeetingSuccessCallbackShouldRestoreGuideSession() { TourOrder order = refundingOrder(); order.setStatus(OrderStatus.PENDING_MEETING.getCode()); order.setGuideId(8L); order.setReceivingSessionId(18L); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); assertThat(callbackService.handle(successBody())).isTrue(); verify(sessionTransitionService).restoreAfterOrderCancellation(order); } @Test void followUpSuccessCallbackShouldCompleteRemainingRefund() { TourOrder order = refundingOrder(); order.setRefundAmount(new BigDecimal("16.00")); order.setRefundedAmount(new BigDecimal("20.00")); when(orderMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(order); JSONObject body = successBody(); body.put("ord_amt", "16.00"); assertThat(callbackService.handle(body)).isTrue(); assertThat(order.getRefundedAmount()).isEqualByComparingTo("36.00"); assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.REFUNDED.getCode()); assertThat(order.getStatus()).isEqualTo(OrderStatus.REFUNDED.getCode()); } @Test void partialSuccessShouldCompleteOnlyTheMatchedRefundRecord() { TourOrderRefundMapper refundMapper = mock(TourOrderRefundMapper.class); callbackService = new HuifuRefundCallbackService(properties(), orderMapper, refundMapper); TourOrder order = refundingOrder(); order.setId(99L); TourOrderRefund record = new TourOrderRefund(); record.setOrderId(99L); record.setRefundOrderNo("RF001"); record.setRefundAmount(new BigDecimal("20.00")); record.setRefundStatus(RefundStatus.REFUNDING.getCode()); when(refundMapper.selectByRefundOrderNoForUpdate("RF001")).thenReturn(record); when(orderMapper.selectForUpdate(99L)).thenReturn(order); JSONObject body = successBody(); body.put("ord_amt", "20.00"); assertThat(callbackService.handle(body)).isTrue(); assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.PARTIAL.getCode()); assertThat(record.getRefundStatus()).isEqualTo(RefundStatus.REFUNDED.getCode()); verify(refundMapper).updateById(record); } private HuiFuProperties properties() { HuiFuProperties properties = new HuiFuProperties(); properties.setHuifuId("merchant-1"); return properties; } private TourOrder refundingOrder() { TourOrder order = new TourOrder(); order.setRefundOrderNo("RF001"); order.setRefundStatus(RefundStatus.REFUNDING.getCode()); order.setRefundAmount(new BigDecimal("36.00")); order.setPaymentAmount(new BigDecimal("36.00")); return order; } private JSONObject successBody() { JSONObject body = new JSONObject(); body.put("req_seq_id", "RF001"); body.put("huifu_id", "merchant-1"); body.put("refund_stat", "S"); body.put("ord_amt", "36.00"); body.put("end_time", "20260803180000"); return body; } }