fix(delivery): expose signed quantity and publish correct signature instant

This commit is contained in:
2026-10-03 23:09:24 +08:00
parent 0c8ede3889
commit 1f7ddf7bc9
11 changed files with 162 additions and 16 deletions
@@ -61,12 +61,28 @@ class DeliveryLongIdJsonContractTest {
});
}
private DeliveryResponses.DeliveryRow deliveryRow() {
@Test
void signedPackagesMustPreserveNullZeroAndPositiveDatabaseFacts() {
contextRunner.run(context -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
objectMapper.setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL);
for (Integer count : new Integer[] {null, 0, 3}) {
JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(deliveryRow(count)));
assertThat(json.has("signedPackages")).isTrue();
if (count == null) assertThat(json.get("signedPackages").isNull()).isTrue();
else assertThat(json.get("signedPackages").intValue()).isEqualTo(count);
}
});
}
private DeliveryResponses.DeliveryRow deliveryRow() { return deliveryRow(null); }
private DeliveryResponses.DeliveryRow deliveryRow(Integer signedPackages) {
return new DeliveryResponses.DeliveryRow(
DELIVERY_ID,
null, null, "PS-1", null, null, null, null, "CREATED", null,
null, null, null, null, null, null, null, null, null, null, null,
0, 0, null, null, null, null, null, null, null, null, 0, java.util.List.of(), null,
0, 0, signedPackages, null, null, null, null, null, null, null, null, 0, java.util.List.of(), null,
com.baiyue.lastleg.delivery.service.DeliveryDocumentSnapshot.summary(null), null, null, null,
null, null, null, null, null);
}
@@ -34,7 +34,8 @@ class DeliveryCompletedOutboxServiceTest {
delivery.setPackageCount(10); delivery.setSignedPackages(10);
service.publish(delivery, List.of(order(11, "101", "201", "IN-1", "ONT8"),
order(12, "102", "202", "IN-2", "ACY2")), proofs(), Instant.parse("2026-09-15T08:00:00Z"));
order(12, "102", "202", "IN-2", "ACY2")), proofs(),
java.time.LocalDateTime.of(2026, 10, 3, 22, 13).atZone(java.time.ZoneId.of("Asia/Shanghai")).toInstant());
ArgumentCaptor<LastlegEventOutbox> rows = ArgumentCaptor.forClass(LastlegEventOutbox.class);
verify(mapper, times(2)).insert(rows.capture());
@@ -48,6 +49,13 @@ class DeliveryCompletedOutboxServiceTest {
.containsExactly("101", "102");
assertThat(payloads).extracting(OverseasDeliveryCompletedV1::fbaWarehouseCode)
.containsExactly("ONT8", "ACY2");
assertThat(payloads).extracting(OverseasDeliveryCompletedV1::completedAt)
.containsOnly(Instant.parse("2026-10-03T14:13:00Z"));
for (var row : rows.getAllValues()) {
assertThat(json.readValue(row.getPayloadJson(), EventEnvelope.class).occurredAt())
.isEqualTo(Instant.parse("2026-10-03T14:13:00Z"));
assertThat(row.getCreatedAt()).isEqualTo(java.time.LocalDateTime.of(2026, 10, 3, 14, 13));
}
}
@Test
@@ -18,6 +18,13 @@ import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockMultipartFile;
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.TimeZone;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.api.parallel.ResourceLock;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -174,6 +181,27 @@ class DeliveryDocumentServiceTest {
assertThat(order.getStatus()).isEqualTo("WAIT_DELIVERY");
assertThat(order.getVersion()).isZero();
}
@ParameterizedTest
@ValueSource(strings = { "Asia/Shanghai", "America/Los_Angeles" })
@ResourceLock("java.util.TimeZone.default")
void signatureEventUsesTheExecutionZoneInsteadOfReinterpretingLocalTimeAsUtc(String zone) {
TimeZone previous = TimeZone.getDefault();
try {
TimeZone.setDefault(TimeZone.getTimeZone(zone));
order.setStatus("WAIT_DELIVERY");
var outbound = new OutboundOrder(); outbound.setStatus("OUTBOUNDED");
when(outboundOrders.findByNo("100", "OUT-1")).thenReturn(outbound);
when(orders.lockByNo("100", "DL-1")).thenReturn(order);
when(files.metadata("100", 10, RECEIPT, 13)).thenReturn(file(13));
Instant before = Instant.now();
service.save("100", "DL-1", "9", request(0, "receipt-zone", List.of(new Binding(RECEIPT, List.of(13L)))));
ArgumentCaptor<Instant> completed = ArgumentCaptor.forClass(Instant.class);
verify(deliveryCompletedOutbox).publish(eq(order), anyList(), anyMap(), completed.capture());
assertThat(completed.getValue()).isEqualTo(order.getSignedAt().atZone(ZoneId.of(zone)).toInstant());
assertThat(completed.getValue()).isBetween(before, Instant.now());
assertThat(completed.getValue()).isNotEqualTo(order.getSignedAt().toInstant(ZoneOffset.UTC));
} finally { TimeZone.setDefault(previous); }
}
@Test void bolMayBePreparedBeforeOutboundWithoutCompletingDelivery() {
order.setStatus("WAIT_DELIVERY");
when(orders.lockByNo("100","DL-1")).thenReturn(order);
@@ -544,6 +544,18 @@ class DeliveryTransferServiceTest {
verify(tailOrders).selectBatchIds(List.of(901L));
}
@Test
void deliveryRowMapsOnlyTheNullableStoredSignedQuantityWithoutShipmentFallback() {
DeliveryOrder order = new DeliveryOrder();
order.setId(401L); order.setDeliveryNo("DEL-401"); order.setPackageCount(9);
for (Integer signed : new Integer[] {null, 0, 3}) {
order.setSignedPackages(signed);
DeliveryResponses.DeliveryRow row = org.springframework.test.util.ReflectionTestUtils.invokeMethod(service, "deliveryRow", order);
assertEquals(signed, row.signedPackages());
assertEquals(9, row.packageCount());
}
}
@Test
void globalOutboundCancelIsIdempotentForAnUnexecutedOrder() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");