fix(firstleg): make sailing copy idempotent

This commit is contained in:
2026-09-27 23:22:56 +08:00
parent 465d26ba42
commit cf76f7d6d3
3 changed files with 127 additions and 2 deletions
@@ -0,0 +1,89 @@
package com.baiyue.firstleg.sailingcalendar.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.sailingcalendar.dto.SailingCalendarDtos.CopyRequest;
import com.baiyue.firstleg.sailingcalendar.entity.SailingEntity;
import com.baiyue.firstleg.sailingcalendar.entity.SailingHistoryEntity;
import com.baiyue.firstleg.sailingcalendar.repository.SailingCalendarRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class SailingCalendarServiceTest {
@Mock private SailingCalendarRepository repository;
@InjectMocks private SailingCalendarService service;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void replaysAnExistingCopyInsteadOfReturningAnInternalError() {
SailingEntity source = sailing(1L, "SL-SOURCE", "MANUAL", "QA-20261005", LocalDateTime.of(2026, 10, 5, 10, 0));
SailingEntity existing = sailing(2L, "SL-COPY", "COPY", "QA-20261005", LocalDateTime.of(2026, 10, 12, 10, 0));
SailingHistoryEntity history = new SailingHistoryEntity();
history.setAction("COPY"); history.setReason("复制自SL-SOURCE"); history.setSailingId(2L);
when(repository.sailing(1L)).thenReturn(source);
when(repository.sailing(2L)).thenReturn(existing);
when(repository.sailingByBusinessKey(8L, "MATSON", "CNNGB", "USLGB", existing.getEtdAt(), "QA-20261005")).thenReturn(existing);
when(repository.histories(2L)).thenReturn(List.of(history));
when(repository.receiptSummary(any(SailingEntity.class))).thenReturn(null);
when(repository.bookingSummary(any(SailingEntity.class))).thenReturn(null);
var result = service.copy(1L, 7L, "admin",
new CopyRequest(existing.getEtdAt(), LocalDateTime.of(2026, 10, 9, 18, 0)));
assertThat(result.sailing().id()).isEqualTo(2L);
verify(repository, never()).insert(any(SailingEntity.class));
}
@Test
void reportsBusinessConflictForAnUnrelatedExistingSailing() {
SailingEntity source = sailing(1L, "SL-SOURCE", "MANUAL", "QA-20261005", LocalDateTime.of(2026, 10, 5, 10, 0));
SailingEntity existing = sailing(2L, "SL-OTHER", "TEMPLATE", "QA-20261005", LocalDateTime.of(2026, 10, 12, 10, 0));
when(repository.sailing(1L)).thenReturn(source);
when(repository.sailingByBusinessKey(8L, "MATSON", "CNNGB", "USLGB", existing.getEtdAt(), "QA-20261005")).thenReturn(existing);
assertThatThrownBy(() -> service.copy(1L, 7L, "admin",
new CopyRequest(existing.getEtdAt(), LocalDateTime.of(2026, 10, 9, 18, 0))))
.isInstanceOf(BizException.class)
.hasMessageContaining("已存在航次 QA-20261005");
verify(repository, never()).insert(any(SailingEntity.class));
}
@Test
void rejectsCutoffAtOrAfterCopiedDeparture() {
SailingEntity source = sailing(1L, "SL-SOURCE", "MANUAL", "QA-20261005", LocalDateTime.of(2026, 10, 5, 10, 0));
when(repository.sailing(1L)).thenReturn(source);
assertThatThrownBy(() -> service.copy(1L, 7L, "admin",
new CopyRequest(LocalDateTime.of(2026, 10, 12, 10, 0), LocalDateTime.of(2026, 10, 12, 10, 0))))
.isInstanceOf(BizException.class)
.hasMessageContaining("收货截止必须早于开船");
verify(repository, never()).insert(any(SailingEntity.class));
}
private static SailingEntity sailing(long id, String sailingNo, String sourceType, String voyageNo, LocalDateTime etdAt) {
SailingEntity value = new SailingEntity();
value.setId(id); value.setSailingNo(sailingNo); value.setSourceType(sourceType); value.setVoyageNo(voyageNo);
value.setWarehouseId(8L); value.setChannelCodeSnapshot("MATSON"); value.setChannelNameSnapshot("美森正班");
value.setDeparturePortCode("CNNGB"); value.setDestinationPortCode("USLGB"); value.setVesselName("MATSON");
value.setEtdAt(etdAt); value.setWarehouseCutoffAt(etdAt.minusDays(3).withHour(18)); value.setStatus("OPEN");
value.setVersion(0); value.setPlannedCapacityCbm(BigDecimal.ZERO);
return value;
}
}