feat(edit): 编辑页上报ozon 开发
This commit is contained in:
@@ -2129,7 +2129,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('俄文发布标题为空时发布就绪校验提示必填,填写后移除提示', async () => {
|
||||
it('俄文标题按通用或逐 SKU 模式校验,并持久保存切换结果', async () => {
|
||||
await database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const store = await tx.query<{ id: string }>(
|
||||
`SELECT id FROM atlas.stores
|
||||
@@ -2148,7 +2148,7 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
[platformId, `editor-title-${randomUUID()}`],
|
||||
);
|
||||
});
|
||||
const created = await createCollectionItem('俄文发布标题校验商品');
|
||||
const created = await createCollectionItem('俄文标题校验商品');
|
||||
const entered = await request(
|
||||
`/api/collection-items/${created.collectionItemId}/editor-contexts`,
|
||||
{
|
||||
@@ -2189,6 +2189,119 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
|
||||
(issue: { code: string }) => issue.code === 'PUBLISH_TITLE_REQUIRED',
|
||||
),
|
||||
);
|
||||
|
||||
const beforeModeSwitch = await request(`/api/products/${created.productId}/editor`, {
|
||||
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
|
||||
});
|
||||
const snapshot = beforeModeSwitch.body.data;
|
||||
assert.equal(snapshot.profile.title_mode, 'common');
|
||||
const draft = {
|
||||
ai_source_material: snapshot.source_material,
|
||||
ai_source_overridden_fields: snapshot.source_material_state.overridden_fields,
|
||||
common: {
|
||||
...snapshot.product.common,
|
||||
default_physical_spec: snapshot.product.default_physical_spec,
|
||||
default_package_spec: snapshot.product.default_package_spec,
|
||||
},
|
||||
profile: {
|
||||
target_store_id: snapshot.profile.target_store.id,
|
||||
fulfillment: snapshot.profile.fulfillment,
|
||||
title: null as string | null,
|
||||
title_mode: 'variant',
|
||||
description: snapshot.profile.description,
|
||||
tags: snapshot.profile.tags,
|
||||
platform_product_category_id: snapshot.profile.category.id,
|
||||
category_source: snapshot.profile.category.source,
|
||||
},
|
||||
variants: snapshot.variants.map((variant: any) => ({
|
||||
id: variant.id,
|
||||
client_key: null,
|
||||
display_name: variant.display_name,
|
||||
source_sku: variant.source_sku,
|
||||
specification_values: variant.specification_values,
|
||||
purchase_cost_cny: variant.purchase_cost_cny,
|
||||
title: null,
|
||||
tags: variant.tags,
|
||||
description: variant.description,
|
||||
physical_spec: variant.physical_spec,
|
||||
package_spec: variant.package_spec,
|
||||
offer_id: variant.listing.offer_id,
|
||||
media_asset_ids: variant.media.map((item: { asset_id: string }) => item.asset_id),
|
||||
})),
|
||||
};
|
||||
const saveTitleMode = async (body: typeof draft, productRevision: number, profileRevision: number) =>
|
||||
request(`/api/products/${created.productId}/editor-draft`, {
|
||||
method: 'PUT',
|
||||
headers: headers({
|
||||
'X-Atlas-Platform-Id': platformId,
|
||||
'Idempotency-Key': randomUUID(),
|
||||
'If-Match': `"${productRevision}:${profileRevision}"`,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const variantMode = await saveTitleMode(
|
||||
draft,
|
||||
snapshot.product.revision,
|
||||
snapshot.profile.revision,
|
||||
);
|
||||
assert.equal(variantMode.response.status, 200, JSON.stringify(variantMode.body));
|
||||
const blankVariant = await request(
|
||||
`/api/product-platform-profiles/${profileId}/validation`,
|
||||
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
|
||||
);
|
||||
assert.ok(
|
||||
!blankVariant.body.data.profile.issues.some(
|
||||
(issue: { code: string }) => issue.code === 'PUBLISH_TITLE_REQUIRED',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
blankVariant.body.data.variants[0].issues.some(
|
||||
(issue: { code: string; field: string }) =>
|
||||
issue.code === 'PUBLISH_TITLE_REQUIRED' &&
|
||||
issue.field === `variants.${snapshot.variants[0].id}.title`,
|
||||
),
|
||||
);
|
||||
const afterVariantMode = await request(`/api/products/${created.productId}/editor`, {
|
||||
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
|
||||
});
|
||||
assert.equal(afterVariantMode.body.data.profile.title_mode, 'variant');
|
||||
assert.equal(afterVariantMode.body.data.profile.title, null);
|
||||
const filledVariant = await saveTitleMode(
|
||||
{
|
||||
...draft,
|
||||
variants: [{ ...draft.variants[0], title: 'Название SKU' }],
|
||||
},
|
||||
afterVariantMode.body.data.product.revision,
|
||||
afterVariantMode.body.data.profile.revision,
|
||||
);
|
||||
assert.equal(filledVariant.response.status, 200, JSON.stringify(filledVariant.body));
|
||||
const readyTitle = await request(
|
||||
`/api/product-platform-profiles/${profileId}/validation`,
|
||||
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
|
||||
);
|
||||
assert.ok(
|
||||
!readyTitle.body.data.variants[0].issues.some(
|
||||
(issue: { code: string }) => issue.code === 'PUBLISH_TITLE_REQUIRED',
|
||||
),
|
||||
);
|
||||
const afterFilledVariant = await request(`/api/products/${created.productId}/editor`, {
|
||||
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
|
||||
});
|
||||
const commonMode = await saveTitleMode(
|
||||
{
|
||||
...draft,
|
||||
profile: { ...draft.profile, title: '通用俄文标题', title_mode: 'common' },
|
||||
variants: [{ ...draft.variants[0], title: null }],
|
||||
},
|
||||
afterFilledVariant.body.data.product.revision,
|
||||
afterFilledVariant.body.data.profile.revision,
|
||||
);
|
||||
assert.equal(commonMode.response.status, 200, JSON.stringify(commonMode.body));
|
||||
const afterCommonMode = await request(`/api/products/${created.productId}/editor`, {
|
||||
headers: headers({ 'X-Atlas-Platform-Id': platformId }),
|
||||
});
|
||||
assert.equal(afterCommonMode.body.data.profile.title_mode, 'common');
|
||||
assert.equal(afterCommonMode.body.data.variants[0].title, null);
|
||||
});
|
||||
|
||||
it('包装重量模式独立保存,并允许 SKU 已确认重量继承通用尺寸', async () => {
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import type { Database } from '@atlas/database';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import type { ProductPublishJob } from '@atlas/contracts';
|
||||
import type { S3Client } from '@aws-sdk/client-s3';
|
||||
import { beforeAll, afterAll, describe, it } from 'vitest';
|
||||
import { DATABASE } from '../src/database/database.module.js';
|
||||
import { createApplication } from '../src/main.js';
|
||||
import {
|
||||
runProductPublish,
|
||||
type PublishDependencies,
|
||||
} from '../../worker/src/product-publish.js';
|
||||
import {
|
||||
OzonRateLimitError,
|
||||
OzonRetryableError,
|
||||
} from '../../worker/src/ozon-client.js';
|
||||
|
||||
let app: NestExpressApplication,
|
||||
db: Database,
|
||||
base = '',
|
||||
workspace = '',
|
||||
token = '',
|
||||
platform = '',
|
||||
store = '',
|
||||
category = '';
|
||||
const storage = {} as S3Client;
|
||||
async function request(
|
||||
path: string,
|
||||
method = 'GET',
|
||||
body?: unknown,
|
||||
extra: Record<string, string> = {},
|
||||
) {
|
||||
const res = await fetch(`${base}/api${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Atlas-Platform-Id': platform,
|
||||
...extra,
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
return { status: res.status, body: (await res.json()) as any };
|
||||
}
|
||||
async function fixture(count = 2) {
|
||||
const created = await request(
|
||||
'/collection-items',
|
||||
'POST',
|
||||
{ title: '发布测试商品' },
|
||||
{ 'Idempotency-Key': randomUUID() },
|
||||
);
|
||||
assert.equal(created.status, 201, JSON.stringify(created));
|
||||
const product = created.body.data.product_id;
|
||||
const entered = await request(
|
||||
`/collection-items/${created.body.data.collection_item.id}/editor-contexts`,
|
||||
'POST',
|
||||
{},
|
||||
{ 'Idempotency-Key': randomUUID() },
|
||||
);
|
||||
assert.equal(entered.status, 201, JSON.stringify(entered));
|
||||
const profile = entered.body.data.profile_id;
|
||||
const ids: string[] = [];
|
||||
await db.workspaceTransaction(workspace, async (tx) => {
|
||||
await tx.query(
|
||||
`UPDATE atlas.products SET brand='Без бренда',model='TEST-MODEL',default_package_length_mm=240,default_package_width_mm=120,default_package_height_mm=60,default_shipping_weight_g=350,package_spec_status='confirmed' WHERE id=$1`,
|
||||
[product],
|
||||
);
|
||||
await tx.query(
|
||||
`UPDATE atlas.product_platform_profiles SET title='Тестовый товар',fulfillment='rfbs',platform_product_category_id=$2,platform_type_id='1002' WHERE id=$1`,
|
||||
[profile, category],
|
||||
);
|
||||
const first = (
|
||||
await tx.query<{ id: string }>(
|
||||
'SELECT id FROM atlas.product_variants WHERE product_id=$1',
|
||||
[product],
|
||||
)
|
||||
).rows[0].id;
|
||||
ids.push(first);
|
||||
for (let i = 1; i < count; i++)
|
||||
ids.push(
|
||||
(
|
||||
await tx.query<{ id: string }>(
|
||||
`INSERT INTO atlas.product_variants(workspace_id,product_id,display_name,sort_order) VALUES($1,$2,$3,$4) RETURNING id`,
|
||||
[workspace, product, `SKU ${i}`, i],
|
||||
)
|
||||
).rows[0].id,
|
||||
);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const variant = ids[i];
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.product_variant_platform_listings(workspace_id,product_id,profile_id,platform_id,product_variant_id,store_id) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING`,
|
||||
[workspace, product, profile, platform, variant, store],
|
||||
);
|
||||
const quote = (
|
||||
await tx.query<{ id: string }>(
|
||||
`INSERT INTO atlas.pricing_quotes(workspace_id,store_id,platform_id,product_id,profile_id,product_variant_id,fulfillment,purchase_cost_cny,package_weight_kg,package_length_cm,package_width_cm,package_height_cm,fx_cny_to_rub,input_snapshot,status,confirmed_at) VALUES($1,$2,$3,$4,$5,$6,'rfbs',20,0.350,24,12,6,12,'{}','confirmed',now()) RETURNING id`,
|
||||
[workspace, store, platform, product, profile, variant],
|
||||
)
|
||||
).rows[0].id;
|
||||
const listing = (
|
||||
await tx.query<{ id: string }>(
|
||||
`UPDATE atlas.product_variant_platform_listings SET offer_id=$2,selling_price=1999.1234,list_price=2999.1234,currency_code='RUB',pricing_quote_id=$3,pricing_valid=true WHERE profile_id=$1 AND product_variant_id=$4 RETURNING id`,
|
||||
[profile, `TEST-${randomUUID()}`, quote, variant],
|
||||
)
|
||||
).rows[0].id;
|
||||
const asset = (
|
||||
await tx.query<{ id: string }>(
|
||||
`INSERT INTO atlas.media_assets(workspace_id,storage_provider,bucket,object_key,content_type,byte_size,checksum_sha256) VALUES($1,'qiniu_kodo','test-bucket',$2,'image/png',100,$3) RETURNING id`,
|
||||
[workspace, randomUUID(), randomUUID()],
|
||||
)
|
||||
).rows[0].id;
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.product_variant_listing_media(workspace_id,product_id,profile_id,listing_id,product_variant_id,asset_id,sort_order) VALUES($1,$2,$3,$4,$5,$6,0)`,
|
||||
[workspace, product, profile, listing, variant, asset],
|
||||
);
|
||||
}
|
||||
});
|
||||
return { profile, product, ids };
|
||||
}
|
||||
async function publish(
|
||||
f: Awaited<ReturnType<typeof fixture>>,
|
||||
ids = f.ids,
|
||||
key = randomUUID(),
|
||||
) {
|
||||
return request(
|
||||
`/product-platform-profiles/${f.profile}/publish-jobs`,
|
||||
'POST',
|
||||
{ variant_ids: ids },
|
||||
{ 'Idempotency-Key': key, 'If-Match': '"1:1"' },
|
||||
);
|
||||
}
|
||||
async function run(
|
||||
job: ProductPublishJob,
|
||||
requestFn: PublishDependencies['request'],
|
||||
images: PublishDependencies['images'] = async () => [
|
||||
'https://images.example.test/fixture.png',
|
||||
],
|
||||
) {
|
||||
await runProductPublish(
|
||||
db,
|
||||
storage,
|
||||
{
|
||||
eventId: randomUUID(),
|
||||
workspaceId: workspace,
|
||||
eventType: 'product_publish.requested',
|
||||
payload: { publish_job_id: job.id },
|
||||
},
|
||||
{
|
||||
request: requestFn,
|
||||
images,
|
||||
decrypt: () => 'fixture-key',
|
||||
},
|
||||
);
|
||||
const response = await request(`/product-publish-jobs/${job.id}`);
|
||||
assert.equal(response.status, 200);
|
||||
return response.body.data as ProductPublishJob;
|
||||
}
|
||||
|
||||
describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
|
||||
beforeAll(async () => {
|
||||
Object.assign(process.env, {
|
||||
NODE_ENV: 'test',
|
||||
SWAGGER_ENABLED: 'false',
|
||||
LOG_LEVEL: 'error',
|
||||
AUTH_REGISTER_IP_LIMIT: '100',
|
||||
AUTH_REGISTER_EMAIL_LIMIT: '100',
|
||||
OZON_PUBLISH_ENABLED: 'true',
|
||||
});
|
||||
({ app } = await createApplication());
|
||||
db = app.get<Database>(DATABASE);
|
||||
await app.listen(0, '127.0.0.1');
|
||||
base = `http://127.0.0.1:${(app.getHttpServer().address() as AddressInfo).port}`;
|
||||
const registration = await fetch(`${base}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: `publish-${randomUUID()}@example.test`,
|
||||
password: 'correct-horse-battery-staple-123',
|
||||
password_confirmation: 'correct-horse-battery-staple-123',
|
||||
}),
|
||||
});
|
||||
const data = (await registration.json()) as any;
|
||||
assert.equal(registration.status, 201, JSON.stringify(data));
|
||||
workspace = data.data.workspace.id;
|
||||
token = data.data.access_token;
|
||||
platform = (await request('/platforms')).body.data[0].id;
|
||||
await db.workspaceTransaction(workspace, async (tx) => {
|
||||
store = (
|
||||
await tx.query<{ id: string }>(
|
||||
`INSERT INTO atlas.stores(workspace_id,platform_id,name,platform_client_id,currency_code,is_default,status,credential_status) VALUES($1,$2,'发布测试店铺',$3,'RUB',true,'active','valid') RETURNING id`,
|
||||
[workspace, platform, randomUUID()],
|
||||
)
|
||||
).rows[0].id;
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.store_secrets(workspace_id,store_id,kind,secret_ciphertext,secret_nonce,encryption_key_version,fingerprint,status) VALUES($1,$2,'seller_api_key',$3,$4,'v1','fixture','valid')`,
|
||||
[workspace, store, Buffer.from('fixture'), Buffer.alloc(12)],
|
||||
);
|
||||
category = (
|
||||
await tx.query<{ id: string }>(
|
||||
`INSERT INTO atlas.platform_product_categories(platform_id,external_category_id,external_type_id,name,path) VALUES($1,$2,'1002','发布测试类目','发布测试类目') RETURNING id`,
|
||||
[platform, `${Date.now()}`],
|
||||
)
|
||||
).rows[0].id;
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.platform_product_category_attributes(platform_id,platform_product_category_id,platform_attribute_id,name,value_type,is_required,is_aspect,max_value_count) VALUES($1,$2,'9048','型号','text',true,false,1)`,
|
||||
[platform, category],
|
||||
);
|
||||
await tx.query(
|
||||
`INSERT INTO atlas.platform_catalog_resource_states(platform_id,scope_key,scope,category_id,last_success_at,is_complete) VALUES($1,$2,'attributes',$3,now(),true)`,
|
||||
[platform, `attributes:${category}`, category],
|
||||
);
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
await app?.close();
|
||||
});
|
||||
|
||||
it('预检、并发幂等、精度快照、Outbox 与跨租户隔离', async () => {
|
||||
const f = await fixture();
|
||||
const key = randomUUID();
|
||||
const [a, b] = await Promise.all([
|
||||
publish(f, undefined, key),
|
||||
publish(f, undefined, key),
|
||||
]);
|
||||
assert.equal(a.status, 202, JSON.stringify(a));
|
||||
assert.deepEqual(a.body, b.body);
|
||||
const job = a.body.data.publish_job as ProductPublishJob;
|
||||
assert.equal(job.items.length, 2);
|
||||
const facts = await db.workspaceTransaction(workspace, async (tx) => ({
|
||||
items: (
|
||||
await tx.query(
|
||||
'SELECT snapshot FROM atlas.product_publish_job_items WHERE job_id=$1',
|
||||
[job.id],
|
||||
)
|
||||
).rows,
|
||||
events: (
|
||||
await tx.query(
|
||||
'SELECT event_payload FROM atlas.outbox_events WHERE aggregate_id=$1',
|
||||
[job.id],
|
||||
)
|
||||
).rows,
|
||||
}));
|
||||
assert.equal(facts.events.length, 1);
|
||||
assert.deepEqual(facts.events[0].event_payload, {
|
||||
publish_job_id: job.id,
|
||||
});
|
||||
assert.equal(facts.items[0].snapshot.price, '1999.1234');
|
||||
assert.equal(facts.items[0].snapshot.depth, 240);
|
||||
assert.ok(!('vat' in facts.items[0].snapshot));
|
||||
assert.ok(!('images' in facts.items[0].snapshot));
|
||||
const other = randomUUID();
|
||||
assert.equal(
|
||||
(
|
||||
await db.workspaceTransaction(other, (tx) =>
|
||||
tx.query(
|
||||
'SELECT id FROM atlas.product_publish_jobs WHERE id=$1',
|
||||
[job.id],
|
||||
),
|
||||
)
|
||||
).rows.length,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await request(
|
||||
`/product-publish-jobs/${job.id}`,
|
||||
'GET',
|
||||
undefined,
|
||||
{ 'X-Atlas-Platform-Id': randomUUID() },
|
||||
)
|
||||
).status,
|
||||
404,
|
||||
);
|
||||
assert.equal(
|
||||
(await publish(f)).body.error.code,
|
||||
'SKU_IMPORT_IN_PROGRESS',
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await publish(f, [
|
||||
...Array.from({ length: 51 }, () => randomUUID()),
|
||||
])
|
||||
).status,
|
||||
422,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await request(
|
||||
`/product-platform-profiles/${f.profile}/publish-jobs`,
|
||||
'POST',
|
||||
{ variant_ids: f.ids },
|
||||
{ 'Idempotency-Key': randomUUID(), 'If-Match': '"2:1"' },
|
||||
)
|
||||
).body.error.code,
|
||||
'VERSION_CONFLICT',
|
||||
);
|
||||
});
|
||||
it('只校验所选 SKU;不就绪的未选 SKU 不阻断部分上架', async () => {
|
||||
const f = await fixture();
|
||||
await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.product_variant_platform_listings SET offer_id=NULL WHERE profile_id=$1 AND product_variant_id=$2`,
|
||||
[f.profile, f.ids[1]],
|
||||
),
|
||||
);
|
||||
const all = await publish(f);
|
||||
assert.equal(all.status, 422);
|
||||
assert.equal(all.body.error.code, 'PUBLISH_VALIDATION_FAILED');
|
||||
const partial = await publish(f, [f.ids[0]]);
|
||||
assert.equal(partial.status, 202, JSON.stringify(partial));
|
||||
assert.equal(partial.body.data.publish_job.item_count, 1);
|
||||
});
|
||||
it('混合结果持久化,成功锁定型号/货号,重复消息不重复上报', async () => {
|
||||
const f = await fixture();
|
||||
let job = (await publish(f)).body.data.publish_job as ProductPublishJob;
|
||||
let imports = 0;
|
||||
job = await run(job, async (_client, _key, path, body) => {
|
||||
if (path === '/v3/product/info/list') return { items: [] };
|
||||
assert.equal(path, '/v3/product/import');
|
||||
imports++;
|
||||
const items = body.items as any[];
|
||||
assert.equal(items.length, 2);
|
||||
assert.equal(items[0].price, '1999.1234');
|
||||
assert.ok(!('vat' in items[0]));
|
||||
assert.ok(!('barcode' in items[0]));
|
||||
assert.ok(!('description' in items[0]));
|
||||
return { result: { task_id: 123456 } };
|
||||
});
|
||||
assert.equal(job.items[0].status, 'polling');
|
||||
const offers = job.items.map((i) => i.offer_id);
|
||||
job = await run(job, async () => ({
|
||||
result: {
|
||||
items: [
|
||||
{
|
||||
offer_id: offers[0],
|
||||
product_id: 101,
|
||||
status: 'imported',
|
||||
errors: [],
|
||||
},
|
||||
{
|
||||
offer_id: offers[1],
|
||||
status: 'failed',
|
||||
errors: [
|
||||
{
|
||||
code: 'BAD_IMAGE',
|
||||
message:
|
||||
'失败 https://private.example.test/a?token=hidden',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
assert.equal(job.status, 'partially_succeeded');
|
||||
assert.equal(
|
||||
job.items.find((i) => i.status === 'imported')?.platform_product_id,
|
||||
'101',
|
||||
);
|
||||
assert.ok(!JSON.stringify(job).includes('token='));
|
||||
await assert.rejects(
|
||||
db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.products SET model='CHANGED' WHERE id=$1`,
|
||||
[f.product],
|
||||
),
|
||||
),
|
||||
/PRODUCT_MODEL_LOCKED/,
|
||||
);
|
||||
await assert.rejects(
|
||||
db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.product_variant_platform_listings SET offer_id='CHANGED' WHERE profile_id=$1 AND status='imported'`,
|
||||
[f.profile],
|
||||
),
|
||||
),
|
||||
/SKU_ALREADY_IMPORTED/,
|
||||
);
|
||||
await run(job, async () => {
|
||||
imports++;
|
||||
throw new Error('终态不应再访问平台');
|
||||
});
|
||||
assert.equal(imports, 1);
|
||||
});
|
||||
it('响应丢失后只按货号对账,绝不重复导入', async () => {
|
||||
const f = await fixture(1);
|
||||
let job = (await publish(f)).body.data.publish_job as ProductPublishJob;
|
||||
let imports = 0;
|
||||
job = await run(job, async (_c, _k, path) => {
|
||||
if (path === '/v3/product/info/list') return { items: [] };
|
||||
imports++;
|
||||
throw new OzonRetryableError('response lost');
|
||||
});
|
||||
assert.equal(job.status, 'reconciling');
|
||||
job = await run(job, async (_c, _k, path) => {
|
||||
assert.equal(path, '/v3/product/info/list');
|
||||
return { items: [] };
|
||||
});
|
||||
assert.equal(job.status, 'reconciling');
|
||||
assert.equal(imports, 1);
|
||||
job = await run(job, async () => ({
|
||||
items: [{ offer_id: job.items[0].offer_id, id: 999 }],
|
||||
}));
|
||||
assert.equal(job.status, 'succeeded');
|
||||
assert.equal(imports, 1);
|
||||
});
|
||||
it('任一图片准备失败时整批不提交,并隐藏存储路径', async () => {
|
||||
const f = await fixture(2);
|
||||
const created = (await publish(f)).body.data
|
||||
.publish_job as ProductPublishJob;
|
||||
let prepared = 0;
|
||||
const job = await run(
|
||||
created,
|
||||
async (_c, _k, path) => {
|
||||
assert.equal(path, '/v3/product/info/list');
|
||||
return { items: [] };
|
||||
},
|
||||
async () => {
|
||||
if (++prepared === 2)
|
||||
throw new Error('AccessDenied secret/object/key');
|
||||
return ['https://images.example.test/fixture.png'];
|
||||
},
|
||||
);
|
||||
assert.equal(job.status, 'failed');
|
||||
assert.ok(job.items.every((i) => i.status === 'failed'));
|
||||
assert.ok(!JSON.stringify(job).includes('secret/object'));
|
||||
const source = await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
'SELECT submit_started_at FROM atlas.product_publish_jobs WHERE id=$1',
|
||||
[job.id],
|
||||
),
|
||||
);
|
||||
assert.equal(source.rows[0].submit_started_at, null);
|
||||
});
|
||||
it('远端已有同货号时跳过,不调用更新接口;429 延迟后允许提交', async () => {
|
||||
const existing = await fixture(1);
|
||||
let job = (await publish(existing)).body.data
|
||||
.publish_job as ProductPublishJob;
|
||||
job = await run(job, async (_c, _k, path) => {
|
||||
assert.equal(path, '/v3/product/info/list');
|
||||
return { items: [{ offer_id: job.items[0].offer_id, id: 88 }] };
|
||||
});
|
||||
assert.equal(job.items[0].status, 'skipped');
|
||||
assert.equal(job.items[0].errors[0].code, 'OZON_OFFER_ALREADY_EXISTS');
|
||||
const f = await fixture(1);
|
||||
job = (await publish(f)).body.data.publish_job as ProductPublishJob;
|
||||
job = await run(job, async (_c, _k, path) => {
|
||||
if (path === '/v3/product/info/list') return { items: [] };
|
||||
throw new OzonRateLimitError(120);
|
||||
});
|
||||
assert.equal(job.items[0].status, 'queued');
|
||||
const source = await db.workspaceTransaction(workspace, (tx) =>
|
||||
tx.query(
|
||||
'SELECT submit_started_at FROM atlas.product_publish_jobs WHERE id=$1',
|
||||
[job.id],
|
||||
),
|
||||
);
|
||||
assert.equal(source.rows[0].submit_started_at, null);
|
||||
job = await run(job, async (_c, _k, path) =>
|
||||
path === '/v3/product/info/list'
|
||||
? { items: [] }
|
||||
: { result: { task_id: 987 } },
|
||||
);
|
||||
assert.equal(job.items[0].status, 'polling');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user