feat(selfop): prefer list SKU quantities with targeted detail fallback
This commit is contained in:
+15
@@ -74,6 +74,21 @@ class RestLingxingAnalyticsClientTest {
|
||||
assertThat(summary.warehouseCode()).isEqualTo("WH-B");
|
||||
server.verify();
|
||||
}
|
||||
@Test void listItemsUseQtyAndKeepUnknownSeparateFromZero() {
|
||||
enable();properties.setQuantityUnit("PCS");
|
||||
expect("/outbound-orders/locate", "{\"deliveryNo\":\"DO-1\",\"current\":1,\"size\":100}", """
|
||||
{"code":"200","data":{"current":1,"size":100,"pages":1,"total":1,"records":[
|
||||
{"deliveryNo":"DO-1","customerCode":"C","whCode":"WH-A","status":100,
|
||||
"productList":[{"productSku":"SKU","qty":2.123456,"actualDeliveryQty":99},
|
||||
{"productSku":"ZERO","qty":0},{"productSku":"UNKNOWN"}]}]}}
|
||||
""");
|
||||
var summary=client.locate(context("synthetic-account"),"DO-1",1,100).records().get(0);
|
||||
assertThat(summary.items().get(0).actualQuantity()).isEqualByComparingTo("2.123456");
|
||||
assertThat(summary.items().get(1).actualQuantity()).isEqualByComparingTo("0");
|
||||
assertThat(summary.items().get(2).actualQuantity()).isNull();
|
||||
assertThat(summary.items().get(0).quantityUnit()).isEqualTo("PCS");
|
||||
assertThat(LingxingAnalyticsClient.needsDetail(summary.order())).isTrue();server.verify();
|
||||
}
|
||||
@Test void detailProjectsActualLinesOnlyAndDoesNotInventUnitOrTimestamp() {
|
||||
enable();
|
||||
expect("/outbound-orders/detail", "{\"warehouseCode\":\"WH-A\",\"customerCode\":\"C\",\"deliveryNo\":\"DO-1\"}", """
|
||||
|
||||
+81
-2
@@ -102,7 +102,7 @@ class SelfopDiscoveryServiceTest {
|
||||
}
|
||||
|
||||
private LingxingAnalyticsClient.Summary summary(String no, String time) {
|
||||
return new LingxingAnalyticsClient.Summary("demo", "customer", "A", no, null, 100, null, time);
|
||||
return new LingxingAnalyticsClient.Summary("demo", "customer", "A", no, null, 100, null, time, null);
|
||||
}
|
||||
private SelfopPayloads.Order detail(String no, String time) {
|
||||
return new SelfopPayloads.Order("demo", "customer", "A", no, null, 100, null, time,
|
||||
@@ -240,7 +240,7 @@ class SelfopDiscoveryServiceTest {
|
||||
}).when(taskService).yield(anyLong(), anyLong(), eq(lease));
|
||||
when(client.detail(any(SelfopRequestContext.class), eq("A"), eq("customer"), eq("moved")))
|
||||
.thenThrow(new AnalyticsClientException("LINGXING_WMS_SOURCE_REJECTED", false));
|
||||
var moved = new LingxingAnalyticsClient.Summary("demo", "customer", "B", "moved", null, 100, null, "2026-10-11 12:00:00");
|
||||
var moved = new LingxingAnalyticsClient.Summary("demo", "customer", "B", "moved", null, 100, null, "2026-10-11 12:00:00", null);
|
||||
when(client.locate(any(SelfopRequestContext.class), eq("moved"), eq(1), eq(2))).thenReturn(page(1, 2, 1, 1, moved));
|
||||
when(client.detail(any(SelfopRequestContext.class), eq("B"), eq("customer"), eq("moved"))).thenReturn(new SelfopPayloads.Order("demo", "customer", "B", "moved", null,
|
||||
100, null, "2026-10-11 12:00:00", List.of(new SelfopPayloads.Item("sku", null, BigDecimal.ONE, "PCS"))));
|
||||
@@ -296,6 +296,85 @@ class SelfopDiscoveryServiceTest {
|
||||
assertEquals("ROUTED",run.getStatus());assertEquals(500L,read(task.getRoutingManifestJson(),SelfopRoutingManifest.class).getObjects().get(key).getEffectiveStageId());
|
||||
assertEquals(SelfopOutboundFingerprint.order(loaded),stored.get(500L).getContentHash());
|
||||
}
|
||||
private LingxingAnalyticsClient.Summary listed(String no, List<SelfopPayloads.Item> items) {
|
||||
var s=summary(no,"2026-10-11 12:00:00");
|
||||
return new LingxingAnalyticsClient.Summary(s.sourceAccount(),s.customerCode(),s.warehouseCode(),s.deliveryNo(),
|
||||
s.sourceNo(),s.sourceStatus(),s.sourceCreatedTime(),s.sourceOutboundTime(),items);
|
||||
}
|
||||
@Test void completeListAndRealZeroNeedNoDetailsEvenWhenRequestBudgetIsExhausted() {
|
||||
account.setRequestBudget(2);
|
||||
when(client.discover(any(),eq(from),eq(until),eq(1),eq(2))).thenReturn(page(1,2,2,1,
|
||||
listed("one",List.of(new SelfopPayloads.Item("sku",null,new BigDecimal("2.123456"),"PCS"))),
|
||||
listed("zero",List.of(new SelfopPayloads.Item("sku",null,BigDecimal.ZERO,"PCS")))));
|
||||
service.collect(task,run,lease);
|
||||
verify(client,never()).detail(any(),anyString(),anyString(),anyString());
|
||||
assertEquals("ROUTED",run.getStatus());assertEquals(2,stored.size());
|
||||
assertTrue(stored.values().stream().allMatch(s->"VALID".equals(s.getValidationStatus())));
|
||||
var quantities=stored.values().stream().map(s->read(s.getPayloadJson(),SelfopPayloads.Order.class).items().get(0).actualQuantity()).toList();
|
||||
assertTrue(quantities.stream().anyMatch(q->q.compareTo(new BigDecimal("2.123456"))==0));
|
||||
assertTrue(quantities.stream().anyMatch(q->q.signum()==0));
|
||||
}
|
||||
@Test void completeListYieldsAtTimeLimitAndResumesWithoutRequests() {
|
||||
when(client.discover(any(),eq(from),eq(until),eq(1),eq(2))).thenReturn(page(1,2,2,1,
|
||||
listed("one",List.of(new SelfopPayloads.Item("sku",null,BigDecimal.ONE,"PCS"))),
|
||||
listed("two",List.of(new SelfopPayloads.Item("sku",null,BigDecimal.TEN,"PCS")))));
|
||||
try(var budgets=mockConstruction(SelfopBudget.class,(budget,context)->{
|
||||
when(budget.take()).thenReturn(true);
|
||||
when(budget.hasTime()).thenReturn(true,false);
|
||||
})) {
|
||||
service.collect(task,run,lease);
|
||||
verify(budgets.constructed().get(0),times(2)).take();
|
||||
}
|
||||
verify(taskService).yield(task.getId(),run.getId(),lease);
|
||||
assertEquals(1,stored.values().stream().filter(s->"VALID".equals(s.getValidationStatus())).count());
|
||||
assertTrue(children.values().stream().allMatch(s->"WAITING_SOURCE".equals(s.getStatus())));
|
||||
service.collect(task,run,lease);
|
||||
verify(client,times(2)).discover(any(),eq(from),eq(until),eq(1),eq(2));
|
||||
verify(client,never()).detail(any(),anyString(),anyString(),anyString());
|
||||
assertEquals(2,stored.values().stream().filter(s->"VALID".equals(s.getValidationStatus())).count());
|
||||
assertEquals("ROUTED",run.getStatus());
|
||||
}
|
||||
@Test void onlyIncompleteOrderFallsBackAndRetryDoesNotRefetchCompletedOrders() {
|
||||
when(client.discover(any(),eq(from),eq(until),eq(1),eq(2))).thenReturn(page(1,2,2,1,
|
||||
listed("complete",List.of(new SelfopPayloads.Item("sku",null,BigDecimal.TEN,"PCS"))),
|
||||
listed("missing",List.of(new SelfopPayloads.Item("sku",null,null,"PCS")))));
|
||||
when(client.detail(any(),eq("A"),eq("customer"),eq("missing")))
|
||||
.thenThrow(new AnalyticsClientException("INTEGRATION_PROVIDER_FAILURE",true))
|
||||
.thenReturn(detail("missing","2026-10-11 12:00:00"));
|
||||
assertThrows(AnalyticsClientException.class,()->service.collect(task,run,lease));
|
||||
assertEquals(1,stored.values().stream().filter(s->"VALID".equals(s.getValidationStatus())).count());
|
||||
service.collect(task,run,lease);
|
||||
verify(client,never()).detail(any(),eq("A"),eq("customer"),eq("complete"));
|
||||
verify(client,times(2)).detail(any(),eq("A"),eq("customer"),eq("missing"));
|
||||
verify(client,times(2)).discover(any(),eq(from),eq(until),eq(1),eq(2));
|
||||
assertEquals("ROUTED",run.getStatus());
|
||||
}
|
||||
@Test void missingSkuOrEmptyItemsFallsBackPerOrder() {
|
||||
when(client.discover(any(),eq(from),eq(until),eq(1),eq(2))).thenReturn(page(1,2,2,1,
|
||||
listed("blank",List.of(new SelfopPayloads.Item(" ",null,BigDecimal.ONE,"PCS"))),listed("empty",List.of())));
|
||||
service.collect(task,run,lease);
|
||||
verify(client).detail(any(),eq("A"),eq("customer"),eq("blank"));
|
||||
verify(client).detail(any(),eq("A"),eq("customer"),eq("empty"));
|
||||
assertEquals("ROUTED",run.getStatus());
|
||||
}
|
||||
@Test void genuinelyEmptyDirectoryDoesNotFetchDetails() {
|
||||
when(client.discover(any(),eq(from),eq(until),eq(1),eq(2))).thenReturn(page(1,2,0,0));
|
||||
service.collect(task,run,lease);
|
||||
verify(client,never()).detail(any(),anyString(),anyString(),anyString());
|
||||
assertEquals("ROUTED",run.getStatus());assertTrue(stored.isEmpty());
|
||||
}
|
||||
@Test void partialOldDirectoryRestartsScansWithListItems() {
|
||||
var old=new SelfopRoutingManifest();old.setScan(2);old.setNextPage(1);old.setPages(1);old.setTotal(1L);
|
||||
old.getFirstScan().put(hash(List.of("demo","customer","one")),"old-summary-without-items");
|
||||
task.setRoutingManifestJson(json(old));
|
||||
when(client.discover(any(),eq(from),eq(until),eq(1),eq(2))).thenReturn(page(1,2,1,1,
|
||||
listed("one",List.of(new SelfopPayloads.Item("sku",null,BigDecimal.ONE,"PCS")))));
|
||||
service.collect(task,run,lease);
|
||||
verify(client,times(2)).discover(any(),eq(from),eq(until),eq(1),eq(2));
|
||||
verify(client,never()).detail(any(),anyString(),anyString(),anyString());
|
||||
assertEquals("ROUTED",run.getStatus());
|
||||
assertEquals(1,read(task.getRoutingManifestJson(),SelfopRoutingManifest.class).getListPayloadVersion());
|
||||
}
|
||||
@Test void validationPurposeCannotReachAccountRoutingOrFormalCoverage() {
|
||||
task.setExecutionPurpose("VALIDATION");run.setExecutionPurpose("VALIDATION");
|
||||
assertEquals("SELFOP_BUSINESS_PURPOSE_REQUIRED",assertThrows(IllegalStateException.class,()->service.collect(task,run,lease)).getMessage());
|
||||
|
||||
+2
-2
@@ -50,7 +50,7 @@ class SelfopValidationRunnerTest {
|
||||
}
|
||||
private <T> LingxingAnalyticsClient.Page<T> page(int current,long total,int pages,List<T> records){return new LingxingAnalyticsClient.Page<>(records,current,10,total,pages);}
|
||||
private SelfopPayloads.Stock stock(String warehouse,String sku,BigDecimal qty){return new SelfopPayloads.Stock("demo","customer",warehouse,sku,0,"Name",qty,qty,BigDecimal.ZERO,BigDecimal.ZERO,null);}
|
||||
private LingxingAnalyticsClient.Summary summary(String number){return new LingxingAnalyticsClient.Summary("demo","customer","A",number,"source",100,null,"2026-10-12 08:02:00");}
|
||||
private LingxingAnalyticsClient.Summary summary(String number){return new LingxingAnalyticsClient.Summary("demo","customer","A",number,"source",100,null,"2026-10-12 08:02:00", null);}
|
||||
|
||||
@Test void selectedWarehouseSampleOnlyStagesItsOwnRowsAndDoesNotClaimCompleteCoverage() {
|
||||
when(client.inventory(any(SelfopRequestContext.class),eq("A"),eq(1),eq(10))).thenReturn(page(1,30,3,List.of(stock("A","one",BigDecimal.ZERO))));
|
||||
@@ -186,7 +186,7 @@ class SelfopValidationRunnerTest {
|
||||
verify(client,never()).discover(any(SelfopRequestContext.class),any(),any(),anyInt(),anyInt());verifyNoInteractions(stages);
|
||||
}
|
||||
@Test void mixedAccountDirectoryOnlyFetchesDetailsOfFrozenWarehouse() {
|
||||
var other=new LingxingAnalyticsClient.Summary("demo","customer","B","other","source",100,null,"2026-10-12 08:02:00");
|
||||
var other=new LingxingAnalyticsClient.Summary("demo","customer","B","other","source",100,null,"2026-10-12 08:02:00", null);
|
||||
when(client.discover(any(SelfopRequestContext.class),any(),any(),eq(1),eq(10))).thenReturn(page(1,2,1,List.of(other,summary("selected"))));
|
||||
when(client.detail(any(SelfopRequestContext.class),eq("A"),eq("customer"),eq("selected")))
|
||||
.thenReturn(new SelfopPayloads.Order("demo","customer","A","selected","source",100,null,"2026-10-12 08:02:00",List.of()));
|
||||
|
||||
Reference in New Issue
Block a user