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 { PricingPreviewRequest, ProductEditorSnapshot, SaveProductEditorDraftRequest, } from '@atlas/contracts'; import { pricingPreviewRequestSchema } from '@atlas/contracts'; import { beforeAll, afterAll, it, describe } from 'vitest'; import { DATABASE } from '../src/database/database.module.js'; import { createApplication } from '../src/main.js'; let app: NestExpressApplication, db: Database, base = '', workspace = '', user = '', token = '', platform = '', store = '', category = '', product = '', profile = '', provider = '', card = '', transport = '', variant = ''; async function request( path: string, method = 'GET', body?: unknown, extra: Record = {}, ) { const r = await fetch(base + '/api' + path, { method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, ...(method !== 'GET' ? { 'Idempotency-Key': randomUUID() } : {}), ...extra, }, body: body === undefined ? undefined : JSON.stringify(body), }); const data = (await r.json()) as any; return { status: r.status, ...data }; } async function read(): Promise { const r = await request(`/products/${product}/editor`, 'GET', undefined, { 'X-Atlas-Platform-Id': platform, }); assert.equal(r.status, 200, JSON.stringify(r)); return r.data; } function draft(s: ProductEditorSnapshot): SaveProductEditorDraftRequest { return { ai_source_material: s.source_material, ai_source_overridden_fields: s.source_material_state.overridden_fields, common: { ...s.product.common, default_physical_spec: s.product.default_physical_spec, default_package_spec: { shipping_weight_g: '380', length_mm: '220', width_mm: '160', height_mm: '90', status: 'confirmed', }, }, profile: { target_store_id: store, fulfillment: 'rfbs', title: s.profile.title, description: s.profile.description, tags: s.profile.tags, platform_product_category_id: category, category_source: 'manual', }, variants: s.variants.map((v) => ({ id: v.id, client_key: null, display_name: v.display_name, source_sku: v.source_sku, purchase_cost_cny: '28.00', specification_values: v.specification_values, title: v.title, tags: v.tags, description: v.description, physical_spec: v.physical_spec, package_spec: v.package_spec, offer_id: v.listing.offer_id, media_asset_ids: [], pricing_draft: v.listing.pricing_draft ?? null, })), }; } function quote( variantId: string | null = variant, clientKey: string | null = null, ): PricingPreviewRequest { return pricingPreviewRequestSchema.parse({ store_id: store, platform_product_category_id: category, fulfillment: 'rfbs', binding: { profile_id: profile, variant_id: variantId, client_key: clientKey, }, purchase_cost_cny: '28', package: { shipping_weight_g: '380', length_mm: '220', width_mm: '160', height_mm: '90', status: 'confirmed', }, pricing: { rate_card_id: card, transport_method_ids: [transport], default_transport_method_id: transport, domestic_shipping_and_label_fee_cny: '3', ad_rate: '0.03', last_mile_rate: '0.02', withdrawal_rate: '0.01', other_rate: '0.02', desired_markup_rate: '0.5', list_price_discount_rate: '0.2', temporary_commission_rate: '0.12', fx: { mode: 'one_time_override', rate: '12.3', reason: '仅集成测试', }, }, }); } async function save( s: ProductEditorSnapshot, d: SaveProductEditorDraftRequest, key = randomUUID(), ) { return request(`/products/${product}/editor-draft`, 'PUT', d, { 'X-Atlas-Platform-Id': platform, 'If-Match': `"${s.product.revision}:${s.profile.revision}"`, 'Idempotency-Key': key, }); } describe('SKU 试算与原子确认 HTTP / PostgreSQL', () => { beforeAll(async () => { Object.assign(process.env, { NODE_ENV: 'test', PRICING_ALLOW_DRAFT_RATES: 'true', SWAGGER_ENABLED: 'false', AUTH_REGISTER_IP_LIMIT: '100', AUTH_REGISTER_EMAIL_LIMIT: '100', }); app = (await createApplication()).app; db = app.get(DATABASE); await app.listen(0, '127.0.0.1'); base = `http://127.0.0.1:${(app.getHttpServer().address() as AddressInfo).port}`; const reg = await request('/auth/register', 'POST', { email: `pricing-${randomUUID()}@example.test`, password: 'pricing-test-password-123', password_confirmation: 'pricing-test-password-123', }); assert.equal(reg.status, 201, JSON.stringify(reg)); workspace = reg.data.workspace.id; user = reg.data.user.id; token = reg.data.access_token; platform = (await request('/platforms')).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,'CNY',true,'active','valid') RETURNING id`, [workspace, platform, randomUUID()], ) ).rows[0].id; 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,'test','测试类目','定价测试') RETURNING id`, [platform, randomUUID()], ) ).rows[0].id; await tx.query( `INSERT INTO atlas.platform_catalog_resource_states(platform_id,scope,scope_key,category_id,last_success_at,is_complete) VALUES($1,'attributes','attributes:' || $2::uuid,$2,now(),true)`, [platform, category], ); provider = ( await tx.query<{ id: string }>( `INSERT INTO atlas.logistics_providers(code,name) VALUES($1,'仅自动化测试') RETURNING id`, [randomUUID()], ) ).rows[0].id; card = ( await tx.query<{ id: string }>( `INSERT INTO atlas.logistics_rate_cards(provider_id,platform_id,fulfillment,version,source_name,status,effective_from,reviewed_at,reviewed_by_user_id) VALUES($1,$2,'rfbs','test-1','测试规则,不用于业务','active',now()-interval '1 day',now(),$3) RETURNING id`, [provider, platform, user], ) ).rows[0].id; transport = ( await tx.query<{ id: string }>( `INSERT INTO atlas.logistics_transport_methods(rate_card_id,code,name) VALUES($1,'standard','标准测试') RETURNING id`, [card], ) ).rows[0].id; const type = ( await tx.query<{ id: string }>( `INSERT INTO atlas.logistics_product_types(rate_card_id,code,name,merchandise_currency_code,max_merchandise_value,max_actual_weight_kg,max_sum_dimensions_cm,max_longest_side_cm,billing_basis) VALUES($1,'ExtraSmall','小包测试','CNY',135,0.5,90,60,'actual_only') RETURNING id`, [card], ) ).rows[0].id; await tx.query( `INSERT INTO atlas.logistics_rate_rules(product_type_id,transport_method_id,fixed_fee_cny,per_billable_kg_fee_cny) VALUES($1,$2,3.12,36.4)`, [type, transport], ); }); const collection = await request('/collection-items', 'POST', { title: '定价集成测试商品', }); assert.equal(collection.status, 201); product = collection.data.product_id; const context = await request( `/collection-items/${collection.data.collection_item.id}/editor-contexts`, 'POST', undefined, { 'X-Atlas-Platform-Id': platform }, ); assert.equal(context.status, 201, JSON.stringify(context)); profile = context.data.profile_id; variant = (await read()).variants[0].id; }); afterAll(async () => { if (db && workspace) { await db.workspaceTransaction(workspace, async (tx) => { // 仅清理本测试创建的租户,按外键依赖顺序删除。 for (const table of [ 'idempotency_requests', 'pricing_previews', 'product_variant_listing_media', 'product_variant_platform_listings', 'pricing_quote_options', 'pricing_quotes', 'pricing_policy_transport_methods', 'pricing_policies', 'product_unmapped_attributes', 'product_attributes', 'product_variants', 'product_platform_profiles', 'products', 'collection_items', 'outbox_events', 'operations', 'stores', ]) await tx.query( `DELETE FROM atlas.${table} WHERE workspace_id=$1`, [workspace], ); }); if (card) await db.query( 'DELETE FROM atlas.logistics_rate_cards WHERE id=$1', [card], ); if (provider) await db.query( 'DELETE FROM atlas.logistics_providers WHERE id=$1', [provider], ); if (category) { await db.query( 'DELETE FROM atlas.platform_catalog_resource_states WHERE category_id=$1', [category], ); await db.query( 'DELETE FROM atlas.platform_product_categories WHERE id=$1', [category], ); } await db.query( 'DELETE FROM atlas.workspace_memberships WHERE user_id=$1', [user], ); await db.query('DELETE FROM atlas.workspaces WHERE id=$1', [ workspace, ]); await db.query('DELETE FROM atlas.users WHERE id=$1', [user]); } if (app) await app.close(); }); it('目录只返回审核规则,预览幂等且独立试算不写商品', async () => { const catalog = await request(`/pricing/catalog?store_id=${store}`); assert.equal(catalog.status, 200, JSON.stringify(catalog)); assert.ok(catalog.data.rate_cards.some((c: any) => c.id === card)); const categories = await request( `/pricing/categories?store_id=${store}&q=定价测试`, ); assert.equal(categories.status, 200, JSON.stringify(categories)); const input = quote(), key = randomUUID(), a = await request('/pricing/quote-previews', 'POST', input, { 'Idempotency-Key': key, }), b = await request('/pricing/quote-previews', 'POST', input, { 'Idempotency-Key': key, }); assert.equal(a.status, 201, JSON.stringify(a)); assert.deepEqual(b.data, a.data); assert.equal(a.data.options[0].selling_price, '77.44'); assert.equal((await read()).variants[0].listing.selling_price, null); }); it('原子确认、重放、重读与同输入再保存保持快照', async () => { const s = await read(), d = draft(s), input = quote(), p = await request('/pricing/quote-previews', 'POST', input); d.variants[0].pricing_draft = input.pricing; d.variants[0].pricing_selection = { preview_id: p.data.preview_id, option_id: p.data.recommended_option_id, input_hash: p.data.input_hash, }; const key = randomUUID(), saved = await save(s, d, key); assert.equal(saved.status, 200, JSON.stringify(saved)); assert.deepEqual((await save(s, d, key)).data, saved.data); const after = await read(); assert.equal(after.variants[0].listing.pricing_valid, true); assert.equal( after.variants[0].listing.pricing_result?.profit_cny, '14.00', ); assert.equal(Number(after.variants[0].listing.selling_price), 77.44); const again = await save(after, draft(after)); assert.equal(again.status, 200, JSON.stringify(again)); assert.equal((await read()).variants[0].listing.pricing_valid, true); }); it('输入/有效期变化返回 SKU 错误,整笔商品保存回滚;无报价允许保存', async () => { const s = await read(), d = draft(s), input = quote(), p = await request('/pricing/quote-previews', 'POST', input); d.common.source_title = '不应被保存'; d.variants[0].pricing_draft = input.pricing; d.variants[0].purchase_cost_cny = '29'; d.variants[0].pricing_selection = { preview_id: p.data.preview_id, option_id: p.data.recommended_option_id, input_hash: p.data.input_hash, }; const failed = await save(s, d); assert.equal(failed.status, 409, JSON.stringify(failed)); assert.equal(failed.error.code, 'PRICING_PREVIEW_STALE'); assert.equal((await read()).product.revision, s.product.revision); d.variants[0].purchase_cost_cny = '28'; await db.workspaceTransaction(workspace, (tx) => tx.query( `UPDATE atlas.pricing_previews SET expires_at=now()-interval '1 second' WHERE id=$1`, [p.data.preview_id], ), ); assert.equal((await save(s, d)).status, 409); d.variants[0].pricing_selection = null; d.variants[0].purchase_cost_cny = '29'; assert.equal((await save(s, d)).status, 200); assert.equal((await read()).variants[0].listing.pricing_valid, false); }); it('新 SKU client_key 与真实 ID 在同一事务映射', async () => { const s = await read(), d = draft(s), input = quote(null, 'new-pricing'), p = await request('/pricing/quote-previews', 'POST', input); d.variants.push({ ...d.variants[0], id: null, client_key: 'new-pricing', display_name: '新报价 SKU', pricing_draft: input.pricing, pricing_selection: { preview_id: p.data.preview_id, option_id: p.data.recommended_option_id, input_hash: p.data.input_hash, }, }); const saved = await save(s, d); assert.equal(saved.status, 200, JSON.stringify(saved)); const after = await read(), newId = saved.data.created_variants[0].id; assert.equal( after.variants.find((v) => v.id === newId)?.listing.pricing_valid, true, ); }); it('其他租户不能读取预览或使用门店;报价复合外键拒绝跨 SKU 引用', async () => { const outsider = randomUUID(); await db.workspaceTransaction(outsider, async (tx) => { assert.equal( ( await tx.query( 'SELECT id FROM atlas.pricing_previews WHERE store_id=$1', [store], ) ).rowCount, 0, ); assert.equal( ( await tx.query( 'SELECT id FROM atlas.pricing_quotes WHERE store_id=$1', [store], ) ).rowCount, 0, ); }); const s = await read(); await assert.rejects(() => db.workspaceTransaction(workspace, async (tx) => { await tx.query( `UPDATE atlas.product_variant_platform_listings SET pricing_quote_id=(SELECT pricing_quote_id FROM atlas.product_variant_platform_listings WHERE product_variant_id=$1) WHERE product_variant_id=$2`, [s.variants[1].id, s.variants[0].id], ); }), ); }); it('方案 CRUD、版本冲突、默认与归档不改变已确认报价', async () => { const body = { name: '测试默认方案', parameters: { ...quote().pricing, fx: { mode: 'policy_fixed', rate: '12.3' }, }, allow_commission_override: true, }; const c = await request( `/stores/${store}/pricing-policies`, 'POST', body, ); assert.equal(c.status, 201, JSON.stringify(c)); const id = c.data.id; assert.equal( (await request(`/pricing-policies/${id}/set-default`, 'POST')) .status, 200, ); assert.equal( ( await request(`/pricing-policies/${id}`, 'PATCH', body, { 'If-Match': '"1"', }) ).status, 409, ); const before = await read(); assert.equal( (await request(`/pricing-policies/${id}`, 'DELETE')).status, 200, ); assert.deepEqual( (await read()).variants[1].listing.pricing_result, before.variants[1].listing.pricing_result, ); }); it('规则更新、独立预览和其他 SKU 预览不能被旧确认意图接受', async () => { const state = await read(), payload = draft(state), input = quote(); const preview = await request('/pricing/quote-previews', 'POST', input); payload.variants[0].pricing_draft = input.pricing; payload.variants[0].pricing_selection = { preview_id: preview.data.preview_id, option_id: preview.data.recommended_option_id, input_hash: preview.data.input_hash, }; await db.query( `UPDATE atlas.logistics_rate_cards SET version='test-2',updated_at=now() WHERE id=$1`, [card], ); assert.equal( (await save(state, payload)).error.code, 'PRICING_PREVIEW_STALE', ); const standalone = await request('/pricing/quote-previews', 'POST', { ...input, binding: null, }); payload.variants[0].pricing_selection = { preview_id: standalone.data.preview_id, option_id: standalone.data.recommended_option_id, input_hash: standalone.data.input_hash, }; assert.equal((await save(state, payload)).status, 409); const other = await request('/pricing/quote-previews', 'POST', { ...input, binding: { ...input.binding, variant_id: state.variants[1].id }, }); payload.variants[0].pricing_selection = { preview_id: other.data.preview_id, option_id: other.data.recommended_option_id, input_hash: other.data.input_hash, }; assert.equal((await save(state, payload)).status, 409); assert.equal((await read()).product.revision, state.product.revision); }); it('确认快照不可更新;历史预览过期不使确认失效', async () => { const before = await read(); await assert.rejects(() => db.workspaceTransaction(workspace, (tx) => tx.query( `UPDATE atlas.pricing_quotes SET purchase_cost_cny=0 WHERE workspace_id=$1 AND status='confirmed'`, [workspace], ), ), ); await assert.rejects(() => db.workspaceTransaction(workspace, (tx) => tx.query( `UPDATE atlas.pricing_quote_options SET profit_rub=0 WHERE workspace_id=$1`, [workspace], ), ), ); await db.workspaceTransaction(workspace, (tx) => tx.query( `UPDATE atlas.pricing_previews SET expires_at=now()-interval '1 day' WHERE workspace_id=$1`, [workspace], ), ); assert.deepEqual( (await read()).variants[1].listing, before.variants[1].listing, ); }); it('没有当前报价的草稿保存成功,但发布校验给出定价问题', async () => { const state = await read(), payload = draft(state); payload.variants[0].purchase_cost_cny = '41.00'; assert.equal((await save(state, payload)).status, 200); const validation = await request( `/product-platform-profiles/${profile}/validation`, 'GET', undefined, { 'X-Atlas-Platform-Id': platform }, ); assert.equal(validation.status, 200, JSON.stringify(validation)); assert.ok( validation.data.variants[0].issues.some( (i: { code: string }) => i.code === 'PRICING_REQUIRED', ), ); }); it('并发商品保存只有一个版本成功', async () => { const state = await read(), a = draft(state), b = draft(state); a.common.source_title = '并发 A'; b.common.source_title = '并发 B'; const results = await Promise.all([save(state, a), save(state, b)]); assert.deepEqual(results.map((r) => r.status).sort(), [200, 409]); }); it('未审核费率、缺失正式佣金以及亏损候选不会变成发布报价', async () => { await db.query( `UPDATE atlas.logistics_rate_cards SET reviewed_at=NULL WHERE id=$1`, [card], ); assert.equal( (await request('/pricing/quote-previews', 'POST', quote())).error .code, 'PRICING_RULES_UNAVAILABLE', ); await db.query( `UPDATE atlas.logistics_rate_cards SET reviewed_at=now() WHERE id=$1`, [card], ); const input = quote(); input.pricing.temporary_commission_rate = null; assert.equal( (await request('/pricing/quote-previews', 'POST', input)).error .code, 'PRICING_COMMISSION_UNAVAILABLE', ); input.pricing.temporary_commission_rate = '0.12'; input.pricing.mode = 'fixed_sale'; input.pricing.selling_price = '10'; const p = await request('/pricing/quote-previews', 'POST', input); assert.equal(p.status, 201); assert.equal(p.data.options[0].can_confirm, false); const state = await read(), payload = draft(state); payload.variants[0].pricing_draft = input.pricing; payload.variants[0].pricing_selection = { preview_id: p.data.preview_id, option_id: p.data.options[0].id, input_hash: p.data.input_hash, }; assert.equal((await save(state, payload)).status, 409); }); it('开发草稿可试算并确认到商品编辑页,但不能通过首次上架校验', async () => { await db.query( `UPDATE atlas.logistics_rate_cards SET status='draft',reviewed_at=NULL,reviewed_by_user_id=NULL WHERE id=$1`, [card], ); await db.query( `UPDATE atlas.logistics_rate_rules SET status='draft' WHERE product_type_id IN (SELECT id FROM atlas.logistics_product_types WHERE rate_card_id=$1)`, [card], ); const catalog = await request(`/pricing/catalog?store_id=${store}`); assert.equal(catalog.status, 200, JSON.stringify(catalog)); assert.equal( catalog.data.rate_cards.find((item: { id: string }) => item.id === card) ?.development_only, true, ); const input = quote(); input.purchase_cost_cny = '41.00'; const preview = await request('/pricing/quote-previews', 'POST', input); assert.equal(preview.status, 201, JSON.stringify(preview)); assert.equal(preview.data.context.development_only, true); const state = await read(); const payload = draft(state); payload.variants[0].purchase_cost_cny = '41.00'; payload.variants[0].pricing_draft = input.pricing; payload.variants[0].pricing_selection = { preview_id: preview.data.preview_id, option_id: preview.data.recommended_option_id, input_hash: preview.data.input_hash, }; const saved = await save(state, payload); assert.equal(saved.status, 200, JSON.stringify(saved)); const validation = await request( `/product-platform-profiles/${profile}/validation`, 'GET', undefined, { 'X-Atlas-Platform-Id': platform }, ); assert.equal(validation.status, 200, JSON.stringify(validation)); assert.ok( validation.data.variants[0].issues.some( (issue: { code: string }) => issue.code === 'PRICING_RATE_REVIEW_REQUIRED', ), ); const rateCard = await db.query<{ status: string; reviewed_at: Date | null }>( 'SELECT status,reviewed_at FROM atlas.logistics_rate_cards WHERE id=$1', [card], ); assert.equal(rateCard.rows[0].status, 'draft'); assert.equal(rateCard.rows[0].reviewed_at, null); }); });