|
|
@@ -3,6 +3,10 @@ package com.ruoyi.app.flashdelivery.service;
|
|
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressChangeRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressConfirmRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryTipAddRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteView;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryCreateRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryProofRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryPricingRequest;
|
|
|
@@ -1051,6 +1055,303 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
return image;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void missingOrInvalidBodyOrderIdIsRejectedBeforeDatabaseAccess() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ for (Long orderId : java.util.Arrays.asList(null, 0L, -1L)) {
|
|
|
+ ServiceException addressFailure = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, orderId, new FlashDeliveryAddressConfirmRequest()));
|
|
|
+ ServiceException tipFailure = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, orderId, tipRequest(20L, 2)));
|
|
|
+ assertEquals("flash.delivery.order.not.found", addressFailure.getMessage());
|
|
|
+ assertEquals("flash.delivery.order.not.found", tipFailure.getMessage());
|
|
|
+ }
|
|
|
+ org.mockito.Mockito.verifyNoInteractions(fixture.orderMapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressQuoteUsesOrderPricingSnapshotAndDoesNotWrite() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ // 已到预约时间也允许待接单订单改地址,不重新验证创建时的预约窗口。
|
|
|
+ order.setDeliveryMode("SCHEDULED");
|
|
|
+ order.setScheduledPickupStartAt(new Date(1));
|
|
|
+ order.setScheduledPickupEndAt(new Date(1800001));
|
|
|
+ FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, addressChange());
|
|
|
+ assertEquals(4500, quote.getDistanceMeters());
|
|
|
+ assertEquals(900, quote.getEstimatedDurationSeconds());
|
|
|
+ assertEquals(23L, quote.getDistanceFee());
|
|
|
+ assertEquals(113L, quote.getBaseDeliveryFee());
|
|
|
+ assertEquals(10L, quote.getTipAmount());
|
|
|
+ assertEquals(123L, quote.getAmount());
|
|
|
+ assertEquals(2, quote.getOrderVersion());
|
|
|
+ assertEquals(1, quote.getPricingVersion());
|
|
|
+ assertEquals("pickup road", order.getPickupAddress());
|
|
|
+ verify(fixture.pricingMapper, never()).selectAtTime(anyString());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressSaveUpdatesBothEndsRouteFeesAndReceiverTogether() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
|
|
|
+ .thenReturn(List.of(88L));
|
|
|
+ when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
|
|
|
+ input.getPickup().setName(" new sender ");
|
|
|
+ input.getPickup().setAddressDetail(null);
|
|
|
+ input.getDelivery().setName("new receiver");
|
|
|
+ var view = fixture.service.updateAddress(7L, 10L, input);
|
|
|
+ assertEquals("new sender", view.getPickup().getName());
|
|
|
+ assertEquals("new receiver", view.getDelivery().getName());
|
|
|
+ assertNull(view.getPickup().getAddressDetail());
|
|
|
+ assertEquals(4500, view.getDistanceMeters());
|
|
|
+ assertEquals(123L, view.getAmount());
|
|
|
+ assertEquals(3, view.getOrderVersion());
|
|
|
+ assertEquals(88L, order.getReceiverUserId());
|
|
|
+ assertEquals(10L, order.getTipAmount());
|
|
|
+ assertEquals("WAITING_ACCEPTANCE", order.getStatus());
|
|
|
+ verify(fixture.orderMapper).updateWaitingAddress(order, 2);
|
|
|
+ verify(fixture.logMapper).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressChangeClearsFormerReceiverWhenNewPhoneDoesNotMatch() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setReceiverUserId(88L);
|
|
|
+ when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
|
|
|
+ fixture.service.updateAddress(7L, 10L, confirmAddress(fixture));
|
|
|
+ assertNull(order.getReceiverUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressQuotePreservesUrgentRulesAndExistingTip() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setDeliveryType("URGENT");
|
|
|
+ var quote = fixture.service.quoteAddress(7L, 10L, addressChange());
|
|
|
+ assertEquals(23L, quote.getUrgentFee());
|
|
|
+ assertEquals(146L, quote.getAmount());
|
|
|
+ assertEquals(10L, quote.getTipAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void changedRouteOrUnconfirmedPriceReturnsLatestQuoteWithoutSaving() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
|
|
|
+ input.setQuotedAmount(1L);
|
|
|
+ var exception = assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ assertEquals(123L, exception.getLatestQuote().getAmount());
|
|
|
+ input.setQuotedAmount(123L);
|
|
|
+ input.setQuotedDistanceMeters(4499);
|
|
|
+ assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ input.setQuotedDistanceMeters(4500);
|
|
|
+ input.setQuotedUrgentFee(null);
|
|
|
+ assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void receiverAndOtherUsersCannotQuoteEditOrAddTip() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setReceiverUserId(88L);
|
|
|
+ for (Long user : List.of(88L, 99L)) {
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(user, 10L, addressChange()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.updateAddress(user, 10L, new FlashDeliveryAddressConfirmRequest()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(user, 10L, tipRequest(5L, 2)));
|
|
|
+ }
|
|
|
+ verify(fixture.routeService, never()).calculate(any(), any());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void everyNonWaitingStateAndAssignedRiderRejectsBothChanges() {
|
|
|
+ for (String status : List.of("ACCEPTED", "PICKED_UP", "DELIVERED", "COMPLETED", "CANCELLED")) {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setStatus(status);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, new FlashDeliveryAddressConfirmRequest()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
|
|
|
+ }
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture).setRiderId(8L);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void staleOrMissingVersionCannotOverwriteNewerOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressChangeRequest input = addressChange();
|
|
|
+ input.setOrderVersion(1);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
|
|
|
+ for (Integer version : java.util.Arrays.asList(null, -1, 1)) {
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, tipRequest(5L, version)));
|
|
|
+ }
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressValidationRejectsSameInvalidAndOverDistanceEndpoints() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressChangeRequest input = addressChange();
|
|
|
+ input.setDelivery(input.getPickup());
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
|
|
|
+ input.setDelivery(request().getDelivery());
|
|
|
+ input.getPickup().setPhone("");
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(40001, 1000, "GOOGLE"));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderAcceptanceBetweenReadAndAddressSaveRejectsWriteWithoutLog() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(0);
|
|
|
+ ServiceException exception = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ assertEquals("flash.delivery.state.changed", exception.getMessage());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addingTipOnlyIncreasesTipAndTotalAndRejectsSameVersionRetry() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ var view = fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
|
|
|
+ assertEquals(30L, view.getTipAmount());
|
|
|
+ assertEquals(132L, view.getAmount());
|
|
|
+ assertEquals(102L, view.getBaseDeliveryFee());
|
|
|
+ assertEquals(3200, view.getDistanceMeters());
|
|
|
+ assertEquals(3, view.getOrderVersion());
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
|
|
|
+ assertEquals(30L, order.getTipAmount());
|
|
|
+ verify(fixture.orderMapper).updateWaitingTip(any(), eq(2));
|
|
|
+ verify(fixture.routeService, never()).calculate(any(), any());
|
|
|
+ verify(fixture.pricingMapper, never()).selectAtTime(anyString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void tipChangeInvalidatesEarlierAddressConfirmation() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressConfirmRequest confirmation = confirmAddress(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, confirmation));
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void additionalTipRejectsFractionalJsonBeforeWriting() throws Exception {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
|
|
|
+ "{\"orderVersion\":2,\"additionalTipAmount\":1.9}", FlashDeliveryTipAddRequest.class);
|
|
|
+ ServiceException exception = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, input));
|
|
|
+ assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void additionalTipRejectsJsonAmountBeyondLongRange() throws Exception {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
|
|
|
+ "{\"orderVersion\":2,\"additionalTipAmount\":1e100}", FlashDeliveryTipAddRequest.class);
|
|
|
+ ServiceException exception = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, input));
|
|
|
+ assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void additionalTipRejectsZeroNegativeNullAndOverflow() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ for (Long value : java.util.Arrays.asList(null, 0L, -1L, Long.MAX_VALUE)) {
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, tipRequest(value, 2)));
|
|
|
+ }
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderAcceptanceBetweenReadAndTipSaveRejectsWriteWithoutLog() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(0);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryOrder editableOrder(Fixture fixture) {
|
|
|
+ FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ order.setRiderId(null);
|
|
|
+ order.setVersion(2);
|
|
|
+ order.setPricingId(1L);
|
|
|
+ order.setPricingVersion(1);
|
|
|
+ order.setPricingStartTime("00:00");
|
|
|
+ order.setPricingEndTime("24:00");
|
|
|
+ order.setStartingDistance(new BigDecimal("3.00"));
|
|
|
+ order.setStartingFare(90L);
|
|
|
+ order.setDistance(new BigDecimal("1.00"));
|
|
|
+ order.setFreight(15L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryAddressChangeRequest addressChange() {
|
|
|
+ FlashDeliveryAddressChangeRequest input = new FlashDeliveryAddressChangeRequest();
|
|
|
+ input.setOrderVersion(2);
|
|
|
+ input.setPickup(request().getPickup());
|
|
|
+ input.setDelivery(request().getDelivery());
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryAddressConfirmRequest confirmAddress(Fixture fixture) {
|
|
|
+ FlashDeliveryAddressChangeRequest change = addressChange();
|
|
|
+ FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, change);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = new FlashDeliveryAddressConfirmRequest();
|
|
|
+ input.setOrderVersion(quote.getOrderVersion());
|
|
|
+ input.setPickup(change.getPickup());
|
|
|
+ input.setDelivery(change.getDelivery());
|
|
|
+ input.setQuotedDistanceMeters(quote.getDistanceMeters());
|
|
|
+ input.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee());
|
|
|
+ input.setQuotedDistanceFee(quote.getDistanceFee());
|
|
|
+ input.setQuotedUrgentFee(quote.getUrgentFee());
|
|
|
+ input.setQuotedAmount(quote.getAmount());
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryTipAddRequest tipRequest(Long amount, Integer version) {
|
|
|
+ FlashDeliveryTipAddRequest input = new FlashDeliveryTipAddRequest();
|
|
|
+ input.setAdditionalTipAmount(amount == null ? null : BigDecimal.valueOf(amount));
|
|
|
+ input.setOrderVersion(version);
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+
|
|
|
private static class Fixture {
|
|
|
final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
|
|
|
final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
|