feat: gate overseas receiving and harden phase4 workflows

This commit is contained in:
2026-08-25 10:31:04 +08:00
parent cb78bc6b41
commit 52b9fc382f
93 changed files with 2779 additions and 201 deletions
@@ -31,6 +31,16 @@ class OverseasProcurementOpenApiContractTest {
}
assertTrue(yaml.contains("platform:overseas-business:truck:create"));
assertTrue(yaml.contains("platform:overseas-business:truck:edit"));
assertTrue(yaml.contains("/{orderNo}/review:"));
assertTrue(yaml.contains("/{orderNo}/receive-pallets:"));
assertTrue(yaml.contains("/{orderNo}/review-pallet-overage:"));
assertTrue(yaml.contains("/{orderNo}/allocate-pallets:"));
assertTrue(yaml.contains("/{orderNo}/prepare-dispatch:"));
assertTrue(yaml.contains("/{orderNo}/dispatch:"));
assertTrue(yaml.contains("/{orderNo}/start-transit:"));
assertTrue(yaml.contains("/{orderNo}/complete:"));
assertTrue(yaml.contains("enum: [PENDING_REVIEW, WAITING_INBOUND, RECEIVING, WAITING_CONSOLIDATION, PALLET_OVERAGE_REVIEW, PARTIALLY_ALLOCATED, ALLOCATED, PENDING_DISPATCH, DISPATCHED, IN_TRANSIT, COMPLETED, REJECTED, CANCELLED]"));
assertTrue(yaml.contains("required: [version, status, idempotencyKey]"));
assertTrue(yaml.contains("/api/platform/v1/last-leg/local-looseboard-orders:"));
assertTrue(yaml.contains("platform:overseas-business:looseboard:list"));
assertTrue(yaml.contains("platform:overseas-business:looseboard:create"));
@@ -16,6 +16,9 @@ class OverseasProcurementSchemaContractTest {
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_status_history"));
assertTrue(sql.contains("operation_type VARCHAR(32) NOT NULL"));
assertTrue(sql.contains("review_idempotency_key VARCHAR(255) NULL"));
assertTrue(sql.contains("review_request_hash CHAR(64) NULL"));
assertTrue(sql.contains("idempotency_key VARCHAR(255) NULL COMMENT '操作幂等键'"));
assertTrue(sql.contains("change_detail JSON NULL"));
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_outbound"));
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("outbound_quantity BIGINT"));
@@ -40,7 +40,7 @@ class OverseasProcurementControllerTest {
@Test
void reviewRequiresExplicitPermission() {
var request = new OverseasProcurementRequests.Review("APPROVE", null);
var request = new OverseasProcurementRequests.Review(0L, "APPROVE", "review-1", null);
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> platform.review(user(0L, "PLATFORM", Set.of()), "100", "HC1", request));
assertEquals(403, error.getStatusCode().value());
@@ -56,7 +56,7 @@ class OverseasProcurementControllerTest {
@Test
void platformReviewUsesExplicitTargetTenant() {
var request = new OverseasProcurementRequests.Review("APPROVE", "ok");
var request = new OverseasProcurementRequests.Review(0L, "APPROVE", "review-1", "ok");
platform.review(user(0L, "PLATFORM", Set.of(OverseasProcurementService.REVIEW_PERMISSION)),
"100", "HC1", request);
verify(service).review("100", "HC1", "7", request);
@@ -145,6 +145,33 @@ class OverseasProcurementServiceTest {
assertEquals("Acme", values.get(0).customerName());
}
@Test
void approveReviewIsIdempotentAuditedAndTerminalCannotReverse() {
OverseasProcurementOrder order = order(OverseasProcurementService.PENDING_REVIEW, 0, 0);
when(orders.lockNo("100", "HC1")).thenReturn(order);
when(orders.findNo("100", "HC1")).thenReturn(order);
when(histories.listOrder("100", 1L)).thenReturn(List.of());
var request = new OverseasProcurementRequests.Review(0L, "APPROVE", "approve-1", "approved");
var approved = service.review("100", "HC1", "operator-7", request);
assertEquals(OverseasProcurementService.APPROVED, approved.summary().status());
assertEquals(1L, approved.summary().version());
assertEquals(1L, service.review("100", "HC1", "operator-7", request).summary().version());
verify(histories).insert(org.mockito.ArgumentMatchers
.<com.baiyue.lastleg.procurement.entity.OverseasProcurementStatusHistory>argThat(value ->
"REVIEW".equals(value.getOperationType()) && "operator-7".equals(value.getOperatorId())
&& "approve-1".equals(value.getIdempotencyKey())
&& OverseasProcurementService.APPROVED.equals(value.getToStatus())));
assertThrows(ResponseStatusException.class, () -> service.review("100", "HC1", "operator-7",
new OverseasProcurementRequests.Review(1L, "REJECT", "approve-1", "different payload")));
assertThrows(ResponseStatusException.class, () -> service.review("100", "HC1", "operator-7",
new OverseasProcurementRequests.Review(0L, "REJECT", "stale-review", "old version")));
assertThrows(ResponseStatusException.class, () -> service.review("100", "HC1", "operator-7",
new OverseasProcurementRequests.Review(1L, "REJECT", "reject-after-approved", "reverse")));
assertEquals(OverseasProcurementService.APPROVED, order.getStatus());
}
private OverseasProcurementOrder order(String status, long inbound, long outbound) {
OverseasProcurementOrder order = new OverseasProcurementOrder();
order.setId(1L);