89 lines
6.2 KiB
Java
89 lines
6.2 KiB
Java
package com.baiyue.lastleg.appointmentplanning;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import java.time.Instant;
|
|
import java.time.LocalDateTime;
|
|
import java.time.ZoneOffset;
|
|
import java.util.List;
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
class AppointmentPlanningPolicyTest {
|
|
private static Instant at(int hour) { return Instant.parse("2026-10-15T" + String.format("%02d", hour) + ":00:00Z"); }
|
|
private static AppointmentPlanningPolicy.Cargo cargo(String source, String customer, String origin, String destination,
|
|
Integer packages, Instant from, Instant to) {
|
|
return new AppointmentPlanningPolicy.Cargo(source, customer, origin, destination, "US", "US", packages, from, to, true, null);
|
|
}
|
|
|
|
@Test void twoCustomersCanPlanOneDestinationWithoutAPlanNumberDriverOrExternalConfirmation() {
|
|
var result = AppointmentPlanningPolicy.group(List.of(
|
|
cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", 10, at(1), at(5)),
|
|
cargo("BOOKING:B-1", "B", "WH-1", "FBA:ONT8", 8, at(3), at(8))));
|
|
assertEquals(18, result.packages()); assertEquals(2, result.customerCount());
|
|
assertEquals(at(3), result.readyFrom()); assertEquals(at(5), result.readyTo());
|
|
assertFalse(result.readinessIncomplete());
|
|
}
|
|
|
|
@Test void unknownReadinessIsPreservedInsteadOfInventingAnArrivalOffset() {
|
|
var result = AppointmentPlanningPolicy.group(List.of(cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", 10, null, null)));
|
|
assertTrue(result.readinessIncomplete()); assertNull(result.readyFrom()); assertNull(result.readyTo());
|
|
}
|
|
|
|
@Test void differentOriginsDestinationsAndDisjointWindowsAreNotSilentlyMerged() {
|
|
var first = cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", 10, at(1), at(3));
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first,
|
|
cargo("ORDER:2", "B", "WH-2", "FBA:ONT8", 8, at(1), at(3)))));
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first,
|
|
cargo("ORDER:2", "B", "WH-1", "FBA:LAX9", 8, at(1), at(3)))));
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first,
|
|
cargo("ORDER:2", "B", "WH-1", "FBA:ONT8", 8, at(5), at(8)))));
|
|
}
|
|
|
|
@Test void sourceIdentityMustNotBeCountedAgainAfterMaterialization() {
|
|
var first = cargo("BOOKING:B-1", "A", "WH-1", "FBA:ONT8", 10, null, null);
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first, first)));
|
|
var unknown=AppointmentPlanningPolicy.group(List.of(cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", null, null, null)));
|
|
assertNull(unknown.packages());assertEquals(1,unknown.unknownQuantitySources());
|
|
}
|
|
|
|
@Test void nonUsAndBlockedSourcesCannotBeTreatedAsEligibleAmericanCargo() {
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(
|
|
new AppointmentPlanningPolicy.Cargo("ORDER:1", "A", "WH-1", "FBA:YYZ", "US", "CA", 1, null, null, true, null))));
|
|
var error = assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(
|
|
new AppointmentPlanningPolicy.Cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", "US", "US", 1, null, null, false, "货物已被有效安排占用"))));
|
|
assertEquals("货物已被有效安排占用", error.getMessage());
|
|
}
|
|
|
|
@Test void changedReadinessIdentifiesImpactWithoutOverwritingTheConfirmedTime() {
|
|
var before = new AppointmentPlanningPolicy.Forecast("ORDER:1", "1", at(1), at(3));
|
|
var after = new AppointmentPlanningPolicy.Forecast("ORDER:1", "2", at(4), at(8));
|
|
var impact = AppointmentPlanningPolicy.compare(before, after, at(6));
|
|
assertTrue(impact.reviewRequired()); assertEquals("READINESS_AFTER_APPOINTMENT", impact.reason());
|
|
assertEquals(at(6), impact.confirmedAt()); assertEquals(at(1), impact.previousEta()); assertEquals(at(4), impact.latestEta());
|
|
assertFalse(AppointmentPlanningPolicy.compare(after, after, at(6)).reviewRequired());
|
|
}
|
|
|
|
@Test void lostForecastNeedsReviewAndUnrelatedSourcesCannotBeCompared() {
|
|
var before = new AppointmentPlanningPolicy.Forecast("ORDER:1", "1", at(1), at(3));
|
|
assertEquals("FORECAST_MISSING", AppointmentPlanningPolicy.compare(before,
|
|
new AppointmentPlanningPolicy.Forecast("ORDER:1", "2", null, null), at(6)).reason());
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.compare(before,
|
|
new AppointmentPlanningPolicy.Forecast("ORDER:2", "2", at(2), at(3)), at(6)));
|
|
}
|
|
|
|
@Test void losAngelesTimesResolveToTheLocationZoneAndKeepAnUnknownEndUnknown() {
|
|
var result = AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-10-15T09:00:00"), null, "America/Los_Angeles", null, null);
|
|
assertEquals(Instant.parse("2026-10-15T16:00:00Z"), result.start()); assertNull(result.end());
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-10-15T09:00:00"), null, null, null, null));
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-10-15T09:00:00"), null, "UTC-08:00", null, null));
|
|
assertEquals(Instant.parse("2026-11-01T08:30:00Z"),AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-11-01T01:30:00"),null,"America/Phoenix",null,null).start());
|
|
}
|
|
|
|
@Test void daylightSavingGapIsRejectedAndOverlapRequiresAnExplicitValidOffset() {
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-03-08T02:30:00"), null, "America/Los_Angeles", null, null));
|
|
var overlap = LocalDateTime.parse("2026-11-01T01:30:00");
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(overlap, null, "America/Los_Angeles", null, null));
|
|
assertEquals(Instant.parse("2026-11-01T09:30:00Z"), AppointmentTimePolicy.resolve(overlap, null, "America/Los_Angeles", ZoneOffset.ofHours(-8), null).start());
|
|
assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(overlap, null, "America/Los_Angeles", ZoneOffset.ofHours(8), null));
|
|
}
|
|
}
|