test(firstleg): harden measurement demo package
This commit is contained in:
@@ -123,11 +123,13 @@ public class InboundMeasurementService {
|
|||||||
long tenantId, long inboundId, long operatorId,
|
long tenantId, long inboundId, long operatorId,
|
||||||
InboundMeasurementRequests.SubmitRequest request) {
|
InboundMeasurementRequests.SubmitRequest request) {
|
||||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||||
|
if (!Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED")
|
||||||
|
.contains(inbound.getProcessStatus())) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "测量已提交或审核完成,不能重复提交或修改冻结结果");
|
||||||
|
}
|
||||||
InboundMeasurementEntity measurement = requireMeasurement(tenantId, inboundId, request.measurementId());
|
InboundMeasurementEntity measurement = requireMeasurement(tenantId, inboundId, request.measurementId());
|
||||||
requireVersion(request.version(), measurement.getVersion());
|
requireVersion(request.version(), measurement.getVersion());
|
||||||
boolean alreadyAdvanced = !Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED")
|
if (!Set.of(MeasurementBatchService.STATUS_DRAFT, MeasurementBatchService.STATUS_RECEIVED)
|
||||||
.contains(inbound.getProcessStatus());
|
|
||||||
if (!alreadyAdvanced && !Set.of(MeasurementBatchService.STATUS_DRAFT, MeasurementBatchService.STATUS_RECEIVED)
|
|
||||||
.contains(measurement.getStatus())) {
|
.contains(measurement.getStatus())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "当前测量批次不能重复提交审核");
|
throw new BizException(ResultCode.CONFLICT, "当前测量批次不能重复提交审核");
|
||||||
}
|
}
|
||||||
@@ -135,19 +137,15 @@ public class InboundMeasurementService {
|
|||||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||||
ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights);
|
ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights);
|
||||||
|
|
||||||
if (!alreadyAdvanced) {
|
requireConfirmedPackageTotal(inbound, items);
|
||||||
requireConfirmedPackageTotal(inbound, items);
|
measurement.setStatus(STATUS_SUBMITTED);
|
||||||
measurement.setStatus(STATUS_SUBMITTED);
|
if (!repository.updateMeasurement(measurement)) {
|
||||||
if (!repository.updateMeasurement(measurement)) {
|
throw concurrentModification();
|
||||||
throw concurrentModification();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!alreadyAdvanced) {
|
inbound.setProcessStatus("MEASURE_PENDING");
|
||||||
inbound.setProcessStatus("MEASURE_PENDING");
|
inbound.setMeasureReviewStatus("PENDING");
|
||||||
inbound.setMeasureReviewStatus("PENDING");
|
inbound.setMeasureCompletedAt(LocalDateTime.now());
|
||||||
inbound.setMeasureCompletedAt(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
if (measurement.getTotalPackageCount() != null) {
|
if (measurement.getTotalPackageCount() != null) {
|
||||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,14 @@ public class MeasurementReviewService {
|
|||||||
}
|
}
|
||||||
if (approved) {
|
if (approved) {
|
||||||
var items = repository.listMeasurementItems(tenantId, measurement.getId());
|
var items = repository.listMeasurementItems(tenantId, measurement.getId());
|
||||||
|
if (items.isEmpty() || items.stream().anyMatch(item -> item.getPackageCount() == null
|
||||||
|
|| item.getPackageCount() <= 0 || item.getTotalVolumeCbm() == null
|
||||||
|
|| item.getTotalVolumeCbm().signum() <= 0 || item.getVolumeWeightKg() == null
|
||||||
|
|| item.getVolumeWeightKg().signum() <= 0)) {
|
||||||
|
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||||
|
"测量明细箱数、体积和体积重必须完整且大于零,才能审核通过");
|
||||||
|
}
|
||||||
long confirmed = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getPackageCount)
|
long confirmed = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getPackageCount)
|
||||||
.filter(java.util.Objects::nonNull)
|
|
||||||
.mapToLong(Integer::longValue)
|
.mapToLong(Integer::longValue)
|
||||||
.sum();
|
.sum();
|
||||||
if (order.getTotalPackageCount() == null || confirmed != order.getTotalPackageCount()) {
|
if (order.getTotalPackageCount() == null || confirmed != order.getTotalPackageCount()) {
|
||||||
@@ -97,13 +103,23 @@ public class MeasurementReviewService {
|
|||||||
.map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getNetWeightKg)
|
.map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getNetWeightKg)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
BigDecimal volume = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalVolumeCbm)
|
BigDecimal volume = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalVolumeCbm)
|
||||||
.filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
BigDecimal volumeWeight = items.stream()
|
||||||
|
.map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getVolumeWeightKg)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
if (order.getActualGrossWeightKg() == null || order.getActualNetWeightKg() == null
|
if (order.getActualGrossWeightKg() == null || order.getActualNetWeightKg() == null
|
||||||
|| order.getActualGrossWeightKg().compareTo(grossWeight) != 0
|
|| order.getActualGrossWeightKg().compareTo(grossWeight) != 0
|
||||||
|| order.getActualNetWeightKg().compareTo(netWeight) != 0
|
|| order.getActualNetWeightKg().compareTo(netWeight) != 0
|
||||||
|| volume.signum() <= 0) {
|
|| order.getActualVolumeCbm() == null || order.getActualVolumeCbm().compareTo(volume) != 0
|
||||||
|
|| order.getVolumeWeightKg() == null || order.getVolumeWeightKg().compareTo(volumeWeight) != 0
|
||||||
|
|| measurement.getTotalPackageCount() == null
|
||||||
|
|| measurement.getTotalPackageCount() != confirmed
|
||||||
|
|| measurement.getTotalVolumeCbm() == null
|
||||||
|
|| measurement.getTotalVolumeCbm().compareTo(volume) != 0
|
||||||
|
|| measurement.getVolumeWeightKg() == null
|
||||||
|
|| measurement.getVolumeWeightKg().compareTo(volumeWeight) != 0) {
|
||||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||||
"审核通过前称重毛重、货物净重汇总和体积必须完整且一致");
|
"审核通过前箱数、称重、体积和体积重必须在明细、测量批次与入库汇总间完整且一致");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-7
@@ -234,6 +234,38 @@ class InboundMeasurementServiceTest {
|
|||||||
assertThat(result.confidence()).isEqualByComparingTo("0.50972");
|
assertThat(result.confidence()).isEqualByComparingTo("0.50972");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ocrFailureCreatesManualWeightThatCanBeAdjustedAndConfirmed() {
|
||||||
|
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||||
|
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||||
|
|
||||||
|
InboundMeasurementResponses.WeightResult created = service.createWeightResult(
|
||||||
|
1L, 10L, 9L,
|
||||||
|
new InboundMeasurementRequests.CreateWeightResultRequest(
|
||||||
|
120L, 501L, "FILE_WEIGHT_OCR", null, null, null, null));
|
||||||
|
|
||||||
|
assertThat(created.recognitionStatus()).isEqualTo("NEEDS_MANUAL");
|
||||||
|
ArgumentCaptor<InboundWeightResultEntity> saved =
|
||||||
|
ArgumentCaptor.forClass(InboundWeightResultEntity.class);
|
||||||
|
verify(repository).insertWeight(saved.capture());
|
||||||
|
InboundWeightResultEntity weight = saved.getValue();
|
||||||
|
weight.setId(20L);
|
||||||
|
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||||
|
when(repository.updateWeight(weight)).thenReturn(true);
|
||||||
|
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||||
|
when(repository.updateInboundWeightSummary(inbound)).thenReturn(true);
|
||||||
|
|
||||||
|
InboundMeasurementResponses.WeightResult confirmed = service.confirmWeightResult(
|
||||||
|
1L, 10L, 20L, 9L, "操作员",
|
||||||
|
new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||||
|
0, new BigDecimal("30.000"), "KG", "OCR失败后人工确认"));
|
||||||
|
|
||||||
|
assertThat(confirmed.recognitionStatus()).isEqualTo("CONFIRMED");
|
||||||
|
assertThat(confirmed.grossWeightKg()).isEqualByComparingTo("30.000");
|
||||||
|
assertThat(confirmed.netWeightKg()).isEqualByComparingTo("11.500");
|
||||||
|
verify(repository, org.mockito.Mockito.times(2)).insertRevision(any(InboundWeightRevisionEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void weightPalletOptionsUsesInboundWarehouseWithoutCustomerScope() {
|
void weightPalletOptionsUsesInboundWarehouseWithoutCustomerScope() {
|
||||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||||
@@ -799,9 +831,13 @@ class InboundMeasurementServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
|
void approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals() {
|
||||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||||
inbound.setMeasureReviewStatus("APPROVED");
|
inbound.setMeasureReviewStatus("APPROVED");
|
||||||
|
inbound.setActualGrossWeightKg(new BigDecimal("40.000"));
|
||||||
|
inbound.setActualNetWeightKg(new BigDecimal("30.000"));
|
||||||
|
inbound.setActualVolumeCbm(new BigDecimal("2.0000"));
|
||||||
|
inbound.setVolumeWeightKg(new BigDecimal("334.000"));
|
||||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||||
measurement.setId(30L);
|
measurement.setId(30L);
|
||||||
measurement.setStatus("APPROVED");
|
measurement.setStatus("APPROVED");
|
||||||
@@ -815,14 +851,17 @@ class InboundMeasurementServiceTest {
|
|||||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||||
|
|
||||||
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
assertThatThrownBy(() -> service.submit(1L, 10L, 9L,
|
||||||
new InboundMeasurementRequests.SubmitRequest(30L, 2));
|
new InboundMeasurementRequests.SubmitRequest(30L, 2)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessageContaining("审核完成");
|
||||||
|
|
||||||
assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT");
|
assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("40.000");
|
||||||
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
|
assertThat(inbound.getActualNetWeightKg()).isEqualByComparingTo("30.000");
|
||||||
assertThat(measurement.getStatus()).isEqualTo("APPROVED");
|
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("2.0000");
|
||||||
|
assertThat(inbound.getVolumeWeightKg()).isEqualByComparingTo("334.000");
|
||||||
verify(repository, never()).updateMeasurement(any());
|
verify(repository, never()).updateMeasurement(any());
|
||||||
verify(repository).updateInbound(inbound);
|
verify(repository, never()).updateInbound(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+49
-4
@@ -91,6 +91,7 @@ class MeasurementReviewServiceTest {
|
|||||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||||
item.setPackageCount(5);
|
item.setPackageCount(5);
|
||||||
item.setTotalVolumeCbm(new BigDecimal("0.7480"));
|
item.setTotalVolumeCbm(new BigDecimal("0.7480"));
|
||||||
|
item.setVolumeWeightKg(new BigDecimal("124.916"));
|
||||||
when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
|
when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
|
||||||
when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true, 20L)).thenReturn(1);
|
when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true, 20L)).thenReturn(1);
|
||||||
when(fixture.repository.transitionMeasurement(10001L, 10L, 20L, "APPROVED")).thenReturn(1);
|
when(fixture.repository.transitionMeasurement(10001L, 10L, 20L, "APPROVED")).thenReturn(1);
|
||||||
@@ -118,6 +119,40 @@ class MeasurementReviewServiceTest {
|
|||||||
verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
|
verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("多SKU测量行的箱数、重量、体积和体积重守恒后才能进入WAIT_SPLIT")
|
||||||
|
void shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch() {
|
||||||
|
Fixture fixture = fixture(false);
|
||||||
|
InboundMeasurementItemEntity first = measurementItem(2, "0.3000", "50.100");
|
||||||
|
InboundMeasurementItemEntity second = measurementItem(3, "0.4480", "74.816");
|
||||||
|
when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(first, second));
|
||||||
|
when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true, 20L)).thenReturn(1);
|
||||||
|
when(fixture.repository.transitionMeasurement(10001L, 10L, 20L, "APPROVED")).thenReturn(1);
|
||||||
|
|
||||||
|
fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-multi-sku",
|
||||||
|
new MeasurementReviewRequest(20L, 3, null));
|
||||||
|
|
||||||
|
ArgumentCaptor<InboundStatusHistoryEntity> history =
|
||||||
|
ArgumentCaptor.forClass(InboundStatusHistoryEntity.class);
|
||||||
|
verify(fixture.repository).insertHistory(history.capture());
|
||||||
|
assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("体积或体积重任一层汇总不一致时审核零写入")
|
||||||
|
void shouldRejectApprovalWhenVolumeSnapshotDoesNotConserve() {
|
||||||
|
Fixture fixture = fixture(false);
|
||||||
|
fixture.order.setActualVolumeCbm(new BigDecimal("0.7479"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L,
|
||||||
|
"approve-volume-mismatch", new MeasurementReviewRequest(20L, 3, null)))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessageContaining("体积和体积重");
|
||||||
|
|
||||||
|
verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
|
||||||
|
verify(fixture.repository, never()).transitionMeasurement(anyLong(), anyLong(), anyLong(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("驳回原因必填且状态推进为测量驳回")
|
@DisplayName("驳回原因必填且状态推进为测量驳回")
|
||||||
void shouldRequireRejectReason() {
|
void shouldRequireRejectReason() {
|
||||||
@@ -217,13 +252,15 @@ class MeasurementReviewServiceTest {
|
|||||||
order.setTotalPackageCount(5);
|
order.setTotalPackageCount(5);
|
||||||
order.setActualGrossWeightKg(new BigDecimal("1787.500"));
|
order.setActualGrossWeightKg(new BigDecimal("1787.500"));
|
||||||
order.setActualNetWeightKg(new BigDecimal("1747.500"));
|
order.setActualNetWeightKg(new BigDecimal("1747.500"));
|
||||||
|
order.setActualVolumeCbm(new BigDecimal("0.7480"));
|
||||||
|
order.setVolumeWeightKg(new BigDecimal("124.916"));
|
||||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||||
measurement.setId(20L);
|
measurement.setId(20L);
|
||||||
measurement.setStatus("SUBMITTED");
|
measurement.setStatus("SUBMITTED");
|
||||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
measurement.setTotalPackageCount(5);
|
||||||
item.setPackageCount(5);
|
measurement.setTotalVolumeCbm(new BigDecimal("0.7480"));
|
||||||
item.setTotalVolumeCbm(new BigDecimal("0.7480"));
|
measurement.setVolumeWeightKg(new BigDecimal("124.916"));
|
||||||
item.setVolumeWeightKg(new BigDecimal("124.916"));
|
InboundMeasurementItemEntity item = measurementItem(5, "0.7480", "124.916");
|
||||||
when(repository.findOwned(10001L, 10L)).thenReturn(order);
|
when(repository.findOwned(10001L, 10L)).thenReturn(order);
|
||||||
when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement);
|
when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement);
|
||||||
when(repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
|
when(repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
|
||||||
@@ -236,6 +273,14 @@ class MeasurementReviewServiceTest {
|
|||||||
return new Fixture(service, repository, idempotency, order, measurement);
|
return new Fixture(service, repository, idempotency, order, measurement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private InboundMeasurementItemEntity measurementItem(int packages, String volume, String volumeWeight) {
|
||||||
|
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||||
|
item.setPackageCount(packages);
|
||||||
|
item.setTotalVolumeCbm(new BigDecimal(volume));
|
||||||
|
item.setVolumeWeightKg(new BigDecimal(volumeWeight));
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private InboundWeightResultEntity confirmedWeight(String grossWeightKg, String netWeightKg) {
|
private InboundWeightResultEntity confirmedWeight(String grossWeightKg, String netWeightKg) {
|
||||||
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
||||||
weight.setRecognitionStatus("CONFIRMED");
|
weight.setRecognitionStatus("CONFIRMED");
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { readFileSync } from 'node:fs'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
const source = readFileSync(new URL('inbound-measurement.mjs', import.meta.url), 'utf8')
|
const source = readFileSync(new URL('inbound-measurement.mjs', import.meta.url), 'utf8')
|
||||||
|
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||||
|
const labelTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java')
|
||||||
|
const measurementTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java')
|
||||||
|
const cartonTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java')
|
||||||
|
const reviewTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java')
|
||||||
|
|
||||||
test('measurement acceptance scans the stable box code frozen by first print', () => {
|
test('measurement acceptance scans the stable box code frozen by first print', () => {
|
||||||
assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/)
|
assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/)
|
||||||
@@ -32,3 +37,37 @@ test('measurement fixture derives pallet resources from confirmed image count',
|
|||||||
test('volume ceiling removes binary floating point noise before scaling', () => {
|
test('volume ceiling removes binary floating point noise before scaling', () => {
|
||||||
assert.match(source, /Number\.EPSILON \* Math\.max\(1, Math\.abs\(value\)\) \* 4/)
|
assert.match(source, /Number\.EPSILON \* Math\.max\(1, Math\.abs\(value\)\) \* 4/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('demo package freezes repeated label printing and Mojia dimension replacement', () => {
|
||||||
|
assert.match(labelTests, /repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation/)
|
||||||
|
assert.match(labelTests, /changedCargoKeepsTheFirstFrozenBoxCodes/)
|
||||||
|
assert.match(cartonTests, /repeatedScanReplacesDeviceValuesButPreservesManualCountAndOldImageIsDeleted/)
|
||||||
|
assert.match(source, /增量 fixture 形成动态真实独立货物测量明细/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('demo package covers OCR failure, manual confirmation, aggregation and stale versions', () => {
|
||||||
|
assert.match(source, /OCR无有效重量时转人工确认/)
|
||||||
|
assert.match(source, /称重旧版本确认被拒绝且不覆盖已确认结果/)
|
||||||
|
assert.match(source, /图片张数决定托数且净重汇总已落地/)
|
||||||
|
assert.match(measurementTests, /ocrFailureCreatesManualWeightThatCanBeAdjustedAndConfirmed/)
|
||||||
|
assert.match(measurementTests, /confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight/)
|
||||||
|
assert.match(measurementTests, /confirmingWithStaleVersionDoesNotTreatRequestAsIdempotent/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('demo package keeps pallet pages bounded and historical snapshots read-only', () => {
|
||||||
|
assert.match(source, /weight-pallet-options[^\n]+page=1&size=20&keyword=/)
|
||||||
|
assert.match(source, /records\.length <= 20/)
|
||||||
|
assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('approval freezes measurement and requires quantity weight and volume conservation', () => {
|
||||||
|
assert.match(source, /审核进入WAIT_SPLIT后箱码尺寸与图片快照不可再覆盖/)
|
||||||
|
assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/)
|
||||||
|
assert.match(reviewTests, /shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch/)
|
||||||
|
assert.match(reviewTests, /shouldRejectApprovalWhenVolumeSnapshotDoesNotConserve/)
|
||||||
|
assert.match(source, /actualPackageCount[^\n]+originalPackageCount/)
|
||||||
|
assert.match(source, /actualGrossWeightKg[^\n]+weightEvidence\.grossWeightKg/)
|
||||||
|
assert.match(source, /actualNetWeightKg[^\n]+weightEvidence\.netWeightKg/)
|
||||||
|
assert.match(source, /actualVolumeCbm[^\n]+totalVolume/)
|
||||||
|
assert.match(source, /volumeWeightKg[^\n]+volumeWeight/)
|
||||||
|
})
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ async function waitForCompletedMeasurement(context, measurementPath, attempts =
|
|||||||
return { response, measurement, attempts }
|
return { response, measurement, attempts }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function provisionWarehouseResources(context, { inboundId, warehouseId, palletCount }) {
|
async function provisionWarehouseResources(context, { inboundId, tenantId, warehouseId, palletCount }) {
|
||||||
if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null
|
if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null
|
||||||
|
|
||||||
const resources = []
|
const resources = []
|
||||||
@@ -92,6 +92,20 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa
|
|||||||
if (!candidates) return null
|
if (!candidates) return null
|
||||||
const palletIds = randomlyReusePalletIds(candidates, palletCount,
|
const palletIds = randomlyReusePalletIds(candidates, palletCount,
|
||||||
context.context?.waitSplitVariant?.randomValue)
|
context.context?.waitSplitVariant?.randomValue)
|
||||||
|
const candidateKeyword = candidates.find((item) => item.id === palletIds[0])?.code || ''
|
||||||
|
const candidateResponse = await context.request('GET',
|
||||||
|
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-pallet-options?${query(tenantId)}&page=1&size=20&keyword=${encodeURIComponent(candidateKeyword)}`)
|
||||||
|
const candidatePage = context.apiSuccess(MODULE, '按关键词分页查询当前入库仓称重托盘候选', candidateResponse,
|
||||||
|
`warehouseId=${warehouseId}, keyword=${candidateKeyword}`, OPTIONS)
|
||||||
|
const pageControlled = Array.isArray(candidatePage?.records) && candidatePage.records.length <= 20
|
||||||
|
&& Number(candidatePage?.page) === 1 && Number(candidatePage?.size) === 20
|
||||||
|
&& Number(candidatePage?.total) >= candidatePage.records.length
|
||||||
|
&& candidatePage.records.some((item) => asId(item?.palletId) === palletIds[0])
|
||||||
|
context.assert(MODULE, '托盘候选保持服务端关键词分页且不返回无界数组', candidateResponse,
|
||||||
|
pageControlled,
|
||||||
|
`records=${candidatePage?.records?.length ?? 'missing'}, page=${candidatePage?.page ?? 'missing'}, size=${candidatePage?.size ?? 'missing'}, total=${candidatePage?.total ?? 'missing'}`,
|
||||||
|
OPTIONS)
|
||||||
|
if (!pageControlled) return null
|
||||||
for (let index = 1; index <= palletCount; index += 1) {
|
for (let index = 1; index <= palletCount; index += 1) {
|
||||||
const palletId = palletIds[index - 1]
|
const palletId = palletIds[index - 1]
|
||||||
context.assert(MODULE, `随机复用原有称重托盘 ${index}/${palletCount}`, null,
|
context.assert(MODULE, `随机复用原有称重托盘 ${index}/${palletCount}`, null,
|
||||||
@@ -134,6 +148,10 @@ async function createConfirmedWeightEvidence(context, {
|
|||||||
const created = context.apiSuccess(MODULE, `登记称重图片与托盘快照 ${index + 1}/${palletIds.length}`,
|
const created = context.apiSuccess(MODULE, `登记称重图片与托盘快照 ${index + 1}/${palletIds.length}`,
|
||||||
createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
||||||
if (!created?.id) return null
|
if (!created?.id) return null
|
||||||
|
context.assert(MODULE, `OCR无有效重量时转人工确认 ${index + 1}/${palletIds.length}`, createResponse,
|
||||||
|
created.recognitionStatus === 'NEEDS_MANUAL' && created.ocrWeightValue == null,
|
||||||
|
`recognitionStatus=${created.recognitionStatus || 'missing'}, ocrWeightValue=${created.ocrWeightValue ?? 'null'}`,
|
||||||
|
OPTIONS)
|
||||||
|
|
||||||
const grossWeight = Number(grossWeightPerPalletKg) + index * 5
|
const grossWeight = Number(grossWeightPerPalletKg) + index * 5
|
||||||
const confirmResponse = await context.request('POST',
|
const confirmResponse = await context.request('POST',
|
||||||
@@ -148,6 +166,16 @@ async function createConfirmedWeightEvidence(context, {
|
|||||||
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
|
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
const staleConfirmResponse = await context.request('POST',
|
||||||
|
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
|
||||||
|
version: Number(created.version), adjustedWeightValue: grossWeight + 1,
|
||||||
|
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 旧版本冲突验证`,
|
||||||
|
})
|
||||||
|
context.assert(MODULE, `称重旧版本确认被拒绝且不覆盖已确认结果 ${index + 1}/${palletIds.length}`,
|
||||||
|
staleConfirmResponse,
|
||||||
|
staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409',
|
||||||
|
`HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`,
|
||||||
|
OPTIONS)
|
||||||
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
|
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +189,9 @@ async function createConfirmedWeightEvidence(context, {
|
|||||||
const netWeightKg = confirmedResults.reduce((sum, result) => sum + result.netWeightKg, 0)
|
const netWeightKg = confirmedResults.reduce((sum, result) => sum + result.netWeightKg, 0)
|
||||||
const persisted = confirmedRows.length === palletIds.length
|
const persisted = confirmedRows.length === palletIds.length
|
||||||
&& confirmedResults.every((expected) => confirmedRows.some((row) =>
|
&& confirmedResults.every((expected) => confirmedRows.some((row) =>
|
||||||
asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId))
|
asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId
|
||||||
|
&& String(row?.palletCode || '').trim() && String(row?.palletType || '').trim()
|
||||||
|
&& Number.isFinite(Number(row?.palletTareWeightKg))))
|
||||||
&& closeTo(workspace?.actualGrossWeightKg, grossWeightKg, 0.001)
|
&& closeTo(workspace?.actualGrossWeightKg, grossWeightKg, 0.001)
|
||||||
&& closeTo(workspace?.actualNetWeightKg, netWeightKg, 0.001)
|
&& closeTo(workspace?.actualNetWeightKg, netWeightKg, 0.001)
|
||||||
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
||||||
@@ -533,7 +563,7 @@ export async function run(context) {
|
|||||||
|
|
||||||
const palletCount = Math.max(1, Number(context.context?.waitSplitVariant?.palletCount || 1))
|
const palletCount = Math.max(1, Number(context.context?.waitSplitVariant?.palletCount || 1))
|
||||||
const warehouseResourcesReady = await provisionWarehouseResources(context, {
|
const warehouseResourcesReady = await provisionWarehouseResources(context, {
|
||||||
inboundId, warehouseId: asId(summary?.warehouseId), palletCount,
|
inboundId, tenantId, warehouseId: asId(summary?.warehouseId), palletCount,
|
||||||
})
|
})
|
||||||
context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse,
|
context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse,
|
||||||
Boolean(warehouseResourcesReady),
|
Boolean(warehouseResourcesReady),
|
||||||
@@ -606,6 +636,15 @@ export async function run(context) {
|
|||||||
&& closeTo(finalMeasurement?.volumeWeightKg, volumeWeight),
|
&& closeTo(finalMeasurement?.volumeWeightKg, volumeWeight),
|
||||||
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}, pallets=${weightEvidence.count}, grossWeight=${finalMeasurement?.actualGrossWeightKg ?? 'missing'}, netWeight=${finalMeasurement?.actualNetWeightKg ?? 'missing'}`, OPTIONS)
|
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}, pallets=${weightEvidence.count}, grossWeight=${finalMeasurement?.actualGrossWeightKg ?? 'missing'}, netWeight=${finalMeasurement?.actualNetWeightKg ?? 'missing'}`, OPTIONS)
|
||||||
|
|
||||||
|
const approvedScanResponse = await multipartRequest(context, scanPath, {
|
||||||
|
boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-AFTER-APPROVAL',
|
||||||
|
measuredAt: new Date().toISOString().slice(0, 19),
|
||||||
|
})
|
||||||
|
context.assert(MODULE, '审核进入WAIT_SPLIT后箱码尺寸与图片快照不可再覆盖', approvedScanResponse,
|
||||||
|
approvedScanResponse.http === 200 && String(approvedScanResponse.body?.code) === 'A0409',
|
||||||
|
`HTTP=${approvedScanResponse.http}, code=${approvedScanResponse.body?.code ?? 'missing'}, requestId=${approvedScanResponse.requestId || 'missing'}`,
|
||||||
|
OPTIONS)
|
||||||
|
|
||||||
context.createdResources.inboundMeasurement = {
|
context.createdResources.inboundMeasurement = {
|
||||||
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
||||||
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user