Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
11 changed files with 253 additions and 80 deletions
Showing only changes of commit cca1bc002e - Show all commits
@@ -2657,7 +2657,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
total_carton_count INT UNSIGNED NOT NULL COMMENT '首次分配时整单总箱数',
carton_index INT UNSIGNED NOT NULL COMMENT '整单从1开始的稳定箱下标',
label_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '箱唛批次版本,从1递增',
label_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '内部兼容批次字段,固定为1且不进入箱码',
batch_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '批次状态:ACTIVE有效、VOID已作废',
cargo_fingerprint CHAR(64) NULL COMMENT '影响标签字段的SHA-256指纹',
voided_at DATETIME(3) NULL COMMENT '批次作废时间',
@@ -2674,7 +2674,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
CONSTRAINT fk_carton_label_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
) ENGINE=InnoDB COMMENT='首次打印前冻结的实际箱号与订单货物明细映射';
-- 存量环境可能已在箱唛版本化前建表;CREATE TABLE IF NOT EXISTS 不会补列。
-- 存量环境可能缺少稳定箱码映射兼容字段;CREATE TABLE IF NOT EXISTS 不会补列。
DROP PROCEDURE IF EXISTS unified_carton_label_add_column;
DELIMITER $$
CREATE PROCEDURE unified_carton_label_add_column(IN p_column VARCHAR(64), IN p_definition TEXT)
@@ -2688,7 +2688,7 @@ BEGIN
END IF;
END$$
DELIMITER ;
CALL unified_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''箱唛批次版本,从1递增'' AFTER carton_index');
CALL unified_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''内部兼容批次字段,固定为1且不进入箱码'' AFTER carton_index');
CALL unified_carton_label_add_column('batch_status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''批次状态:ACTIVE有效、VOID已作废'' AFTER label_version');
CALL unified_carton_label_add_column('cargo_fingerprint', 'CHAR(64) NULL COMMENT ''影响标签字段的SHA-256指纹'' AFTER batch_status');
CALL unified_carton_label_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''批次作废时间'' AFTER cargo_fingerprint');
@@ -2699,9 +2699,12 @@ UPDATE fl_inbound_carton_label
batch_status='ACTIVE'
WHERE label_version IS NULL OR batch_status IS NULL;
DELETE FROM fl_inbound_carton_label WHERE batch_status='VOID';
UPDATE fl_inbound_carton_label
SET box_code=CONCAT(inbound_code, '#V', label_version, '#', total_carton_count, '-', carton_index)
WHERE box_code NOT LIKE CONCAT(inbound_code, '#V%#%');
SET label_version=1,
box_code=CONCAT(inbound_code, '#', total_carton_count, '-', carton_index)
WHERE batch_status='ACTIVE';
SET @ddl = IF(
EXISTS(SELECT 1 FROM information_schema.statistics
@@ -13760,7 +13763,7 @@ BEGIN
AND table_name = 'fl_inbound_carton_label'
AND column_name IN ('label_version', 'batch_status', 'cargo_fingerprint', 'voided_at')) <> 4 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '统一迁移失败:箱唛版本化字段未完整创建';
SET MESSAGE_TEXT = '统一迁移失败:稳定箱码映射兼容字段未完整创建';
END IF;
IF (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = 'baiyue_firstleg'
@@ -52,25 +52,6 @@ public class InboundCartonLabelRepository {
.eq(InboundCartonLabelEntity::getBoxCode, boxCode));
}
public int nextVersion(long tenantId, long inboundId) {
InboundCartonLabelEntity latest = labels.selectOne(new LambdaQueryWrapper<InboundCartonLabelEntity>()
.select(InboundCartonLabelEntity::getLabelVersion)
.eq(InboundCartonLabelEntity::getTenantId, tenantId)
.eq(InboundCartonLabelEntity::getInboundId, inboundId)
.orderByDesc(InboundCartonLabelEntity::getLabelVersion)
.last("LIMIT 1"));
return latest == null || latest.getLabelVersion() == null ? 1 : latest.getLabelVersion() + 1;
}
public void voidActive(long tenantId, long inboundId, java.time.LocalDateTime at) {
labels.update(null, new LambdaUpdateWrapper<InboundCartonLabelEntity>()
.eq(InboundCartonLabelEntity::getTenantId, tenantId)
.eq(InboundCartonLabelEntity::getInboundId, inboundId)
.eq(InboundCartonLabelEntity::getBatchStatus, "ACTIVE")
.set(InboundCartonLabelEntity::getBatchStatus, "VOID")
.set(InboundCartonLabelEntity::getVoidedAt, at));
}
public void initializeLegacyFingerprint(long tenantId, long inboundId, int version, String fingerprint) {
labels.update(null, new LambdaUpdateWrapper<InboundCartonLabelEntity>()
.eq(InboundCartonLabelEntity::getTenantId, tenantId)
@@ -59,8 +59,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
return batch(inbound, consistent);
}
if (existingFingerprint.equals(fingerprint)) return batch(inbound, consistent);
repository.voidActive(tenantId, inboundId, LocalDateTime.now());
existing.clear();
return batch(inbound, consistent);
}
int total = cargoes.stream().mapToInt(this::requirePackageCount).sum();
if (total < 1 || total > MAX_LABELS) {
@@ -68,8 +67,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
}
repository.updatePackageCount(tenantId, inboundId, total);
inbound.setTotalPackageCount(total);
int labelVersion = repository.nextVersion(tenantId, inboundId);
requirePrintableInboundCode(inbound.getInboundCode(), total, labelVersion);
requirePrintableInboundCode(inbound.getInboundCode(), total);
int cartonIndex = 0;
LocalDateTime now = LocalDateTime.now();
@@ -77,7 +75,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
int detailCount = requirePackageCount(cargo);
for (int offset = 0; offset < detailCount; offset++) {
cartonIndex++;
InboundCartonLabelEntity label = label(inbound, cargo, total, cartonIndex, labelVersion, fingerprint, now);
InboundCartonLabelEntity label = label(inbound, cargo, total, cartonIndex, fingerprint, now);
repository.insert(label);
existing.add(label);
}
@@ -148,7 +146,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
InboundCartonLabelEntity label = repository.findByBoxCode(tenantId, boxCode.trim());
if (label == null) throw new BizException(ResultCode.NOT_FOUND, "箱号不存在");
if ("VOID".equals(label.getBatchStatus())) {
throw new BizException(ResultCode.CONFLICT, "箱唛版本已作废,请重新打印");
throw new BizException(ResultCode.CONFLICT, "箱码已失效,请重新打印");
}
return label;
}
@@ -182,11 +180,11 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
return cargo.packageCount();
}
private void requirePrintableInboundCode(String inboundCode, int total, int version) {
private void requirePrintableInboundCode(String inboundCode, int total) {
if (!StringUtils.hasText(inboundCode) || !inboundCode.matches("[A-Za-z0-9._:-]+")) {
throw new BizException(ResultCode.VALIDATION_FAILED, "入仓码不能生成可打印二维码");
}
String longest = inboundCode + "#V" + version + "#" + total + "-" + total;
String longest = inboundCode + "#" + total + "-" + total;
if (longest.codePointCount(0, longest.length()) > MAX_QR_CODE_POINTS) {
throw new BizException(ResultCode.VALIDATION_FAILED, "入仓码和箱号组合超过二维码长度限制");
}
@@ -194,7 +192,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
private InboundCartonLabelEntity label(InboundOrderEntity inbound,
OrderCargoClient.CargoLabelBinding cargo,
int total, int index, int labelVersion, String fingerprint,
int total, int index, String fingerprint,
LocalDateTime now) {
InboundCartonLabelEntity label = new InboundCartonLabelEntity();
label.setId(IdWorker.getId());
@@ -206,10 +204,10 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
label.setCargoName(cargo.cargoName());
label.setDetailPackageCount(cargo.packageCount());
label.setInboundCode(inbound.getInboundCode());
label.setBoxCode(inbound.getInboundCode() + "#V" + labelVersion + "#" + total + "-" + index);
label.setBoxCode(inbound.getInboundCode() + "#" + total + "-" + index);
label.setTotalCartonCount(total);
label.setCartonIndex(index);
label.setLabelVersion(labelVersion);
label.setLabelVersion(1);
label.setBatchStatus("ACTIVE");
label.setCargoFingerprint(fingerprint);
label.setLengthCm(cargo.lengthCm());
@@ -12,7 +12,7 @@ BEGIN
END IF;
END$$
DELIMITER ;
CALL phase2_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''箱唛批次版本,从1递增'' AFTER carton_index');
CALL phase2_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''内部兼容批次字段,固定为1且不进入箱码'' AFTER carton_index');
CALL phase2_carton_label_add_column('batch_status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''批次状态:ACTIVE有效、VOID已作废'' AFTER label_version');
CALL phase2_carton_label_add_column('cargo_fingerprint', 'CHAR(64) NULL COMMENT ''影响标签字段的SHA-256指纹'' AFTER batch_status');
CALL phase2_carton_label_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''批次作废时间'' AFTER cargo_fingerprint');
@@ -23,9 +23,12 @@ UPDATE fl_inbound_carton_label
batch_status = 'ACTIVE'
WHERE label_version IS NULL OR batch_status IS NULL;
DELETE FROM fl_inbound_carton_label WHERE batch_status = 'VOID';
UPDATE fl_inbound_carton_label
SET box_code = CONCAT(inbound_code, '#V', label_version, '#', total_carton_count, '-', carton_index)
WHERE box_code NOT LIKE CONCAT(inbound_code, '#V%#%');
SET label_version = 1,
box_code = CONCAT(inbound_code, '#', total_carton_count, '-', carton_index)
WHERE batch_status = 'ACTIVE';
SET @has_old_index := (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_carton_label'
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
total_carton_count INT UNSIGNED NOT NULL COMMENT '首次分配时整单总箱数',
carton_index INT UNSIGNED NOT NULL COMMENT '整单从1开始的稳定箱下标',
label_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '箱唛批次版本,从1递增',
label_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '内部兼容批次字段,固定为1且不进入箱码',
batch_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '批次状态:ACTIVE有效、VOID已作废',
cargo_fingerprint CHAR(64) NULL COMMENT '影响标签字段的SHA-256指纹',
voided_at DATETIME(3) NULL COMMENT '批次作废时间',
@@ -153,7 +153,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
total_carton_count INT UNSIGNED NOT NULL COMMENT '首次分配时整单总箱数',
carton_index INT UNSIGNED NOT NULL COMMENT '整单从1开始的稳定箱下标',
label_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '箱唛批次版本,从1递增',
label_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '内部兼容批次字段,固定为1且不进入箱码',
batch_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '批次状态:ACTIVE有效、VOID已作废',
cargo_fingerprint CHAR(64) NULL COMMENT '影响标签字段的SHA-256指纹',
voided_at DATETIME(3) NULL COMMENT '批次作废时间',
@@ -38,7 +38,7 @@ class InboundCartonLabelSqlContractTest {
.contains("uk_carton_label_box_code")
.contains("uk_carton_label_index")
.contains("稳定箱号映射表未创建")
.contains("箱唛版本化字段未完整创建")
.contains("稳定箱码映射兼容字段未完整创建")
.contains("箱唛有效批次索引未完整创建");
}
@@ -50,7 +50,6 @@ class InboundCartonLabelServiceTest {
var first = cargo(80001L, 1, 3, "60", "40", "30");
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
.thenReturn(ApiResponse.success(List.of(second, first)));
when(repository.nextVersion(10001L, 70001L)).thenReturn(1);
List<InboundCartonLabelEntity> inserted = new ArrayList<>();
org.mockito.Mockito.doAnswer(invocation -> {
inserted.add(invocation.getArgument(0));
@@ -61,8 +60,8 @@ class InboundCartonLabelServiceTest {
assertThat(batch.totalCount()).isEqualTo(5);
assertThat(batch.labels()).extracting(label -> label.boxCode())
.containsExactly("PCHK2608040174#V1#5-1", "PCHK2608040174#V1#5-2", "PCHK2608040174#V1#5-3",
"PCHK2608040174#V1#5-4", "PCHK2608040174#V1#5-5");
.containsExactly("PCHK2608040174#5-1", "PCHK2608040174#5-2", "PCHK2608040174#5-3",
"PCHK2608040174#5-4", "PCHK2608040174#5-5");
assertThat(batch.labels()).extracting(label -> label.inboundDetailId())
.containsExactly(80001L, 80001L, 80001L, 80002L, 80002L);
assertThat(batch.labels().get(0).boxSpecification()).isEqualTo("60x40x30cm");
@@ -73,7 +72,7 @@ class InboundCartonLabelServiceTest {
}
@Test
void repeatedAndPartialPrintsReadCurrentVersionWithoutReallocation() {
void repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation() {
InboundOrderEntity inbound = inbound();
inbound.setTotalPackageCount(5);
List<InboundCartonLabelEntity> existing = labels(inbound, 5);
@@ -92,7 +91,7 @@ class InboundCartonLabelServiceTest {
}
@Test
void changedCargoCreatesNewVersion() {
void changedCargoKeepsTheFirstFrozenBoxCodes() {
InboundOrderEntity inbound = inbound();
inbound.setTotalPackageCount(4);
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
@@ -101,13 +100,11 @@ class InboundCartonLabelServiceTest {
when(repository.findActiveByInbound(10001L, 70001L)).thenReturn(existing);
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
.thenReturn(ApiResponse.success(List.of(cargo(80001L, 1, 2, "60", "40", "30"))));
when(repository.nextVersion(10001L, 70001L)).thenReturn(2);
org.mockito.Mockito.doAnswer(invocation -> null).when(repository).insert(any());
var result = service.getOrCreate(10001L, 70001L);
assertThat(result.labels()).extracting(label -> label.boxCode())
.containsExactly("PCHK2608040174#V2#2-1", "PCHK2608040174#V2#2-2");
verify(repository).voidActive(org.mockito.ArgumentMatchers.eq(10001L),
org.mockito.ArgumentMatchers.eq(70001L), any());
.containsExactly("PCHK2608040174#5-1", "PCHK2608040174#5-2", "PCHK2608040174#5-3",
"PCHK2608040174#5-4", "PCHK2608040174#5-5");
verify(repository, never()).insert(any());
}
@Test
@@ -119,14 +116,13 @@ class InboundCartonLabelServiceTest {
when(repository.findByInbound(10001L, 70001L)).thenReturn(new ArrayList<>());
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
.thenReturn(ApiResponse.success(List.of(cargo(80001L, 1, 2, "60", "40", "30"))));
when(repository.nextVersion(10001L, 70001L)).thenReturn(1);
org.mockito.Mockito.doAnswer(invocation -> null).when(repository).insert(any());
var result = service.getOrCreate(10001L, 70001L);
assertThat(result.totalCount()).isEqualTo(2);
assertThat(result.labels()).extracting(label -> label.boxCode())
.containsExactly("PCHK2608040174#V1#2-1", "PCHK2608040174#V1#2-2");
.containsExactly("PCHK2608040174#2-1", "PCHK2608040174#2-2");
}
@Test
@@ -143,7 +139,7 @@ class InboundCartonLabelServiceTest {
}
@Test
void changedCargoAfterLoadingVoidsCurrentBatchAndCreatesNextVersion() {
void changedCargoAfterLoadingKeepsTheFirstFrozenBoxCodes() {
InboundOrderEntity inbound = inbound();
inbound.setProcessStatus("LOADING");
List<InboundCartonLabelEntity> existing = labels(inbound, 1);
@@ -155,22 +151,19 @@ class InboundCartonLabelServiceTest {
when(repository.findActiveByInbound(10001L, 70001L)).thenReturn(existing);
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
.thenReturn(ApiResponse.success(List.of(cargo(80001L, 1, 2, "60", "40", "30"))));
when(repository.nextVersion(10001L, 70001L)).thenReturn(2);
var result = service.getOrCreate(10001L, 70001L);
assertThat(result.labels()).extracting(label -> label.boxCode())
.containsExactly("PCHK2608040174#V2#2-1", "PCHK2608040174#V2#2-2");
verify(repository).voidActive(org.mockito.ArgumentMatchers.eq(10001L),
org.mockito.ArgumentMatchers.eq(70001L), any());
.containsExactly("PCHK2608040174#1-1");
verify(repository, never()).insert(any());
}
@Test
void resolvesBoxCodeInsideCustomerBoundary() {
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
when(repository.findByBoxCode(10001L, "PCHK2608040174#V1#1-1")).thenReturn(label);
when(repository.findByBoxCode(10001L, "PCHK2608040174#1-1")).thenReturn(label);
var resolved = service.resolveByBoxCode(10001L, " PCHK2608040174#V1#1-1 ");
var resolved = service.resolveByBoxCode(10001L, " PCHK2608040174#1-1 ");
assertThat(resolved.inboundId()).isEqualTo(70001L);
assertThat(resolved.inboundDetailId()).isEqualTo(80001L);
@@ -178,30 +171,30 @@ class InboundCartonLabelServiceTest {
assertThat(resolved.cartonIndex()).isEqualTo(1);
assertThat(resolved.totalCartons()).isEqualTo(1);
assertThat(resolved.boxSpecification()).isEqualTo("60x40x30cm");
verify(repository).findByBoxCode(10001L, "PCHK2608040174#V1#1-1");
verify(repository).findByBoxCode(10001L, "PCHK2608040174#1-1");
}
@Test
void exposesDetailedResolutionForFirstlegConsumers() {
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
when(repository.findByBoxCode(10001L, "PCHK2608040174#V1#1-1")).thenReturn(label);
when(repository.findByBoxCode(10001L, "PCHK2608040174#1-1")).thenReturn(label);
var resolved = service.resolveDetailsByBoxCode(10001L, "PCHK2608040174#V1#1-1");
var resolved = service.resolveDetailsByBoxCode(10001L, "PCHK2608040174#1-1");
assertThat(resolved.orderId()).isEqualTo(60001L);
assertThat(resolved.boxCode()).isEqualTo("PCHK2608040174#V1#1-1");
assertThat(resolved.boxCode()).isEqualTo("PCHK2608040174#1-1");
assertThat(resolved.cargoItemNo()).isEqualTo(1);
}
@Test
void rejectsVoidedLabelVersionDuringScanResolution() {
void rejectsInactiveBoxCodeDuringScanResolution() {
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
label.setBatchStatus("VOID");
when(repository.findByBoxCode(10001L, "PCHK2608040174#V1#1-1")).thenReturn(label);
when(repository.findByBoxCode(10001L, "PCHK2608040174#1-1")).thenReturn(label);
assertThatThrownBy(() -> service.resolveByBoxCode(10001L, "PCHK2608040174#V1#1-1"))
assertThatThrownBy(() -> service.resolveByBoxCode(10001L, "PCHK2608040174#1-1"))
.isInstanceOf(BizException.class)
.hasMessage("箱唛版本已作废,请重新打印");
.hasMessage("箱码已失效,请重新打印");
}
@Test
@@ -284,7 +277,7 @@ class InboundCartonLabelServiceTest {
label.setCargoName("货物1");
label.setDetailPackageCount(total);
label.setInboundCode(inbound.getInboundCode());
label.setBoxCode(inbound.getInboundCode() + "#V1#" + total + "-" + index);
label.setBoxCode(inbound.getInboundCode() + "#" + total + "-" + index);
label.setTotalCartonCount(total);
label.setCartonIndex(index);
label.setLabelVersion(1);
@@ -0,0 +1,15 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const source = readFileSync(new URL('inbound-measurement.mjs', import.meta.url), 'utf8')
test('measurement acceptance scans the stable box code frozen by first print', () => {
assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/)
assert.doesNotMatch(source, /#V\d+#/)
})
test('reprint and Mojia scan share the same frozen box code variable', () => {
assert.match(source, /重复整单打印复用已冻结箱码映射[\s\S]*boxCode=\$\{boxCode\}/)
assert.match(source, /const firstScanResponse = await multipartRequest[\s\S]*boxCode, \.\.\.firstDimensions/)
})
@@ -0,0 +1,91 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { findPricedOrderRoute } from './orders.mjs'
const source = readFileSync(new URL('orders.mjs', import.meta.url), 'utf8')
test('orders fixture verifies the complete pricing tuple before selecting an FBA warehouse', () => {
assert.match(source, /\/api\/merchant\/v1\/first-leg\/pricing\/estimate/)
for (const field of ['originWarehouseId', 'channelCode', 'destinationCode', 'destinationType',
'destinationWarehouseId', 'pricingTaxMode', 'actualWeightKg', 'actualVolumeCbm']) {
assert.match(source, new RegExp(`${field}:`))
}
assert.match(source, /estimateMatches\(response, candidate\)/)
assert.doesNotMatch(source, /const destination = records\(fbaPage\)\[0\]/)
})
test('order payload reuses the tax mode from the successful estimate', () => {
assert.match(source, /pricingTaxMode: master\.pricingTaxMode/)
assert.doesNotMatch(source, /pricingTaxMode: master\.channel\.pricingTaxModes\[0\]/)
assert.match(source, /没有可计价组合/)
})
test('pricing route accepts only an estimate matching the active version channel and tax tuple', async () => {
const requests = []
const result = await findPricedOrderRoute({
receiving: { id: '100' },
destinations: [
{ id: '201', warehouseCode: 'ABE2', warehouseName: 'ABE2' },
{ id: '202', warehouseCode: 'ABE3', warehouseName: 'ABE3' },
],
channels: [{ priceBookVersionId: '300', channelCode: 'EXX_RAIL_NY',
pricingTaxModes: ['TAX_INCLUDED'] }],
concurrency: 1,
estimate: async (request) => {
requests.push(request)
if (request.destinationCode === 'ABE2') {
return { http: 200, body: { code: 'A0404' }, requestId: 'request-abe2', traceId: 'trace-abe2' }
}
return { http: 200, body: { code: '200', data: {
priceBookVersionId: '300', channelCode: 'EXX_RAIL_NY', pricingTaxMode: 'TAX_INCLUDED',
} } }
},
})
assert.equal(result.priced.destination.warehouseCode, 'ABE3')
assert.equal(result.attempts, 2)
assert.deepEqual(requests[1], {
originWarehouseId: '100', channelCode: 'EXX_RAIL_NY', destinationCode: 'ABE3',
destinationType: 'FBA', destinationWarehouseId: '202', destinationName: 'ABE3',
pricingTaxMode: 'TAX_INCLUDED', actualWeightKg: 46, actualVolumeCbm: 0.278,
})
assert.equal(result.failures[0].requestId, 'request-abe2')
})
test('pricing route rejects a nominal success from a different version or tuple', async () => {
const result = await findPricedOrderRoute({
receiving: { id: '100' },
destinations: [{ id: '202', warehouseCode: 'ABE3', warehouseName: 'ABE3' }],
channels: [{ priceBookVersionId: '300', channelCode: 'EXX_RAIL_NY',
pricingTaxModes: ['TAX_INCLUDED'] }],
estimate: async () => ({ http: 200, body: { code: '200', data: {
priceBookVersionId: 'old-version', channelCode: 'OTHER', pricingTaxMode: 'TAX_EXCLUDED',
} }, requestId: 'request-drift', traceId: 'trace-drift' }),
})
assert.equal(result.priced, null)
assert.equal(result.attempts, 1)
assert.match(result.failures[0].mismatch, /version=old-version,channel=OTHER,tax=TAX_EXCLUDED/)
})
test('pricing route bounds parallel probes and keeps deterministic candidate selection', async () => {
let active = 0; let maximumActive = 0
const result = await findPricedOrderRoute({
receiving: { id: '100' }, concurrency: 50,
destinations: Array.from({ length: 8 }, (_, index) => ({
id: String(200 + index), warehouseCode: `FBA${index}`, warehouseName: `FBA ${index}`,
})),
channels: [{ priceBookVersionId: '300', channelCode: 'CHANNEL', pricingTaxModes: ['TAX_INCLUDED'] }],
estimate: async (request) => {
active += 1; maximumActive = Math.max(maximumActive, active)
await new Promise((resolve) => setTimeout(resolve, request.destinationCode === 'FBA0' ? 5 : 1))
active -= 1
return { http: 200, body: { code: '200', data: {
priceBookVersionId: '300', channelCode: 'CHANNEL', pricingTaxMode: 'TAX_INCLUDED',
} } }
},
})
assert.equal(maximumActive, 6)
assert.equal(result.priced.destination.warehouseCode, 'FBA0')
assert.equal(result.attempts, 6)
})
+98 -9
View File
@@ -9,6 +9,10 @@ const BOXES = [
{ boxType: 'EXTRA_LARGE', lengthCm: 60, widthCm: 50, heightCm: 50 },
]
const MEASURABLE_STATUSES = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED'])
const PRICING_PROBE_CONCURRENCY = 6
const ORDER_ESTIMATE_WEIGHT_KG = BOXES.reduce((total, _box, index) => total + 10 + index, 0)
const ORDER_ESTIMATE_VOLUME_CBM = BOXES.reduce((total, box) =>
total + box.lengthCm * box.widthCm * box.heightCm / 1_000_000, 0)
function id(value) { return value == null ? '' : String(value) }
function records(value) { return Array.isArray(value?.records) ? value.records : [] }
@@ -29,6 +33,77 @@ function tomorrow(days = 30) {
return date.toISOString().slice(0, 10)
}
function pricingEstimateRequest(receiving, channel, pricingTaxMode, destination) {
return {
originWarehouseId: receiving.id,
channelCode: channel.channelCode,
destinationCode: destination.warehouseCode,
destinationType: 'FBA',
destinationWarehouseId: destination.id,
destinationName: destination.warehouseName,
pricingTaxMode,
actualWeightKg: ORDER_ESTIMATE_WEIGHT_KG,
actualVolumeCbm: ORDER_ESTIMATE_VOLUME_CBM,
}
}
function estimateMatches(response, candidate) {
const estimate = success(response) ? response.body?.data : null
return Boolean(estimate)
&& id(estimate.priceBookVersionId) === id(candidate.channel.priceBookVersionId)
&& id(estimate.channelCode) === id(candidate.channel.channelCode)
&& id(estimate.pricingTaxMode) === id(candidate.pricingTaxMode)
}
export async function findPricedOrderRoute({ receiving, destinations, channels, estimate,
concurrency = PRICING_PROBE_CONCURRENCY }) {
const candidates = []
for (const channel of channels) {
if (!channel?.priceBookVersionId || !channel?.channelCode || !Array.isArray(channel.pricingTaxModes)) continue
for (const pricingTaxMode of channel.pricingTaxModes) {
if (!pricingTaxMode) continue
for (const destination of destinations) {
if (!destination?.id || !destination?.warehouseCode) continue
candidates.push({ channel, pricingTaxMode, destination,
request: pricingEstimateRequest(receiving, channel, pricingTaxMode, destination) })
}
}
}
const failures = []
let lastResponse = null
const batchSize = Math.max(1, Math.min(Number(concurrency) || 1, PRICING_PROBE_CONCURRENCY))
let attempts = 0
for (let offset = 0; offset < candidates.length; offset += batchSize) {
const batch = candidates.slice(offset, offset + batchSize)
const results = await Promise.all(batch.map(async (candidate) => ({
candidate, response: await estimate(candidate.request),
})))
attempts += results.length
lastResponse = results.at(-1)?.response || lastResponse
const match = results.find(({ candidate, response }) => estimateMatches(response, candidate))
if (match) return { priced: match.candidate, response: match.response, attempts, failures }
for (const { candidate, response } of results) {
const returned = success(response) ? response.body?.data : null
failures.push({
tuple: `${candidate.channel.channelCode}/${candidate.pricingTaxMode}/${candidate.destination.warehouseCode}`,
http: response?.http ?? 'missing', code: response?.body?.code ?? 'missing',
requestId: response?.requestId ?? 'missing', traceId: response?.traceId ?? 'missing',
mismatch: returned
? `version=${id(returned.priceBookVersionId) || 'missing'},channel=${id(returned.channelCode) || 'missing'},tax=${id(returned.pricingTaxMode) || 'missing'}`
: null,
})
}
}
return { priced: null, response: lastResponse, attempts, failures }
}
function pricingFailureEvidence(result) {
const describe = (failure) => failure
? `${failure.tuple}[HTTP=${failure.http},code=${failure.code},requestId=${failure.requestId},traceId=${failure.traceId}${failure.mismatch ? `,response=${failure.mismatch}` : ''}]`
: 'missing'
return `没有可计价组合;attempts=${result.attempts}, first=${describe(result.failures[0])}, last=${describe(result.failures.at(-1))}`
}
async function request(context, method, path, body, token, extraHeaders = {}) {
const headers = { Accept: 'application/json', Authorization: `Bearer ${token}`, ...extraHeaders }
let payload = body
@@ -96,19 +171,32 @@ async function loadMasterData(context, merchantToken) {
const fbaPage = context.apiSuccess(MODULE, 'Client 查询 FBA 目的仓', fbaResponse)
context.assert(MODULE, '存在可用 FBA 目的仓', fbaResponse, records(fbaPage).length > 0,
`count=${records(fbaPage).length}`)
const receiving = records(domesticPage)[0]; const destination = records(fbaPage)[0]
if (!receiving || !destination) throw new Error('缺少创建测试订单所需的收货仓或目的仓')
const receiving = records(domesticPage)[0]
const destinations = records(fbaPage)
if (!receiving || destinations.length === 0) throw new Error('缺少创建测试订单所需的收货仓或目的仓')
const channelResponse = await request(context, 'GET',
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
undefined, merchantToken)
const channels = context.apiSuccess(MODULE, 'Client 查询可用计费渠道', channelResponse)
const channel = Array.isArray(channels)
? channels.find((item) => Array.isArray(item.pricingTaxModes) && item.pricingTaxModes.length > 0)
: null
context.assert(MODULE, '存在带计价税制的可用渠道', channelResponse, Boolean(channel),
const priceableChannels = Array.isArray(channels)
? channels.filter((item) => Array.isArray(item.pricingTaxModes) && item.pricingTaxModes.length > 0)
: []
context.assert(MODULE, '存在带计价税制的可用渠道', channelResponse, priceableChannels.length > 0,
`count=${Array.isArray(channels) ? channels.length : 'n/a'}`)
if (!channel) throw new Error('缺少创建测试订单所需的可用渠道')
if (priceableChannels.length === 0) throw new Error('缺少创建测试订单所需的可用渠道')
const pricingResult = await findPricedOrderRoute({
receiving, destinations, channels: priceableChannels,
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
})
const priced = pricingResult.priced
context.addStep(MODULE, 'Client 选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
priced
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination.warehouseCode}, attempts=${pricingResult.attempts}`
: pricingFailureEvidence(pricingResult),
CONTRACT_OPTIONS)
if (!priced) throw new Error('当前起运仓、渠道、报价税制和FBA目的仓没有可计价组合')
const partnerResponse = await request(context, 'GET',
'/api/merchant/v1/partners/candidates?type=SHIPPER', undefined, merchantToken)
@@ -116,7 +204,8 @@ async function loadMasterData(context, merchantToken) {
const partner = Array.isArray(partners)
? partners.find((item) => item.status === 'ACTIVE' && item.types?.includes?.('SHIPPER'))
: null
return { receiving, destination, channel, partner }
return { receiving, destination: priced.destination, channel: priced.channel,
pricingTaxMode: priced.pricingTaxMode, partner }
}
function buildPayload(context, profile, master, productImageFileId) {
@@ -135,7 +224,7 @@ function buildPayload(context, profile, master, productImageFileId) {
receivingWarehouseName: master.receiving.warehouseName,
channelCode: master.channel.channelCode, channelName: master.channel.channelName,
carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION',
pricingTaxMode: master.channel.pricingTaxModes[0], containerMode: 'LCL',
pricingTaxMode: master.pricingTaxMode, containerMode: 'LCL',
customsInspection: false, destinationType: 'FBA',
amazonWarehouseCode: master.destination.warehouseCode,
shipmentId, referenceId,