cangku/backend/test/alibaba1688ProductEncodingService.test.js

743 lines
27 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const planningDatabase = require('../src/services/planningDatabase');
const productCodeClient = require('../src/services/alibaba1688ProductCodeClient');
const encodingRegistry = require('../src/services/alibaba1688ProductEncodingRegistry');
const {
MAX_CODING_SKUS,
normalizeCodingRequest,
normalizeUpstreamResult,
encodeAlibaba1688Product,
assertAlibaba1688PlanItemsEncoded,
} = require('../src/services/alibaba1688ProductEncodingService');
function productRecord(overrides = {}) {
return {
productId: '1073252781818',
title: '实木圆桌',
storeName: '测试家具厂',
skuCount: 2,
product: {
skus: [
{
index: 1,
name: '原木色 / 60cm',
specs: [{ name: '颜色', value: '原木色' }, { name: '尺寸', value: '60cm' }],
},
{
index: 2,
name: '胡桃色 / 80cm',
specs: [{ name: '颜色', value: '胡桃色' }, { name: '尺寸', value: '80cm' }],
},
],
},
...overrides,
};
}
function codingInput(overrides = {}) {
return {
operationCenter: '事业二部',
categoryName: '住宅家具 - 书架',
taxStatus: '含税',
invoiceType: '普票',
installationService: '不包安装',
installationMode: '组装',
skuItems: [
{
sourceSkuKey: '1:颜色=原木色|尺寸=60cm',
productName: '实木圆桌原木色60cm',
colorSpec: '颜色:原木色;尺寸:60cm',
lengthCm: 60,
widthCm: 60,
heightCm: 75,
},
{
sourceSkuKey: '2:颜色=胡桃色|尺寸=80cm',
productName: '实木圆桌胡桃色80cm',
colorSpec: '颜色:胡桃色;尺寸:80cm',
lengthCm: 80,
widthCm: 80,
heightCm: 75,
},
],
...overrides,
};
}
test('builds a server-owned coding payload with every captured SKU', () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord(), {
id: 7,
name: '香茗',
departmentName: '事业二部',
});
assert.equal(normalized.requestPayload.ali1688ProductId, '1073252781818');
assert.equal(normalized.requestPayload.ali1688SupplierName, '测试家具厂');
assert.equal(normalized.requestPayload.sourceSystem, 'maixiang');
assert.equal(normalized.requestPayload.operatorName, '香茗');
assert.equal(normalized.requestPayload.operatorDepartment, '事业二部');
assert.equal(normalized.requestPayload.skuItems.length, 2);
assert.match(normalized.sourceSkus[0].sourceSkuKey, /^1:/);
});
test('rejects partial SKU submission and products above the coding limit', () => {
assert.throws(
() => normalizeCodingRequest(codingInput({
skuItems: codingInput().skuItems.map((item, index) => (
index === 0 ? { ...item, widthCm: null } : item
)),
}), productRecord()),
error => error.statusCode === 400 && error.code === 'PRODUCT_CODE_SKU_DIMENSIONS_REQUIRED'
);
assert.throws(
() => normalizeCodingRequest(codingInput({ skuItems: codingInput().skuItems.slice(0, 1) }), productRecord()),
error => error.statusCode === 422 && error.code === 'PRODUCT_CODE_SKU_COUNT_MISMATCH'
);
const skus = Array.from({ length: MAX_CODING_SKUS + 1 }, (_, index) => ({
index: index + 1,
name: `规格${index + 1}`,
specs: [{ name: '规格', value: String(index + 1) }],
}));
assert.throws(
() => normalizeCodingRequest(codingInput({ skuItems: [] }), productRecord({
skuCount: skus.length,
product: { skus },
})),
error => error.statusCode === 422 && error.code === 'PRODUCT_CODE_SKU_LIMIT_EXCEEDED'
);
assert.throws(
() => normalizeCodingRequest(codingInput({
skuItems: codingInput().skuItems.map((item, index) => (
index === 0 ? { ...item, sourceSkuKey: 'stale-key' } : item
)),
}), productRecord()),
error => error.statusCode === 409 && error.code === 'PRODUCT_CODE_SKU_SNAPSHOT_CHANGED'
);
});
test('an already encoded product returns before validating or calling the upstream API', async () => {
let upstreamCalls = 0;
const encoding = {
productId: '1073252781818',
status: 'encoded',
styleCode: 'DF-A2SJ0094',
};
const result = await encodeAlibaba1688Product('1073252781818', {}, { name: '香茗' }, {
getProduct: async () => productRecord(),
getExistingEncoding: async () => encoding,
createCodes: async () => {
upstreamCalls += 1;
throw new Error('must not be called');
},
});
assert.equal(result.alreadyEncoded, true);
assert.equal(result.encoding, encoding);
assert.equal(upstreamCalls, 0);
});
test('maps returned product codes to source SKUs by the submitted order', () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord(), { name: '香茗' });
const result = normalizeUpstreamResult({
created: true,
styleCode: 'DF-A2SJ0094',
productCodes: ['DF-A2SJ009401', 'DF-A2SJ009402'],
items: [
{ styleCode: 'DF-A2SJ0094', productCode: 'DF-A2SJ009401' },
{ styleCode: 'DF-A2SJ0094', productCode: 'DF-A2SJ009402' },
],
}, normalized);
assert.equal(result.productCodes.length, 2);
assert.equal(result.skuMappings[0].sourceSkuName, '原木色 / 60cm');
assert.equal(result.skuMappings[0].lengthCm, 60);
assert.equal(result.skuMappings[0].heightCm, 75);
assert.equal(result.skuMappings[1].productCode, 'DF-A2SJ009402');
assert.throws(
() => normalizeUpstreamResult({ productCodes: ['DF-A2SJ009401'] }, normalized),
error => error.statusCode === 409 && error.code === 'PRODUCT_CODE_RESPONSE_COUNT_MISMATCH'
);
assert.throws(
() => normalizeUpstreamResult({ productCodes: ['DF-A2SJ009401', 'DF-A2SJ009401'] }, normalized),
error => error.statusCode === 409 && error.code === 'PRODUCT_CODE_RESPONSE_DUPLICATE'
);
assert.throws(
() => normalizeUpstreamResult({
productCodes: ['DF-A2SJ009401', 'DF-A2SJ009402'],
items: [{ ali1688ProductId: 'different-id' }, {}],
}, normalized),
error => error.statusCode === 409 && error.code === 'PRODUCT_CODE_RESPONSE_PRODUCT_MISMATCH'
);
});
test('reused upstream codes are matched by SKU name instead of response order', () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord(), { name: '香茗' });
const result = normalizeUpstreamResult({
created: false,
styleCode: 'DF-A2SJ0094',
productCodes: ['DF-A2SJ009402', 'DF-A2SJ009401'],
items: [
{ productName: '实木圆桌胡桃色80cm', productCode: 'DF-A2SJ009402' },
{ productName: '实木圆桌原木色60cm', productCode: 'DF-A2SJ009401' },
],
}, normalized);
assert.deepEqual(result.productCodes, ['DF-A2SJ009401', 'DF-A2SJ009402']);
assert.throws(
() => normalizeUpstreamResult({
created: false,
productCodes: ['DF-A2SJ009401', 'DF-A2SJ009402'],
items: [
{ productName: '无法匹配', productCode: 'DF-A2SJ009401' },
{ productName: '仍然无法匹配', productCode: 'DF-A2SJ009402' },
],
}, normalized),
error => error.statusCode === 409 && error.code === 'PRODUCT_CODE_REUSED_MAPPING_CONFLICT'
);
});
test('does not call the upstream service again when the product is already encoded', async () => {
let upstreamCalls = 0;
const result = await encodeAlibaba1688Product('1073252781818', codingInput(), { name: '香茗' }, {
getProduct: async () => productRecord(),
claim: async () => ({
claimed: false,
record: { productId: '1073252781818', status: 'encoded', productCodes: ['DF-A2SJ009401'] },
}),
createCodes: async () => {
upstreamCalls += 1;
throw new Error('must not be called');
},
});
assert.equal(result.alreadyEncoded, true);
assert.equal(upstreamCalls, 0);
});
test('persists both newly-created and reused upstream code responses', async () => {
for (const created of [true, false]) {
let completed;
const result = await encodeAlibaba1688Product('1073252781818', codingInput(), { name: '香茗' }, {
getProduct: async () => productRecord(),
claim: async () => ({ claimed: true, token: 'claim-token' }),
createCodes: async () => ({
statusCode: created ? 201 : 200,
data: {
created,
matchedExisting: !created,
styleCode: 'DF-A2SJ0094',
productCodes: ['DF-A2SJ009401', 'DF-A2SJ009402'],
items: created ? [] : [
{ productName: '实木圆桌原木色60cm', productCode: 'DF-A2SJ009401' },
{ productName: '实木圆桌胡桃色80cm', productCode: 'DF-A2SJ009402' },
],
},
}),
complete: async (_productId, token, value) => {
assert.equal(token, 'claim-token');
completed = value;
return { status: 'encoded', ...value };
},
});
assert.equal(result.alreadyEncoded, false);
assert.equal(result.encoding.status, 'encoded');
assert.equal(completed.upstreamCreated, created);
assert.equal(completed.skuMappings.length, 2);
}
});
test('marks the result as a conflict when SKUs change during the outbound request', async () => {
let conflict;
await assert.rejects(
encodeAlibaba1688Product('1073252781818', codingInput(), { name: '香茗' }, {
getProduct: async () => productRecord(),
getLatestProduct: async () => productRecord({
product: {
skus: [
{ index: 1, name: 'changed', specs: [{ name: '颜色', value: '白色' }] },
{ index: 2, name: 'changed-2', specs: [{ name: '颜色', value: '黑色' }] },
],
},
}),
claim: async () => ({ claimed: true, token: 'claim-token' }),
createCodes: async () => ({
data: {
created: true,
styleCode: 'DF-A2SJ0094',
productCodes: ['DF-A2SJ009401', 'DF-A2SJ009402'],
},
}),
conflict: async (productId, token, error) => {
conflict = { productId, token, code: error.code };
},
}),
error => error.code === 'PRODUCT_CODE_SKU_CHANGED_DURING_ENCODING'
);
assert.deepEqual(conflict, {
productId: '1073252781818',
token: 'claim-token',
code: 'PRODUCT_CODE_SKU_CHANGED_DURING_ENCODING',
});
});
test('selection plans are hydrated from canonical encoded 1688 data and reject stale mappings', async () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord(), { name: '香茗' });
const encoding = {
status: 'encoded',
sourceSkuCount: normalized.sourceSkus.length,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
productCodes: ['DF-A2SJ009401', 'DF-A2SJ009402'],
skuMappings: normalized.sourceSkus.map((entry, index) => ({
sourceSkuKey: entry.sourceSkuKey,
productCode: `DF-A2SJ00940${index + 1}`,
})),
};
const rows = await assertAlibaba1688PlanItemsEncoded([
{ marketItemId: '1073252781818', channelName: '1688', sourceRow: { untrusted: true } },
], {
getProduct: async () => ({
...productRecord(),
skuFingerprint: normalized.sourceSkuFingerprint,
}),
getEncoding: async () => encoding,
});
assert.equal(rows[0].channelName, '1688');
assert.deepEqual(rows[0].sourceRow.encoding.skuMappings, encoding.skuMappings);
assert.equal(rows[0].sourceRow.source, 'alibaba_1688_products');
await assert.rejects(
assertAlibaba1688PlanItemsEncoded([{ marketItemId: '1073252781818', channelName: '1688' }], {
getProduct: async () => ({ ...productRecord(), skuFingerprint: 'changed-fingerprint' }),
getEncoding: async () => encoding,
}),
error => error.code === 'ALIBABA_1688_PRODUCT_ENCODING_STALE'
);
let ordinaryLookupCount = 0;
const ordinaryRows = await assertAlibaba1688PlanItemsEncoded([
{ marketItemId: '1073252781818', channelName: '拼多多' },
], {
getProduct: async () => {
ordinaryLookupCount += 1;
return productRecord();
},
});
assert.equal(ordinaryLookupCount, 0);
assert.equal(ordinaryRows[0].channelName, '拼多多');
});
test('records a failed claim without retrying the upstream request', async () => {
let upstreamCalls = 0;
let failed;
await assert.rejects(
encodeAlibaba1688Product('1073252781818', codingInput(), { name: '香茗' }, {
getProduct: async () => productRecord(),
claim: async () => ({ claimed: true, token: 'claim-token' }),
createCodes: async () => {
upstreamCalls += 1;
throw Object.assign(new Error('upstream timeout'), { statusCode: 504 });
},
fail: async (productId, token, error) => {
failed = { productId, token, message: error.message };
},
}),
/upstream timeout/
);
assert.equal(upstreamCalls, 1);
assert.deepEqual(failed, {
productId: '1073252781818',
token: 'claim-token',
message: 'upstream timeout',
});
});
test('the outbound client keeps the API key in a backend request header', async () => {
const previous = {
key: process.env.PRODUCT_CODE_API_KEY,
baseUrl: process.env.PRODUCT_CODE_API_BASE_URL,
timeout: process.env.PRODUCT_CODE_API_TIMEOUT_MS,
};
process.env.PRODUCT_CODE_API_KEY = 'server-only-test-key';
process.env.PRODUCT_CODE_API_BASE_URL = 'https://yyd.jiansuntuzhi.cn';
process.env.PRODUCT_CODE_API_TIMEOUT_MS = '31000';
let requestConfig;
try {
const response = await productCodeClient.getProductCodeMeta({
httpClient: async (config) => {
requestConfig = config;
return { status: 200, data: { success: true, data: { operationCenters: [] } } };
},
});
assert.deepEqual(response.data.operationCenters, []);
assert.equal(requestConfig.headers['X-API-Key'], 'server-only-test-key');
assert.equal(requestConfig.timeout, 31000);
assert.equal(requestConfig.url, 'https://yyd.jiansuntuzhi.cn/api/public/product-code-board/meta');
} finally {
if (previous.key === undefined) delete process.env.PRODUCT_CODE_API_KEY;
else process.env.PRODUCT_CODE_API_KEY = previous.key;
if (previous.baseUrl === undefined) delete process.env.PRODUCT_CODE_API_BASE_URL;
else process.env.PRODUCT_CODE_API_BASE_URL = previous.baseUrl;
if (previous.timeout === undefined) delete process.env.PRODUCT_CODE_API_TIMEOUT_MS;
else process.env.PRODUCT_CODE_API_TIMEOUT_MS = previous.timeout;
}
});
test('the outbound client refuses to run without a server-side API key', () => {
const previous = process.env.PRODUCT_CODE_API_KEY;
delete process.env.PRODUCT_CODE_API_KEY;
try {
assert.throws(
() => productCodeClient.getProductCodeApiConfig(),
error => error.statusCode === 503 && error.code === 'PRODUCT_CODE_API_KEY_MISSING'
);
} finally {
if (previous === undefined) delete process.env.PRODUCT_CODE_API_KEY;
else process.env.PRODUCT_CODE_API_KEY = previous;
}
});
test('an existing encoded registry row wins a duplicate insert race', async () => {
const originalExecute = planningDatabase.execute;
const originalQuery = planningDatabase.query;
let encodingLookupCount = 0;
planningDatabase.execute = async (sql) => {
if (sql.startsWith('CREATE TABLE')) return { affectedRows: 0 };
if (sql.startsWith('INSERT INTO')) {
throw Object.assign(new Error('duplicate'), { code: 'ER_DUP_ENTRY', errno: 1062 });
}
throw new Error(`unexpected SQL: ${sql}`);
};
planningDatabase.query = async (sql) => {
if (sql.includes('information_schema.tables')) return [{ ok: 1 }];
if (sql.includes('information_schema.columns')) {
return [{ column_name: 'source_sku_count' }, { column_name: 'source_sku_fingerprint' }];
}
encodingLookupCount += 1;
if (encodingLookupCount === 1) return [];
return [{
product_id: '1073252781818',
status: 'encoded',
style_code: 'DF-A2SJ0094',
style_codes_json: '["DF-A2SJ0094"]',
product_codes_json: '["DF-A2SJ009401"]',
sku_mapping_json: '[]',
source_sku_count: 1,
source_sku_fingerprint: 'fingerprint',
attempt_count: 1,
}];
};
try {
const normalized = normalizeCodingRequest(codingInput(), productRecord());
const result = await encodingRegistry.claimEncoding(
'1073252781818',
normalized.requestPayload,
{ name: '香茗' },
{
sourceSkuCount: normalized.sourceSkus.length,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
}
);
assert.equal(result.claimed, false);
assert.equal(result.record.status, 'encoded');
assert.deepEqual(result.record.productCodes, ['DF-A2SJ009401']);
} finally {
planningDatabase.execute = originalExecute;
planningDatabase.query = originalQuery;
}
});
test('the claim lease always exceeds the configured outbound timeout', () => {
const previousTimeout = process.env.PRODUCT_CODE_API_TIMEOUT_MS;
const previousTtl = process.env.PRODUCT_CODE_CLAIM_TTL_SECONDS;
process.env.PRODUCT_CODE_API_TIMEOUT_MS = '120000';
process.env.PRODUCT_CODE_CLAIM_TTL_SECONDS = '120';
try {
assert.equal(encodingRegistry.getClaimTtlSeconds(), 180);
} finally {
if (previousTimeout === undefined) delete process.env.PRODUCT_CODE_API_TIMEOUT_MS;
else process.env.PRODUCT_CODE_API_TIMEOUT_MS = previousTimeout;
if (previousTtl === undefined) delete process.env.PRODUCT_CODE_CLAIM_TTL_SECONDS;
else process.env.PRODUCT_CODE_CLAIM_TTL_SECONDS = previousTtl;
}
});
test('retry comparison preserves the first business request while allowing a different operator', () => {
const original = {
...codingInput(),
operatorName: '香茗',
operatorDepartment: '事业二部',
};
const sameBusiness = {
...codingInput(),
operatorName: '管理员',
operatorDepartment: '系统管理',
};
assert.equal(
encodingRegistry.retryComparableRequest(original),
encodingRegistry.retryComparableRequest(sameBusiness)
);
assert.notEqual(
encodingRegistry.retryComparableRequest(original),
encodingRegistry.retryComparableRequest({
...sameBusiness,
categoryName: '住宅家具 - 电脑桌',
})
);
});
test('legacy failed requests may fill only missing dimensions while preserving the first request', () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord(), {
name: 'first-operator',
departmentName: 'first-department',
});
const legacyRequest = JSON.parse(JSON.stringify(normalized.requestPayload));
delete legacyRequest.skuItems[0].lengthCm;
legacyRequest.skuItems[0].widthCm = null;
delete legacyRequest.skuItems[1].heightCm;
const incoming = {
...normalized.requestPayload,
operatorName: 'retry-operator',
operatorDepartment: 'retry-department',
};
const upgraded = encodingRegistry.reconcileRetryRequest(legacyRequest, incoming);
assert.equal(upgraded.operatorName, 'first-operator');
assert.equal(upgraded.skuItems[0].lengthCm, 60);
assert.equal(upgraded.skuItems[0].widthCm, 60);
assert.equal(upgraded.skuItems[1].heightCm, 75);
assert.throws(
() => encodingRegistry.reconcileRetryRequest(normalized.requestPayload, {
...incoming,
skuItems: incoming.skuItems.map((item, index) => (
index === 0 ? { ...item, lengthCm: 61 } : item
)),
}),
error => error.code === 'PRODUCT_ENCODING_RETRY_REQUEST_CHANGED'
);
assert.throws(
() => encodingRegistry.reconcileRetryRequest(legacyRequest, {
...incoming,
skuItems: incoming.skuItems.map((item, index) => (
index === 0 ? { ...item, productName: 'changed-name' } : item
)),
}),
error => error.code === 'PRODUCT_ENCODING_RETRY_REQUEST_CHANGED'
);
});
test('source snapshots require the full request count and a SHA-256 fingerprint', () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord());
assert.deepEqual(
encodingRegistry.normalizeSourceSnapshot({
sourceSkuCount: normalized.sourceSkus.length,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
}, normalized.requestPayload),
{
sourceSkuCount: 2,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
}
);
assert.throws(
() => encodingRegistry.normalizeSourceSnapshot({
sourceSkuCount: 1,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
}, normalized.requestPayload),
error => error.code === 'PRODUCT_ENCODING_SOURCE_SKU_CHANGED'
);
assert.throws(
() => encodingRegistry.normalizeSourceSnapshot({
sourceSkuCount: 2,
sourceSkuFingerprint: 'not-a-fingerprint',
}, normalized.requestPayload),
error => error.code === 'PRODUCT_ENCODING_SOURCE_SNAPSHOT_REQUIRED'
);
});
test('the first registry claim persists the SKU count and fingerprint in the insert', async () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord());
const originalExecute = planningDatabase.execute;
const originalQuery = planningDatabase.query;
let inserted = false;
let insertSql = '';
let insertParams = [];
planningDatabase.query = async (sql) => {
if (sql.includes('information_schema.tables')) return [{ ok: 1 }];
if (sql.includes('information_schema.columns')) {
return [{ column_name: 'source_sku_count' }, { column_name: 'source_sku_fingerprint' }];
}
if (sql.includes('SELECT * FROM alibaba_1688_product_encodings')) {
if (!inserted) return [];
return [{
product_id: productRecord().productId,
status: 'encoding',
request_json: JSON.stringify(normalized.requestPayload),
source_sku_count: normalized.sourceSkus.length,
source_sku_fingerprint: normalized.sourceSkuFingerprint,
attempt_count: 1,
}];
}
throw new Error(`unexpected SQL: ${sql}`);
};
planningDatabase.execute = async (sql, params) => {
if (sql.includes('INSERT INTO alibaba_1688_product_encodings')) {
inserted = true;
insertSql = sql;
insertParams = params;
return { affectedRows: 1 };
}
throw new Error(`unexpected SQL: ${sql}`);
};
try {
const result = await encodingRegistry.claimEncoding(
productRecord().productId,
normalized.requestPayload,
{},
{
sourceSkuCount: normalized.sourceSkus.length,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
}
);
assert.equal(result.claimed, true);
assert.match(insertSql, /source_sku_count, source_sku_fingerprint/);
assert.equal(insertParams[3], normalized.sourceSkus.length);
assert.equal(insertParams[4], normalized.sourceSkuFingerprint);
} finally {
planningDatabase.execute = originalExecute;
planningDatabase.query = originalQuery;
}
});
test('a retry claim atomically upgrades only legacy missing dimensions and rejects a changed fingerprint', async () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord());
const legacyRequest = JSON.parse(JSON.stringify(normalized.requestPayload));
delete legacyRequest.skuItems[0].lengthCm;
legacyRequest.skuItems[0].widthCm = null;
const originalExecute = planningDatabase.execute;
const originalQuery = planningDatabase.query;
let storedFingerprint = null;
let updateSql = '';
let updateParams = [];
let updateCount = 0;
planningDatabase.query = async (sql) => {
if (sql.includes('information_schema.tables')) return [{ ok: 1 }];
if (sql.includes('information_schema.columns')) {
return [{ column_name: 'source_sku_count' }, { column_name: 'source_sku_fingerprint' }];
}
if (sql.includes('SELECT * FROM alibaba_1688_product_encodings')) {
return [{
product_id: productRecord().productId,
status: 'failed',
request_json: JSON.stringify(legacyRequest),
source_sku_count: null,
source_sku_fingerprint: storedFingerprint,
attempt_count: 1,
}];
}
throw new Error(`unexpected SQL: ${sql}`);
};
planningDatabase.execute = async (sql, params) => {
if (sql.includes('INSERT INTO alibaba_1688_product_encodings')) {
throw Object.assign(new Error('duplicate'), { code: 'ER_DUP_ENTRY', errno: 1062 });
}
if (sql.includes('UPDATE alibaba_1688_product_encodings')) {
updateCount += 1;
updateSql = sql;
updateParams = params;
return { affectedRows: 1 };
}
throw new Error(`unexpected SQL: ${sql}`);
};
const snapshot = {
sourceSkuCount: normalized.sourceSkus.length,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
};
try {
const result = await encodingRegistry.claimEncoding(
productRecord().productId,
normalized.requestPayload,
{},
snapshot
);
assert.equal(result.claimed, true);
assert.equal(result.requestPayload.skuItems[0].lengthCm, 60);
assert.equal(result.requestPayload.skuItems[0].widthCm, 60);
assert.match(updateSql, /source_sku_count IS NULL OR source_sku_count = \?/);
assert.match(updateSql, /source_sku_fingerprint IS NULL OR source_sku_fingerprint = \?/);
assert.equal(updateParams[2], snapshot.sourceSkuCount);
assert.equal(updateParams[3], snapshot.sourceSkuFingerprint);
storedFingerprint = 'a'.repeat(64);
await assert.rejects(
encodingRegistry.claimEncoding(
productRecord().productId,
normalized.requestPayload,
{},
snapshot
),
error => error.code === 'PRODUCT_ENCODING_SOURCE_SKU_CHANGED'
);
assert.equal(updateCount, 1);
} finally {
planningDatabase.execute = originalExecute;
planningDatabase.query = originalQuery;
}
});
test('selection plans reject incomplete, duplicate or mismatched encoded SKU mappings', async () => {
const normalized = normalizeCodingRequest(codingInput(), productRecord());
const product = {
...productRecord(),
skuFingerprint: normalized.sourceSkuFingerprint,
};
const validEncoding = {
status: 'encoded',
sourceSkuCount: 2,
sourceSkuFingerprint: normalized.sourceSkuFingerprint,
productCodes: ['CODE-1', 'CODE-2'],
skuMappings: normalized.sourceSkus.map((entry, index) => ({
sourceSkuKey: entry.sourceSkuKey,
productCode: `CODE-${index + 1}`,
})),
};
const invalidEncodings = [
{ ...validEncoding, sourceSkuCount: 1 },
{ ...validEncoding, productCodes: ['CODE-1'] },
{ ...validEncoding, productCodes: ['CODE-1', 'CODE-1'] },
{ ...validEncoding, skuMappings: validEncoding.skuMappings.slice(0, 1) },
{
...validEncoding,
skuMappings: validEncoding.skuMappings.map(mapping => ({
...mapping,
sourceSkuKey: validEncoding.skuMappings[0].sourceSkuKey,
})),
},
{
...validEncoding,
skuMappings: validEncoding.skuMappings.map((mapping, index) => (
index === 0 ? { ...mapping, productCode: 'UNKNOWN-CODE' } : mapping
)),
},
];
for (const encoding of invalidEncodings) {
await assert.rejects(
assertAlibaba1688PlanItemsEncoded([
{ marketItemId: product.productId, channelName: '1688' },
], {
getProduct: async () => product,
getEncoding: async () => encoding,
}),
error => error.code === 'ALIBABA_1688_PRODUCT_ENCODING_STALE'
);
}
});