feat(ai-edit): 开发 AI 填报功能

This commit is contained in:
R524809
2026-09-23 17:18:09 +08:00
parent 95a2a37f1b
commit b3958166dd
35 changed files with 2832 additions and 278 deletions
@@ -0,0 +1,233 @@
import { Inject, Injectable } from '@nestjs/common';
import type { ProductAiTask, ProductEditorSnapshot } from '@atlas/contracts';
import type { Database } from '@atlas/database';
import { ApiError } from '../common/api-error.js';
import { IdempotencyService } from '../common/idempotency.service.js';
import { DATABASE } from '../database/database.module.js';
import { ProductEditorService } from './product-editor.service.js';
@Injectable()
export class ProductAiService {
public constructor(
@Inject(DATABASE) private readonly database: Database,
@Inject(IdempotencyService)
private readonly idempotency: IdempotencyService,
@Inject(ProductEditorService)
private readonly editor: ProductEditorService,
) {}
/** 固化当前编辑快照并在同一事务写入任务与 Outbox。 */
async create(
workspaceId: string,
userId: string,
profileId: string,
platformId: string,
variantId: string | null,
key: string,
hint: string | undefined,
) {
const profile = await this.database.workspaceTransaction(
workspaceId,
async (tx) => {
const row = await tx.query<{ product_id: string }>(
`SELECT product_id FROM atlas.product_platform_profiles
WHERE id = $1 AND platform_id = $2`,
[profileId, platformId],
);
if (!row.rows[0])
throw new ApiError(
404,
'PROFILE_NOT_FOUND',
'商品发布配置不存在',
);
return row.rows[0];
},
);
const snapshot = await this.editor.read(
workspaceId,
profile.product_id,
platformId,
);
if (snapshot.profile.id !== profileId)
throw new ApiError(
409,
'PROFILE_CHANGED',
'商品发布配置已变化,请刷新页面',
);
if (
variantId &&
!snapshot.variants.some((variant) => variant.id === variantId)
)
throw new ApiError(404, 'VARIANT_NOT_FOUND', 'SKU 不属于当前商品');
const kind = variantId ? 'sku_copy' : 'autofill';
const path = variantId
? `/api/product-platform-profiles/${profileId}/variants/${variantId}/ai/copy`
: `/api/product-platform-profiles/${profileId}/ai/autofills`;
return this.idempotency.run(
{
workspaceId,
userId,
method: 'POST',
path,
key,
request: { platform_id: platformId, hint: hint ?? null },
},
async (tx) => {
const current = await tx.query<{
product_revision: number;
profile_revision: number;
source_snapshot_revision: number;
}>(
`SELECT product.revision AS product_revision,
profile.revision AS profile_revision,
item.source_snapshot_revision
FROM atlas.product_platform_profiles profile
JOIN atlas.products product ON product.id = profile.product_id
JOIN atlas.collection_items item ON item.id = product.collection_item_id
WHERE profile.id = $1 AND profile.platform_id = $2
FOR UPDATE OF profile, product, item`,
[profileId, platformId],
);
const row = current.rows[0];
if (
!row ||
Number(row.product_revision) !==
snapshot.product.revision ||
Number(row.profile_revision) !==
snapshot.profile.revision ||
Number(row.source_snapshot_revision) !==
snapshot.source_material.source_snapshot_revision
) {
throw new ApiError(
409,
'AI_INPUT_CHANGED',
'商品资料已变化,请重新点击 AI 填报',
);
}
const input = taskInput(snapshot, variantId, hint);
const inserted = await tx.query<{ id: string }>(
`INSERT INTO atlas.product_ai_tasks
(workspace_id, product_id, profile_id, variant_id, kind, input_snapshot, created_by_user_id)
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4, $5::jsonb, $6)
RETURNING id`,
[
profile.product_id,
profileId,
variantId,
kind,
JSON.stringify(input),
userId,
],
);
const taskId = inserted.rows[0].id;
await tx.query(
`INSERT INTO atlas.outbox_events
(workspace_id, aggregate_type, aggregate_id, event_type, event_payload, deduplication_key)
VALUES (current_setting('app.workspace_id')::uuid, 'product_ai_task', $1,
'product_ai_task.requested', $2::jsonb, $3)`,
[
taskId,
JSON.stringify({ task_id: taskId }),
`product_ai_task:${taskId}`,
],
);
return {
status: 202,
body: { data: { ai_task_id: taskId, status: 'queued' } },
};
},
);
}
/** 只返回已校验结果和输入版本,不暴露采集快照与模型提示词。 */
async read(
workspaceId: string,
taskId: string,
platformId: string,
): Promise<ProductAiTask> {
return this.database.workspaceTransaction(workspaceId, async (tx) => {
const result = await tx.query<{
id: string;
kind: 'autofill' | 'sku_copy' | 'attribute_fill';
status: ProductAiTask['status'];
variant_id: string | null;
input_snapshot: Record<string, unknown>;
result: ProductAiTask['result'];
error_code: string | null;
error_message: string | null;
}>(
`SELECT task.id, task.kind, task.status, task.variant_id,
task.input_snapshot, task.result, task.error_code, task.error_message
FROM atlas.product_ai_tasks task
JOIN atlas.product_platform_profiles profile ON profile.id = task.profile_id
WHERE task.id = $1 AND profile.platform_id = $2`,
[taskId, platformId],
);
const row = result.rows[0];
if (!row)
throw new ApiError(404, 'AI_TASK_NOT_FOUND', 'AI 任务不存在');
const input = row.input_snapshot as {
revision?: ProductAiTask['input_revision'];
};
return {
id: row.id,
kind: row.kind,
status: row.status,
input_revision: input.revision ?? {
product: 0,
profile: 0,
source_snapshot: 0,
},
variant_id: row.variant_id,
result:
row.status === 'succeeded' || row.status === 'partial'
? row.result
: null,
error_code: row.error_code,
error_message: row.error_message,
};
});
}
}
function taskInput(
snapshot: ProductEditorSnapshot,
variantId: string | null,
hint?: string,
) {
return {
revision: {
product: snapshot.product.revision,
profile: snapshot.profile.revision,
source_snapshot: snapshot.source_material.source_snapshot_revision,
},
platform_id: snapshot.profile.platform_id,
product_id: snapshot.product.id,
profile_id: snapshot.profile.id,
variant_id: variantId,
hint: hint ?? null,
source_material: snapshot.source_material,
common: snapshot.product.common,
profile: {
title: snapshot.profile.title,
tags: snapshot.profile.tags,
description: snapshot.profile.description,
category: snapshot.profile.category,
target_store_id: snapshot.profile.target_store.id,
},
variants: snapshot.variants.map((variant) => ({
id: variant.id,
display_name: variant.display_name,
source_sku: variant.source_sku,
specification_values: variant.specification_values,
listing: {
status: variant.listing.status,
offer_id: variant.listing.offer_id,
},
title: variant.title,
tags: variant.tags,
description: variant.description,
package_spec: variant.effective_package_spec,
})),
};
}
@@ -14,6 +14,7 @@ import {
import { z } from 'zod';
import {
productPublishPreflightRequestSchema,
requestProductAiTaskSchema,
saveProductEditorDraftRequestSchema,
saveProductSourceDescriptionRequestSchema,
type Workspace,
@@ -22,6 +23,7 @@ import { ApiError } from '../common/api-error.js';
import type { AuthenticatedRequest } from '../auth/auth.guard.js';
import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js';
import { ProductEditorService } from './product-editor.service.js';
import { ProductAiService } from './product-ai.service.js';
type Response = { status: (value: number) => Response };
@@ -31,6 +33,8 @@ export class ProductEditorController {
public constructor(
@Inject(ProductEditorService)
private readonly editor: ProductEditorService,
@Inject(ProductAiService)
private readonly productAi: ProductAiService,
) {}
/** 为采集记录创建或复用当前平台的商品编辑上下文。 */
@@ -70,6 +74,85 @@ export class ProductEditorController {
};
}
/** 用户主动创建 SPU AI 填报任务。 */
@Post('product-platform-profiles/:profileId/ai/autofills')
async aiAutofill(
@Param('profileId') profileId: string,
@Body() body: unknown,
@Headers('x-atlas-platform-id') platformId: string | undefined,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const parsed = requestProductAiTaskSchema.safeParse(body ?? {});
if (!parsed.success)
throw new ApiError(
422,
'VALIDATION_FAILED',
'AI 请求参数不符合要求',
);
const result = await this.productAi.create(
workspace.id,
request.auth.userId,
requiredUuid('profile_id', profileId),
requiredUuid('X-Atlas-Platform-Id', platformId),
null,
requiredUuid('Idempotency-Key', key),
parsed.data.hint,
);
response.status(result.status);
return result.body;
}
/** 用户主动创建单个 SKU 的文案任务,不影响其他 SKU。 */
@Post('product-platform-profiles/:profileId/variants/:variantId/ai/copy')
async aiSkuCopy(
@Param('profileId') profileId: string,
@Param('variantId') variantId: string,
@Body() body: unknown,
@Headers('x-atlas-platform-id') platformId: string | undefined,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: Response,
) {
const parsed = requestProductAiTaskSchema.safeParse(body ?? {});
if (!parsed.success)
throw new ApiError(
422,
'VALIDATION_FAILED',
'AI 请求参数不符合要求',
);
const result = await this.productAi.create(
workspace.id,
request.auth.userId,
requiredUuid('profile_id', profileId),
requiredUuid('X-Atlas-Platform-Id', platformId),
requiredUuid('variant_id', variantId),
requiredUuid('Idempotency-Key', key),
parsed.data.hint,
);
response.status(result.status);
return result.body;
}
/** 查询当前工作空间和平台的 AI 任务状态。 */
@Get('product-ai-tasks/:taskId')
async aiTask(
@Param('taskId') taskId: string,
@Headers('x-atlas-platform-id') platformId: string | undefined,
@CurrentWorkspace() workspace: Workspace,
) {
return {
data: await this.productAi.read(
workspace.id,
requiredUuid('ai_task_id', taskId),
requiredUuid('X-Atlas-Platform-Id', platformId),
),
};
}
/** 返回当前平台共享的类目树,供编辑器选择。 */
@Get('product-platform-profiles/:profileId/category-options')
async categoryOptions(
@@ -288,7 +371,8 @@ export class ProductEditorController {
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
) {
const parsed = saveProductSourceDescriptionRequestSchema.safeParse(body);
const parsed =
saveProductSourceDescriptionRequestSchema.safeParse(body);
if (!parsed.success) {
throw new ApiError(
422,
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { IdempotencyService } from '../common/idempotency.service.js';
import { ProductEditorController } from './product-editor.controller.js';
import { ProductEditorService } from './product-editor.service.js';
import { ProductAiService } from './product-ai.service.js';
@Module({
controllers: [ProductEditorController],
providers: [IdempotencyService, ProductEditorService],
providers: [IdempotencyService, ProductEditorService, ProductAiService],
})
export class ProductEditorModule {}
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import type {
ProductEditorContext,
ProductEditorSnapshot,
ProductCopyTranslation,
SaveProductEditorDraftRequest,
SaveProductSourceDescriptionRequest,
} from '@atlas/contracts';
@@ -214,6 +215,7 @@ export class ProductEditorService {
purchase_cost_cny: string | null;
listing_id: string;
offer_id: string | null;
title: string | null;
description: string | null;
tags: unknown;
status: string;
@@ -236,7 +238,7 @@ export class ProductEditorService {
}>(
`SELECT v.id, v.source_variant_key, v.source_sku, v.display_name, v.sort_order,
v.purchase_cost_cny::text AS purchase_cost_cny, l.id AS listing_id, l.offer_id,
l.description, l.tags, l.status, l.selling_price::text,
l.title, l.description, l.tags, l.status, l.selling_price::text,
l.list_price::text, l.currency_code, v.specification_values,
v.physical_length_mm::text, v.physical_width_mm::text, v.physical_height_mm::text,
v.net_weight_g::text, v.physical_spec_status,
@@ -350,6 +352,17 @@ export class ProductEditorService {
WHERE profile_id = $1 AND status = 'pending' ORDER BY created_at, id`,
[profile.id],
);
const translations = await transaction.query<{
variant_id: string | null;
field: 'title' | 'tags' | 'description';
russian_value: string | string[];
chinese_value: string | string[];
source_task_id: string | null;
}>(
`SELECT variant_id, field, russian_value, chinese_value, source_task_id
FROM atlas.product_copy_translations WHERE profile_id = $1`,
[profile.id],
);
const defaultPhysicalSpec = mapProductPhysicalSpec(product);
const defaultPackageSpec = mapProductPackageSpec(product);
const sourceMaterialResult = resolveSourceMaterial(product);
@@ -362,6 +375,7 @@ export class ProductEditorService {
display_name: variant.display_name,
sort_order: variant.sort_order,
purchase_cost_cny: variant.purchase_cost_cny,
title: variant.title,
tags: Array.isArray(variant.tags)
? variant.tags.filter(
(value): value is string =>
@@ -466,6 +480,7 @@ export class ProductEditorService {
source_material: sourceMaterial,
source_material_state: sourceMaterialResult.state,
captured_category_match: capturedCategoryMatch,
copy_translations: translations.rows,
ai_input: {
source_material: sourceMaterial,
default_physical_spec: defaultPhysicalSpec,
@@ -883,13 +898,16 @@ export class ProductEditorService {
await transaction.query(
`UPDATE atlas.product_variant_platform_listings
SET offer_id = $1, store_id = $2, tags = $3::jsonb, description = $4,
title = CASE WHEN $5::boolean THEN $6 ELSE title END,
revision = revision + 1, updated_at = now()
WHERE profile_id = $5 AND product_variant_id = $6 AND status <> 'imported'`,
WHERE profile_id = $7 AND product_variant_id = $8 AND status <> 'imported'`,
[
variant.offer_id,
input.profile.target_store_id,
JSON.stringify(variant.tags),
variant.description,
variant.title !== undefined,
variant.title ?? null,
profile.id,
variant.id,
],
@@ -974,9 +992,9 @@ export class ProductEditorService {
await transaction.query(
`INSERT INTO atlas.product_variant_platform_listings
(workspace_id, product_id, profile_id, platform_id, product_variant_id, store_id, offer_id,
currency_code, tags, description)
currency_code, tags, description, title)
SELECT current_setting('app.workspace_id')::uuid, $1, $2, $3, $4, $5, $6, s.currency_code,
$7::jsonb, $8
$7::jsonb, $8, $9
FROM atlas.stores s WHERE s.id = $5`,
[
productId,
@@ -987,6 +1005,7 @@ export class ProductEditorService {
variant.offer_id,
JSON.stringify(variant.tags),
variant.description,
variant.title ?? null,
],
);
}
@@ -1019,6 +1038,15 @@ export class ProductEditorService {
profile,
variants: variantMedia,
});
if (input.copy_translations) {
await this.persistCopyTranslations(
transaction,
productId,
profile.id,
retainedIds,
input.copy_translations,
);
}
return {
status: 200,
body: {
@@ -1038,6 +1066,68 @@ export class ProductEditorService {
);
}
/** 中俄对照与草稿同一事务保存,继承的 SKU 字段不创建副本。 */
private async persistCopyTranslations(
transaction: DatabaseTransaction,
productId: string,
profileId: string,
retainedVariantIds: string[],
translations: ProductCopyTranslation[],
) {
const retained = new Set(retainedVariantIds);
const seen = new Set<string>();
for (const item of translations) {
if (item.variant_id && !retained.has(item.variant_id))
throw new ApiError(
422,
'INVALID_TRANSLATION_VARIANT',
'中文对照关联了无效 SKU',
);
const identity = `${item.variant_id ?? 'spu'}:${item.field}`;
if (seen.has(identity))
throw new ApiError(
422,
'DUPLICATE_TRANSLATION',
'同一字段不能重复保存中文对照',
);
seen.add(identity);
if (item.source_task_id) {
const task = await transaction.query<{ id: string }>(
`SELECT id FROM atlas.product_ai_tasks WHERE id = $1 AND profile_id = $2`,
[item.source_task_id, profileId],
);
if (!task.rows[0])
throw new ApiError(
422,
'INVALID_TRANSLATION_TASK',
'中文对照来源任务不属于当前商品',
);
}
}
await transaction.query(
`DELETE FROM atlas.product_copy_translations WHERE profile_id = $1`,
[profileId],
);
for (const item of translations) {
await transaction.query(
`INSERT INTO atlas.product_copy_translations
(workspace_id, product_id, profile_id, variant_id, field,
russian_value, chinese_value, source_task_id)
VALUES (current_setting('app.workspace_id')::uuid, $1, $2, $3, $4,
$5::jsonb, $6::jsonb, $7)`,
[
productId,
profileId,
item.variant_id,
item.field,
JSON.stringify(item.russian_value),
JSON.stringify(item.chinese_value),
item.source_task_id,
],
);
}
}
/** 保存采集描述覆盖,避免未完成的整页草稿阻塞该独立编辑动作。 */
async saveSourceDescription(
workspaceId: string,
@@ -1063,8 +1153,16 @@ export class ProductEditorService {
},
async (transaction) => {
await this.requireTargetPlatform(transaction, platformId);
const product = await this.requireProduct(transaction, productId, true);
const profile = await this.findProfile(transaction, productId, platformId);
const product = await this.requireProduct(
transaction,
productId,
true,
);
const profile = await this.findProfile(
transaction,
productId,
platformId,
);
if (!profile)
throw new ApiError(
409,
@@ -1096,24 +1194,36 @@ export class ProductEditorService {
field: 'source_snapshot_revision',
code: 'SOURCE_SNAPSHOT_CHANGED',
message: `当前采集版本为 ${product.source_snapshot_revision}`,
current_revision: Number(product.source_snapshot_revision),
expected_revision: input.source_snapshot_revision,
current_revision: Number(
product.source_snapshot_revision,
),
expected_revision:
input.source_snapshot_revision,
},
],
);
}
const currentSource = resolveSourceMaterial(product);
const overriddenFields = new Set(currentSource.state.overridden_fields);
if (input.description === currentSource.state.latest.description) {
const overriddenFields = new Set(
currentSource.state.overridden_fields,
);
if (
input.description === currentSource.state.latest.description
) {
overriddenFields.delete('description');
} else {
overriddenFields.add('description');
}
const overrides = selectSourceMaterialOverrides(
{ ...currentSource.effective, description: input.description },
{
...currentSource.effective,
description: input.description,
},
[...overriddenFields],
);
const updatedProduct = await transaction.query<{ revision: number }>(
const updatedProduct = await transaction.query<{
revision: number;
}>(
`UPDATE atlas.products
SET ai_source_material_overrides = $1::jsonb,
ai_source_material_base_revision = $2,
@@ -1136,13 +1246,16 @@ export class ProductEditorService {
const savedSource = resolveSourceMaterial({
...product,
ai_source_material_overrides: overrides,
ai_source_material_base_revision: input.source_snapshot_revision,
ai_source_material_base_revision:
input.source_snapshot_revision,
});
return {
status: 200,
body: {
data: {
product_revision: Number(updatedProduct.rows[0].revision),
product_revision: Number(
updatedProduct.rows[0].revision,
),
profile_revision: Number(profile.revision),
source_material: savedSource.effective,
source_material_state: savedSource.state,
@@ -1796,7 +1909,8 @@ export class ProductEditorService {
WHERE item.id = $1 AND item.deleted_at IS NULL`,
[product.collection_item_id],
);
if (sourcePlatform.rows[0]?.code.toUpperCase() !== 'OZON') return unavailable;
if (sourcePlatform.rows[0]?.code.toUpperCase() !== 'OZON')
return unavailable;
const signals = capturedOzonCategorySignals(
latestProductAiSourceMaterial(product),
@@ -1851,11 +1965,12 @@ export class ProductEditorService {
LIMIT 20`,
[platformId, signals.typeName, signals.rootCategoryName],
);
const status = candidates.rows.length === 1
? 'matched'
: candidates.rows.length
? 'ambiguous'
: 'unmatched';
const status =
candidates.rows.length === 1
? 'matched'
: candidates.rows.length
? 'ambiguous'
: 'unmatched';
return {
status,
root_category_name: signals.rootCategoryName,
@@ -205,7 +205,13 @@ export class ProductEditorValidationOperations {
issues.push({
code: 'TITLE_REQUIRED',
field: 'common.source_title',
message: '请填写标题',
message: '请填写中文商品名',
});
if (!profile.title?.trim())
issues.push({
code: 'PUBLISH_TITLE_REQUIRED',
field: 'profile.title',
message: '请填写俄文发布标题',
});
if (!product.model?.trim())
issues.push({
+74
View File
@@ -452,6 +452,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
},
],
purchase_cost_cny: '88.50',
tags: [],
description: null,
physical_spec: {
length_mm: null,
width_mm: null,
@@ -476,6 +478,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
source_sku: null,
specification_values: [],
purchase_cost_cny: '99.00',
tags: [],
description: null,
physical_spec: {
length_mm: null,
width_mm: null,
@@ -707,6 +711,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
source_sku: null,
specification_values: [],
purchase_cost_cny: '99.00',
tags: [],
description: null,
physical_spec: {
length_mm: null,
width_mm: null,
@@ -805,6 +811,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
]
: variant.specification_values,
purchase_cost_cny: variant.purchase_cost_cny,
tags: variant.tags,
description: variant.description,
physical_spec:
index === 0
? {
@@ -994,6 +1002,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
source_sku: variant.source_sku,
specification_values: variant.specification_values,
purchase_cost_cny: variant.purchase_cost_cny,
tags: variant.tags,
description: variant.description,
physical_spec: variant.physical_spec,
package_spec: variant.package_spec,
offer_id: variant.listing.offer_id,
@@ -1862,6 +1872,8 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
source_sku: item.source_sku,
specification_values: item.specification_values,
purchase_cost_cny: item.purchase_cost_cny,
tags: item.tags,
description: item.description,
physical_spec: item.physical_spec,
package_spec: item.package_spec,
offer_id: item.listing.offer_id,
@@ -1955,4 +1967,66 @@ describe('商品编辑 M3.0–M3.3 HTTP 集成测试', () => {
'独立持久化后的采集商品描述',
);
});
it('俄文发布标题为空时发布就绪校验提示必填,填写后移除提示', async () => {
await database.workspaceTransaction(workspaceId, async (tx) => {
const store = await tx.query<{ id: string }>(
`SELECT id FROM atlas.stores
WHERE workspace_id = current_setting('app.workspace_id')::uuid
AND platform_id = $1 AND is_default = true
LIMIT 1`,
[platformId],
);
if (!store.rows.length)
await tx.query(
`INSERT INTO atlas.stores
(workspace_id, platform_id, name, platform_client_id,
currency_code, is_default, status, credential_status)
VALUES (current_setting('app.workspace_id')::uuid, $1,
'俄文标题测试店铺', $2, 'RUB', true, 'active', 'valid')`,
[platformId, `editor-title-${randomUUID()}`],
);
});
const created = await createCollectionItem('俄文发布标题校验商品');
const entered = await request(
`/api/collection-items/${created.collectionItemId}/editor-contexts`,
{
method: 'POST',
headers: headers({
'X-Atlas-Platform-Id': platformId,
'Idempotency-Key': randomUUID(),
}),
},
);
assert.equal(entered.response.status, 201);
const profileId = entered.body.data.profile_id as string;
const missing = await request(
`/api/product-platform-profiles/${profileId}/validation`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.equal(missing.response.status, 200);
assert.ok(
missing.body.data.profile.issues.some(
(issue: { code: string; field: string }) =>
issue.code === 'PUBLISH_TITLE_REQUIRED' &&
issue.field === 'profile.title',
),
);
await database.workspaceTransaction(workspaceId, (tx) =>
tx.query(
`UPDATE atlas.product_platform_profiles SET title = $1 WHERE id = $2`,
['Название товара', profileId],
),
);
const filled = await request(
`/api/product-platform-profiles/${profileId}/validation`,
{ headers: headers({ 'X-Atlas-Platform-Id': platformId }) },
);
assert.equal(filled.response.status, 200);
assert.ok(
!filled.body.data.profile.issues.some(
(issue: { code: string }) => issue.code === 'PUBLISH_TITLE_REQUIRED',
),
);
});
});