goods-exporter
Version:
A versatile JavaScript library for exporting goods data to various formats such as YML, CSV, and Excel. Simplify data export tasks with ease. Supports streams.
1 lines • 109 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../../src/utils/buildCategoryPath.ts","../../src/utils/writeWithDrain.ts","../../src/utils/delay.ts","../../src/utils/getRFC3339Date.ts","../../src/utils/urlQueryEncode.ts","../../src/streams/CSVStream.ts","../../src/formatter/formater.types.ts","../../src/formatter/CSV.formatter.ts","../../src/formatter/Excel.formatter.ts","../../src/formatter/Insales.formatter.ts","../../src/formatter/JSON.formatter.ts","../../src/formatter/SimpleJSON.formatter.ts","../../src/formatter/TgShop.formatter.ts","../../src/formatter/Tilda.formatter.ts","../../src/formatter/WooCommerce.formatter.ts","../../src/formatter/YML.formatter.ts","../../src/formatter/XML.formatter.ts","../../src/formatter/index.ts","../../src/exporter/goodsExporter.ts","../../src/types/Product.types.ts"],"sourcesContent":["import { type Category } from \"../types\";\r\n\r\nexport const buildCategoryPaths = (\r\n categories: Category[],\r\n): Map<number, Category[]> => {\r\n const idToCategory = new Map<number, Category>();\r\n\r\n categories.forEach((category) => {\r\n idToCategory.set(category.id, category);\r\n });\r\n\r\n const categoryPaths = new Map<number, Category[]>();\r\n\r\n categories.forEach((category) => {\r\n const path: Category[] = [];\r\n\r\n let currentCategory: Category | undefined = category;\r\n\r\n while (currentCategory) {\r\n path.unshift(currentCategory);\r\n\r\n if (currentCategory.parentId !== undefined) {\r\n currentCategory = idToCategory.get(currentCategory.parentId);\r\n } else {\r\n currentCategory = undefined;\r\n }\r\n }\r\n\r\n categoryPaths.set(category.id, path);\r\n });\r\n\r\n return categoryPaths;\r\n};\r\n","import { once } from \"events\";\r\nimport { type Writable } from \"stream\";\r\n\r\nexport const writeWithDrain = (stream: Writable) => {\r\n return async (chunk: any) => {\r\n const canWrite = stream.write(chunk);\r\n if (!canWrite) {\r\n await once(stream, \"drain\");\r\n }\r\n };\r\n};\r\n","export const delay = async (ms: number) =>\r\n await new Promise((resolve) => setTimeout(resolve, ms));\r\n","export function getRFC3339Date(date: Date): string {\r\n const pad = (n: number) => n.toString().padStart(2, \"0\");\r\n const year = date.getFullYear();\r\n const month = pad(date.getMonth() + 1);\r\n const day = pad(date.getDate());\r\n const hour = pad(date.getHours());\r\n const min = pad(date.getMinutes());\r\n\r\n // Смещение в минутах\r\n const tzOffset = -date.getTimezoneOffset();\r\n const sign = tzOffset >= 0 ? \"+\" : \"-\";\r\n const absOffset = Math.abs(tzOffset);\r\n const offsetHour = pad(Math.floor(absOffset / 60));\r\n const offsetMin = pad(absOffset % 60);\r\n\r\n return `${year}-${month}-${day}T${hour}:${min}${sign}${offsetHour}:${offsetMin}`;\r\n}","export const urlQueryEncode = (inputUrl: string): string => {\r\n try {\r\n const url = new URL(inputUrl);\r\n url.search = url.search.replace(/^\\?/, \"\").replace(/,/g, \"%2C\");\r\n return url.toString();\r\n } catch (error) {\r\n console.error(\"Invalid URL:\", error);\r\n return \"\";\r\n }\r\n};\r\n","import { writeWithDrain } from \"../utils\";\r\n\r\nimport { PassThrough } from \"stream\";\r\n\r\nexport interface CSVStreamOptions {\r\n delimiter?: string;\r\n emptyFieldValue?: string;\r\n lineSeparator?: string;\r\n}\r\n\r\nexport class CSVStream {\r\n private readonly stream: PassThrough = new PassThrough();\r\n private readonly delimiter: string = \";\";\r\n private readonly lineSeparator: string = \"\\n\";\r\n private readonly emptyFieldValue: string = \"\";\r\n private columns = new Set<string>();\r\n private readonly writer = writeWithDrain(this.stream);\r\n\r\n constructor({ delimiter, lineSeparator, emptyFieldValue }: CSVStreamOptions) {\r\n if (delimiter !== undefined) this.delimiter = delimiter;\r\n if (lineSeparator !== undefined) this.lineSeparator = lineSeparator;\r\n if (emptyFieldValue !== undefined) this.emptyFieldValue = emptyFieldValue;\r\n }\r\n\r\n public get writableStream() {\r\n return this.stream;\r\n }\r\n\r\n setColumns(columns: Set<string>) {\r\n this.columns = columns;\r\n this.stream.write(\r\n Array.from(this.columns).join(this.delimiter) + this.lineSeparator,\r\n );\r\n }\r\n\r\n async addRow(items: Record<string, any>) {\r\n const data =\r\n Array.from(this.columns)\r\n .map((key) =>\r\n items[key] === undefined ? this.emptyFieldValue : items[key] + \"\",\r\n )\r\n .join(this.delimiter) + this.lineSeparator;\r\n\r\n await this.writer(data);\r\n }\r\n}\r\n","import { type Brand, type Category, type Product } from \"../types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport abstract class FormatterAbstract {\r\n public abstract formatterName: string;\r\n public abstract fileExtension: Extension;\r\n\r\n public abstract format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n option?: FormatterOptions,\r\n ): Promise<void>;\r\n}\r\n\r\nexport interface FormatterOptions {\r\n shopName?: string;\r\n\r\n companyName?: string;\r\n\r\n splitParams?: boolean;\r\n}\r\n\r\nexport enum Extension {\r\n CSV = \"csv\",\r\n YML = \"yml\",\r\n XML = \"xml\",\r\n XLSX = \"xlsx\",\r\n JSON = \"json\",\r\n}\r\n","import { CSVStream } from \"../streams/CSVStream\";\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport class CSVFormatter implements FormatterAbstract {\r\n public formatterName = \"CSV\";\r\n public fileExtension = Extension.CSV;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, string> = {};\r\n categories?.forEach(({ id, name }) => (mappedCategories[id] = name));\r\n\r\n const csvStream = new CSVStream({\r\n delimiter: \";\",\r\n emptyFieldValue: \"\",\r\n lineSeparator: \"\\n\",\r\n });\r\n csvStream.writableStream.pipe(writableStream);\r\n const columns = new Set<string>([\r\n \"url\",\r\n \"productId\",\r\n \"parentId\",\r\n \"variantId\",\r\n \"title\",\r\n \"description\",\r\n \"vendor\",\r\n \"vendorCode\",\r\n \"category\",\r\n \"images\",\r\n \"videos\",\r\n \"timeDeliveryMin\",\r\n \"timeDeliveryMax\",\r\n \"price\",\r\n \"oldPrice\",\r\n \"purchasePrice\",\r\n \"currency\",\r\n \"saleDate\",\r\n \"countryOfOrigin\",\r\n \"tags\",\r\n \"codesTN\",\r\n \"params\",\r\n \"properties\",\r\n \"sizes\",\r\n \"keywords\",\r\n \"relatedProducts\",\r\n ]);\r\n products.forEach((product) => {\r\n Object.entries(product).forEach(([key, value]) => {\r\n if (value) columns.add(key);\r\n });\r\n });\r\n csvStream.setColumns(columns);\r\n for (const product of products) {\r\n const row: Record<string, any> = {\r\n ...product,\r\n category: mappedCategories[product.categoryId],\r\n images: product.images?.join(\",\"),\r\n videos: product.videos?.join(\",\"),\r\n tags: product.tags?.join(\",\"),\r\n codesTN: product.codesTN?.join(\", \"),\r\n params: product.params\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n properties: product.properties\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n sizes: product.sizes\r\n ?.map(({ name, value }) => `${name}=${value}`)\r\n .join(\", \"),\r\n keywords: product.keywords?.join(\",\"),\r\n relatedProducts: product.relatedProducts?.join(\",\"),\r\n timeDeliveryMin: product.timeDelivery?.min,\r\n timeDeliveryMax: product.timeDelivery?.max,\r\n };\r\n await csvStream.addRow(row);\r\n }\r\n\r\n // Закрываем поток\r\n csvStream.writableStream.end();\r\n }\r\n}\r\n","import pkg from \"exceljs\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\nconst { stream } = pkg;\r\n\r\nexport class ExcelFormatter implements FormatterAbstract {\r\n public formatterName = \"Excel\";\r\n public fileExtension = Extension.XLSX;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, string> = {};\r\n categories?.forEach(({ id, name }) => (mappedCategories[id] = name));\r\n const columns = new Set<string>([\r\n \"url\",\r\n \"productId\",\r\n \"parentId\",\r\n \"variantId\",\r\n \"title\",\r\n \"description\",\r\n \"vendor\",\r\n \"vendorCode\",\r\n \"category\",\r\n \"images\",\r\n \"videos\",\r\n \"timeDeliveryMin\",\r\n \"timeDeliveryMax\",\r\n \"price\",\r\n \"oldPrice\",\r\n \"purchasePrice\",\r\n \"currency\",\r\n \"saleDate\",\r\n \"countryOfOrigin\",\r\n \"tags\",\r\n \"codesTN\",\r\n \"params\",\r\n \"properties\",\r\n \"sizes\",\r\n \"keywords\",\r\n \"relatedProducts\",\r\n ]);\r\n products.forEach((product) => {\r\n Object.entries(product).forEach(([key, value]) => {\r\n if (value) columns.add(key);\r\n });\r\n });\r\n\r\n const workbook = new stream.xlsx.WorkbookWriter({\r\n stream: writableStream,\r\n });\r\n const worksheet = workbook.addWorksheet(\"products\");\r\n worksheet.columns = Array.from(columns).map((column) => ({\r\n key: column,\r\n header: column,\r\n }));\r\n\r\n products.forEach((product) => {\r\n const row = {\r\n ...product,\r\n category: mappedCategories[product.categoryId],\r\n images: product.images?.join(\",\"),\r\n videos: product.videos?.join(\",\"),\r\n tags: product.tags?.join(\",\"),\r\n keywords: product.keywords?.join(\",\"),\r\n relatedProducts: product.relatedProducts?.join(\",\"),\r\n codesTN: product.codesTN?.join(\", \"),\r\n params: product.params\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n properties: product.properties\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n sizes: product.sizes\r\n ?.map(({ name, value }) => `${name}=${value}`)\r\n .join(\", \"),\r\n timeDeliveryMin: product.timeDelivery?.min,\r\n timeDeliveryMax: product.timeDelivery?.max,\r\n };\r\n worksheet.addRow(row).commit();\r\n });\r\n worksheet.commit();\r\n await workbook.commit();\r\n }\r\n}\r\n","import pkg from \"exceljs\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nconst { stream } = pkg;\r\n\r\nexport class InsalesFormatter implements FormatterAbstract {\r\n public formatterName = \"Insales\";\r\n public fileExtension = Extension.XLSX;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, Category> = {};\r\n categories?.forEach(\r\n (category) => (mappedCategories[category.id] = category),\r\n );\r\n\r\n const getParams = (product: Product): Record<string, string> => {\r\n const properties: Record<string, string> = {};\r\n\r\n product.params?.forEach(\r\n (p) => (properties[`Свойство: ${p.key}`] = p.value),\r\n );\r\n\r\n return properties;\r\n };\r\n const getProperties = (product: Product): Record<string, string> => {\r\n const properties: Record<string, string> = {};\r\n\r\n product.properties?.forEach(\r\n (p) => (properties[`Параметр: ${p.key}`] = p.value),\r\n );\r\n\r\n return properties;\r\n };\r\n\r\n const getCategories = (product: Product) => {\r\n const categories: Record<string, string> = {};\r\n const categoryList = new Array<string>();\r\n\r\n function addCategory(categoryId: number | undefined) {\r\n if (categoryId === undefined) return;\r\n\r\n const category = mappedCategories[categoryId];\r\n if (category) {\r\n categoryList.push(category.name);\r\n addCategory(category.parentId);\r\n }\r\n }\r\n\r\n addCategory(product.categoryId);\r\n\r\n categoryList.forEach((name, i) => {\r\n const index = categoryList.length - 1 - i;\r\n const key = index === 0 ? \"Корневая\" : `Подкатегория ${index}`;\r\n categories[key] = name;\r\n });\r\n\r\n return categories;\r\n };\r\n const workbook = new stream.xlsx.WorkbookWriter({\r\n stream: writableStream,\r\n });\r\n const worksheet = workbook.addWorksheet(\"products\");\r\n const columns = new Set<string>([\r\n \"Внешний ID\",\r\n \"Ссылка на товар\",\r\n \"Артикул\",\r\n \"Корневая\",\r\n \"Подкатегория 1\",\r\n \"Подкатегория 2\",\r\n \"Название товара или услуги\",\r\n \"Время доставки: Минимальное\",\r\n \"Время доставки: Максимальное\",\r\n \"Старая цена\",\r\n \"Цена продажи\",\r\n \"Cебестоимость\",\r\n \"Категории\",\r\n \"Остаток\",\r\n \"Штрих-код\",\r\n \"Краткое описание\",\r\n \"Полное описание\",\r\n \"Габариты варианта\",\r\n \"Вес\",\r\n \"Размещение на сайте\",\r\n \"НДС\",\r\n \"Валюта склада\",\r\n \"Изображения варианта\",\r\n \"Изображения\",\r\n \"Ссылка на видео\",\r\n \"Параметр: Артикул\",\r\n \"Параметры\",\r\n \"Свойства\",\r\n \"Размерная сетка\",\r\n \"Связанные товары\",\r\n \"Ключевые слова\",\r\n ]);\r\n products.forEach((product) => {\r\n Object.keys({\r\n ...getParams(product),\r\n ...getProperties(product),\r\n }).forEach((key) => {\r\n columns.add(key);\r\n });\r\n });\r\n\r\n worksheet.columns = Array.from(columns).map((column) => ({\r\n header: column,\r\n key: column,\r\n }));\r\n\r\n for (const product of products) {\r\n const externalId = `${product.productId}-${product.variantId}`;\r\n const row = {\r\n \"Внешний ID\": externalId,\r\n \"Ссылка на товар\": product.url,\r\n Артикул: externalId, // TODO: product.vendorCode,\r\n \"Название товара или услуги\": product.title,\r\n \"Время доставки: Минимальное\": product.timeDelivery?.min,\r\n \"Время доставки: Максимальное\": product.timeDelivery?.max,\r\n \"Старая цена\": product.oldPrice,\r\n \"Цена продажи\": product.price,\r\n Cебестоимость: product.purchasePrice,\r\n ...getCategories(product),\r\n Остаток: product.count,\r\n \"Штрих-код\": product.barcode,\r\n \"Краткое описание\": undefined,\r\n \"Полное описание\": product.description,\r\n \"Габариты варианта\": product.dimensions,\r\n Вес: product.weight,\r\n \"Размещение на сайте\": product.available,\r\n НДС: product.vat?.toString(),\r\n \"Валюта склада\": product.currency.toString(),\r\n \"Изображения варианта\":\r\n product.parentId === undefined\r\n ? product.images?.join(\" \")\r\n : undefined,\r\n Изображения:\r\n product.parentId === undefined\r\n ? undefined\r\n : product.images?.join(\" \"),\r\n \"Ссылка на видео\": product.videos ? product.videos[0] : undefined,\r\n \"Параметр: Артикул\": product.vendorCode, // TODO: брать из обычных параметров\r\n ...getParams(product),\r\n ...getProperties(product),\r\n \"Размерная сетка\": JSON.stringify(product.sizes),\r\n \"Связанные товары\": product.relatedProducts?.join(\",\"),\r\n \"Ключевые слова\": product.keywords?.join(\",\"),\r\n };\r\n // todo(delay)\r\n worksheet.addRow(row).commit();\r\n }\r\n\r\n worksheet.commit();\r\n await workbook.commit();\r\n }\r\n}\r\n","import { JsonStreamStringify } from \"json-stream-stringify\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport class JSONFormatter implements FormatterAbstract {\r\n public formatterName = \"JSON\";\r\n public fileExtension = Extension.JSON;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n _?: FormatterOptions,\r\n ): Promise<void> {\r\n const stream = new JsonStreamStringify({\r\n categories,\r\n brands,\r\n products,\r\n });\r\n stream.pipe(writableStream);\r\n }\r\n}\r\n","import { JsonStreamStringify } from \"json-stream-stringify\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\ninterface SimpleProduct extends Product {\r\n children: Product[];\r\n}\r\n\r\nexport class SimpleJSONFormatter implements FormatterAbstract {\r\n public formatterName = \"JSON\";\r\n public fileExtension = Extension.JSON;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n _?: FormatterOptions,\r\n ): Promise<void> {\r\n const groupedProduct = new Map<number, SimpleProduct>();\r\n products.forEach((product) => {\r\n if (product.parentId !== undefined) return;\r\n groupedProduct.set(product.variantId, {\r\n ...product,\r\n children: [],\r\n });\r\n });\r\n products.forEach((product) => {\r\n if (product.parentId === undefined) return;\r\n const parent = groupedProduct.get(product.parentId);\r\n if (!parent) return;\r\n parent.children.push(product);\r\n });\r\n const stream = new JsonStreamStringify({\r\n categories,\r\n brands,\r\n products: Array.from(groupedProduct.values()),\r\n });\r\n stream.pipe(writableStream);\r\n }\r\n}\r\n","import pkg from \"exceljs\";\r\n\r\nimport { type Brand, type Category, type IParam, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\nconst { stream } = pkg;\r\n\r\nexport class TgShopFormatter implements FormatterAbstract {\r\n public formatterName = \"TgShop\";\r\n public fileExtension = Extension.XLSX;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const getParameter = (product: Product, key: string): IParam | undefined =>\r\n product.params?.find((value) => value.key === key);\r\n\r\n const convertProduct = (product: Product) => ({\r\n \"category id\": product.categoryId,\r\n \"group id\": product.parentId,\r\n \"id product\": product.variantId,\r\n \"name product\": product.title,\r\n price: product.price,\r\n picture: product.images?.join(\", \"),\r\n vendorCode: product.vendorCode,\r\n oldprice: product.oldPrice,\r\n description: product.description,\r\n shortDescription: \"\",\r\n quantityInStock: product.count,\r\n color: getParameter(product, \"color\")?.value,\r\n size: getParameter(product, \"size\")?.value,\r\n priority: undefined,\r\n });\r\n const workbook = new stream.xlsx.WorkbookWriter({\r\n stream: writableStream,\r\n });\r\n const categoryWorksheet = workbook.addWorksheet(\"categories\");\r\n const productsWorksheet = workbook.addWorksheet(\"offers\");\r\n categoryWorksheet.columns = [\r\n {\r\n header: \"id\",\r\n key: \"id\",\r\n },\r\n {\r\n header: \"parentId\",\r\n key: \"parentId\",\r\n },\r\n {\r\n header: \"name\",\r\n key: \"name\",\r\n },\r\n ];\r\n const columns = [\r\n \"category id\",\r\n \"group id\",\r\n \"id product\",\r\n \"name product\",\r\n \"price\",\r\n \"picture\",\r\n \"vendorCode\",\r\n \"oldprice\",\r\n \"description\",\r\n \"shortDescription\",\r\n \"quantityInStock\",\r\n \"color\",\r\n \"size\",\r\n \"priority\",\r\n ];\r\n\r\n productsWorksheet.columns = columns.map((column) => ({\r\n header: column,\r\n key: column,\r\n }));\r\n\r\n categories?.forEach((category) => {\r\n categoryWorksheet.addRow(category).commit();\r\n });\r\n\r\n products.forEach((product) => {\r\n productsWorksheet.addRow(convertProduct(product)).commit();\r\n });\r\n categoryWorksheet.commit();\r\n productsWorksheet.commit();\r\n\r\n await workbook.commit();\r\n }\r\n}\r\n","import { CSVStream } from \"../streams/CSVStream\";\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport { urlQueryEncode } from \"../utils\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport class TildaFormatter implements FormatterAbstract {\r\n public formatterName = \"Tilda\";\r\n public fileExtension = Extension.CSV;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, string> = {};\r\n categories?.forEach(({ id, name }) => (mappedCategories[id] = name));\r\n\r\n const csvStream = new CSVStream({\r\n delimiter: \"\\t\",\r\n emptyFieldValue: \"\",\r\n lineSeparator: \"\\n\",\r\n });\r\n csvStream.writableStream.pipe(writableStream);\r\n const columns = new Set<string>([\r\n \"SKU\",\r\n \"Brand\",\r\n \"Category\",\r\n \"Title\",\r\n \"Text\",\r\n \"Photo\",\r\n \"Price\",\r\n \"Price Old\",\r\n \"Quantity\",\r\n \"Editions\",\r\n \"External ID\",\r\n \"Parent UID\",\r\n ]);\r\n\r\n const characteristics = new Set<string>();\r\n\r\n products.forEach((product) => {\r\n product.properties?.forEach(({ key }) => {\r\n characteristics.add(key);\r\n });\r\n });\r\n\r\n characteristics.forEach((charKey) => {\r\n columns.add(`Characteristics:${charKey}`);\r\n });\r\n\r\n csvStream.setColumns(columns);\r\n for (const product of products) {\r\n const row: Record<string, string | number | undefined> = {\r\n SKU: product.vendorCode,\r\n Brand: product.vendor,\r\n Category: mappedCategories[product.categoryId],\r\n Title: product.title,\r\n Text: product.description,\r\n Photo: product.images?.map(urlQueryEncode).join(\",\"),\r\n Price: product.price,\r\n \"Price Old\": product.oldPrice,\r\n Quantity: product.count,\r\n Editions: product.params\r\n ?.map(({ key, value }) => `${key}:${value}`)\r\n .join(\";\"),\r\n \"External ID\": product.variantId,\r\n \"Parent UID\": product.parentId,\r\n };\r\n\r\n product.properties?.forEach(({ key, value }) => {\r\n row[`Characteristics:${key}`] = value;\r\n });\r\n\r\n await csvStream.addRow(row);\r\n }\r\n\r\n csvStream.writableStream.end();\r\n }\r\n}\r\n","import { CSVStream } from \"../streams/CSVStream\";\r\nimport { type IParam, type Brand, type Category, type Product } from \"../types\";\r\nimport { buildCategoryPaths, urlQueryEncode } from \"../utils\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport interface CreateAttributeProps {\r\n name?: string;\r\n id?: number;\r\n values?: string | number;\r\n visible?: number;\r\n global?: number;\r\n}\r\n\r\nexport class WooCommerceFormatter implements FormatterAbstract {\r\n public formatterName = \"WooCommerce\";\r\n public fileExtension = Extension.CSV;\r\n\r\n private readonly DEFAULT_COLUMNS = [\r\n \"ID\",\r\n \"Type\",\r\n \"SKU\",\r\n \"Name\",\r\n \"Parent\",\r\n \"Short description\",\r\n \"Description\",\r\n \"Stock\",\r\n \"Regular price\",\r\n \"Position\",\r\n \"Categories\",\r\n \"Tags\",\r\n \"Images\",\r\n \"SizeGrid\",\r\n ];\r\n\r\n private formatKeyAttribute(key: string) {\r\n const keyWithoutInvalidCharacters = key\r\n .replaceAll(\",\", \"\")\r\n .replaceAll(\";\", \"\");\r\n\r\n return keyWithoutInvalidCharacters.length >= 28\r\n ? keyWithoutInvalidCharacters.slice(0, 24) + \"...\"\r\n : keyWithoutInvalidCharacters;\r\n }\r\n\r\n private formatValueAttribute(value: string) {\r\n return value.replaceAll(\",\", \"\").replaceAll(\";\", \"\");\r\n }\r\n\r\n private formatProducts(products: Product[]) {\r\n const formatParams = (params: IParam[] | undefined) => {\r\n return params?.map(({ key, value }) => {\r\n const formatedKey = this.formatKeyAttribute(key);\r\n const formatedValue = this.formatValueAttribute(value);\r\n return { key: formatedKey, value: formatedValue };\r\n });\r\n };\r\n\r\n return products.map((product) => {\r\n const params = formatParams(product.params);\r\n const properties = formatParams(product.properties);\r\n return { ...product, params, properties };\r\n });\r\n }\r\n\r\n private createAttribute(data: CreateAttributeProps) {\r\n if (!data?.name || data.id === undefined) return;\r\n\r\n const attributeStartName = \"Attribute\";\r\n\r\n const attribute: Record<string, string | number> = {};\r\n\r\n attribute[`${attributeStartName} ${data.id} name`] = data.name;\r\n\r\n if (data.values !== undefined)\r\n attribute[`${attributeStartName} ${data.id} value(s)`] = data.values;\r\n if (data.visible !== undefined)\r\n attribute[`${attributeStartName} ${data.id} visible`] = data.visible;\r\n if (data.global !== undefined)\r\n attribute[`${attributeStartName} ${data.id} global`] = data.global;\r\n\r\n return attribute;\r\n }\r\n\r\n private extractAttributes(products: Product[]) {\r\n const formatedProducts = this.formatProducts(products);\r\n const paramsMap = new Map<number, Record<string, string | number>>();\r\n const propertiesMap = new Map<number, Record<string, string | number>>();\r\n const uniqueAttributes = new Map<string, number>();\r\n\r\n const genderTitle = \"Пол\";\r\n\r\n formatedProducts.forEach((product) => {\r\n product.params?.forEach(({ key }) => {\r\n if (!uniqueAttributes.has(key)) {\r\n uniqueAttributes.set(key, uniqueAttributes.size);\r\n }\r\n });\r\n\r\n uniqueAttributes.set(genderTitle, uniqueAttributes.size);\r\n\r\n product.properties?.forEach(({ key }) => {\r\n if (!uniqueAttributes.has(key)) {\r\n uniqueAttributes.set(key, uniqueAttributes.size);\r\n }\r\n });\r\n });\r\n\r\n formatedProducts.forEach((product) => {\r\n const paramAttributes = paramsMap.get(product.variantId) ?? {};\r\n const propertyAttributes = propertiesMap.get(product.variantId) ?? {};\r\n\r\n product.params?.forEach(({ key, value }) => {\r\n const index = uniqueAttributes.get(key);\r\n\r\n if (index === undefined) {\r\n console.error(`Не нашлось уникального ключа для параметра - ${key}`);\r\n return;\r\n }\r\n\r\n const attribute = this.createAttribute({\r\n name: key,\r\n id: index,\r\n values: value,\r\n visible: 0,\r\n global: 0,\r\n });\r\n\r\n if (!attribute) return;\r\n\r\n Object.entries(attribute).forEach(\r\n ([key, value]) => (paramAttributes[key] = value),\r\n );\r\n });\r\n\r\n const genderIndex = uniqueAttributes.get(genderTitle);\r\n\r\n const genderAttribute = this.createAttribute({\r\n name: genderTitle,\r\n id: genderIndex,\r\n values: product.gender,\r\n global: 0,\r\n });\r\n\r\n if (genderAttribute) {\r\n Object.entries(genderAttribute).forEach(\r\n ([key, value]) => (propertyAttributes[key] = value),\r\n );\r\n }\r\n\r\n product.properties?.forEach(({ key, value }) => {\r\n const index = uniqueAttributes.get(key);\r\n\r\n if (index === undefined) {\r\n console.error(`Не нашлось уникального ключа для параметра - ${key}`);\r\n return;\r\n }\r\n\r\n if (paramAttributes[`Attribute ${index} name`]) {\r\n console.warn(`Данное свойство уже существует в параметрах - ${key}`);\r\n return;\r\n }\r\n\r\n const attribute = this.createAttribute({\r\n name: key,\r\n id: index,\r\n values: value,\r\n global: 0,\r\n });\r\n\r\n if (!attribute) return;\r\n\r\n Object.entries(attribute).forEach(\r\n ([key, value]) => (propertyAttributes[key] = value),\r\n );\r\n });\r\n\r\n paramsMap.set(product.variantId, paramAttributes);\r\n propertiesMap.set(product.variantId, propertyAttributes);\r\n });\r\n\r\n return { params: paramsMap, properties: propertiesMap };\r\n }\r\n\r\n private removeVisibleFromAttributes(params: Record<string, string | number>) {\r\n Object.entries(params).forEach(([key]) => {\r\n if (key.includes(\"visible\")) params[key] = \"\";\r\n });\r\n }\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const categoryPaths = buildCategoryPaths(categories ?? []);\r\n\r\n const csvStream = new CSVStream({\r\n delimiter: \";\",\r\n emptyFieldValue: \"\",\r\n lineSeparator: \"\\n\",\r\n });\r\n csvStream.writableStream.pipe(writableStream);\r\n const columns = new Set<string>(this.DEFAULT_COLUMNS);\r\n\r\n const attributes = this.extractAttributes(products);\r\n\r\n const variationsByParentId = new Map<\r\n number,\r\n Array<Record<string, string | number | undefined>>\r\n >();\r\n\r\n const imagesByParentId = new Map<number, string | undefined>();\r\n const sizesByParentId = new Map<number, string | undefined>();\r\n\r\n const variations = products.map((product, index) => {\r\n const pathsArray = categoryPaths\r\n .get(product.categoryId)\r\n ?.map((category) => category.name);\r\n\r\n const price = product.price ? product.price : \"\";\r\n const images = product.images?.map(urlQueryEncode).join(\",\");\r\n\r\n let row = {\r\n ID: product.variantId,\r\n Type: \"variation\",\r\n SKU: product.variantId,\r\n Name: product.title,\r\n Parent: product.parentId ?? 0,\r\n \"Short description\": \"\",\r\n Description: product.description,\r\n Stock: product.count ?? 0,\r\n \"Regular price\": price,\r\n Position: index + 1,\r\n Categories: pathsArray?.join(\" > \"),\r\n Tags: product.keywords?.join(\",\"),\r\n Images: images,\r\n SizeGrid: \"\",\r\n };\r\n\r\n const productParams = attributes.params.get(product.variantId) ?? {};\r\n\r\n if (!imagesByParentId.has(row.Parent)) {\r\n imagesByParentId.set(row.Parent, images);\r\n }\r\n\r\n if (!sizesByParentId.has(row.Parent)) {\r\n sizesByParentId.set(\r\n row.Parent,\r\n product.sizes ? JSON.stringify(product.sizes) : \"\",\r\n );\r\n }\r\n\r\n this.removeVisibleFromAttributes(productParams);\r\n\r\n row = { ...row, ...productParams };\r\n\r\n if (variationsByParentId.has(row.Parent)) {\r\n variationsByParentId.get(row.Parent)?.push(row);\r\n } else {\r\n variationsByParentId.set(row.Parent, [row]);\r\n }\r\n\r\n return row;\r\n });\r\n\r\n const parentProducts = new Map<number, any>();\r\n\r\n variations.forEach((product) => {\r\n const currentParent = parentProducts.get(product.Parent);\r\n\r\n let row = {\r\n ...product,\r\n Type: \"variable\",\r\n ID: product.Parent,\r\n SKU: product.Parent,\r\n Position: 0,\r\n Parent: \"\",\r\n \"Regular price\": \"\",\r\n Images: imagesByParentId.get(product.Parent) ?? \"\",\r\n SizeGrid: sizesByParentId.get(product.Parent) ?? \"\",\r\n };\r\n\r\n row.Stock = (currentParent?.Stock || 0) + (product?.Stock || 0);\r\n\r\n const productParams = attributes.params.get(product.SKU) ?? {};\r\n const productProperties = attributes.properties.get(product.SKU) ?? {};\r\n\r\n Object.entries(productParams).forEach(([key]) => {\r\n if (key.includes(\"visible\")) productParams[key] = 0;\r\n });\r\n\r\n if (currentParent) {\r\n Object.entries(productParams).forEach(([key, value]) => {\r\n if (key.includes(\"value(s)\")) {\r\n productParams[key] = currentParent[key] + `, ${value}`;\r\n }\r\n });\r\n }\r\n\r\n Object.keys({ ...row, ...productParams, ...productProperties }).forEach(\r\n (item) => columns.add(item),\r\n );\r\n\r\n row = { ...row, ...productParams, ...productProperties };\r\n\r\n parentProducts.set(product.Parent, row);\r\n });\r\n\r\n const variableProducts = Array.from(parentProducts.values());\r\n\r\n csvStream.setColumns(columns);\r\n for (const parentProduct of variableProducts) {\r\n await csvStream.addRow(parentProduct);\r\n\r\n for (const variationProduct of variationsByParentId.get(\r\n parentProduct.ID,\r\n ) ?? []) {\r\n await csvStream.addRow(variationProduct);\r\n }\r\n }\r\n\r\n csvStream.writableStream.end();\r\n }\r\n}\r\n","import { XMLBuilder } from \"fast-xml-parser\";\r\n\r\nimport { type Product, type Category, type Brand } from \"../types\";\r\nimport { getRFC3339Date, writeWithDrain } from \"../utils\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { PassThrough, type Writable } from \"stream\";\r\n\r\nexport class YMLFormatter implements FormatterAbstract {\r\n public formatterName = \"YMl\";\r\n public fileExtension = Extension.YML;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n options?: FormatterOptions,\r\n ): Promise<void> {\r\n const result = new PassThrough();\r\n result.pipe(writableStream);\r\n\r\n const builder = new XMLBuilder({\r\n ignoreAttributes: false,\r\n cdataPropName: \"__cdata\",\r\n format: true,\r\n indentBy: \" \",\r\n });\r\n\r\n const date = getRFC3339Date(new Date());\r\n\r\n // Начинаем формирование XML\r\n result.write('<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n');\r\n result.write('<yml_catalog date=\"' + date + '\">\\n');\r\n\r\n // Открываем тег <shop>\r\n result.write(\"<shop>\\n\");\r\n\r\n const resultWriter = writeWithDrain(result);\r\n // Добавляем информацию о магазине\r\n if (options?.shopName) {\r\n await resultWriter(builder.build({ name: options.shopName }));\r\n await resultWriter(\"\\n\");\r\n }\r\n if (options?.companyName) {\r\n await resultWriter(builder.build({ company: options.companyName }));\r\n await resultWriter(\"\\n\");\r\n }\r\n\r\n // Добавляем категории и бренды\r\n if (categories) {\r\n await resultWriter(\r\n builder.build({\r\n // tagname: \"categories\",\r\n categories: { category: this.getCategories(categories) },\r\n }),\r\n );\r\n await resultWriter(\"\\n\");\r\n }\r\n if (brands) {\r\n await resultWriter(\r\n builder.build({ brands: { brand: this.getBrands(brands) } }),\r\n );\r\n await resultWriter(\"\\n\");\r\n }\r\n\r\n // Открываем секцию <offers>\r\n await resultWriter(\"<offers>\\n\");\r\n\r\n // Создаем поток для обработки offer элементов\r\n const offerStream = new PassThrough();\r\n const offerWriter = writeWithDrain(offerStream);\r\n\r\n // Пайпим поток offer элементов в основной итоговый поток\r\n offerStream.pipe(result, { end: false });\r\n\r\n // Записываем каждый продукт в поток\r\n for (const product of products) {\r\n if (product.price === 0) continue;\r\n const offer = builder.build({ offer: this.getOffer(product) });\r\n await offerWriter(offer + \"\\n\");\r\n }\r\n\r\n // Завершаем поток offer\r\n offerStream.end();\r\n\r\n offerStream.on(\"end\", () => {\r\n // Закрываем секцию <offers>\r\n result.write(\"</offers>\\n\");\r\n\r\n // Закрываем тег <shop>\r\n result.write(\"</shop>\\n\");\r\n\r\n // Закрываем тег <yml_catalog>\r\n result.write(\"</yml_catalog>\\n\");\r\n\r\n // Завершаем итоговый поток\r\n result.end();\r\n });\r\n }\r\n\r\n private getBrands(brands?: Brand[]) {\r\n if (!brands) return [];\r\n\r\n return brands.map((brand) => ({\r\n \"@_id\": brand.id,\r\n \"@_url\": brand.coverURL ?? \"\",\r\n \"#text\": brand.name,\r\n }));\r\n }\r\n\r\n private getCategories(categories?: Category[]) {\r\n if (!categories) return [];\r\n\r\n return categories.map((cat) => ({\r\n \"@_id\": cat.id,\r\n \"@_parentId\": cat.parentId ?? \"\",\r\n \"#text\": cat.name || `Категория #${cat.id}`,\r\n }));\r\n }\r\n\r\n private getOffer(product: Product): any {\r\n const result = {\r\n \"@_id\": product.variantId,\r\n name: product.title,\r\n price: product.price,\r\n oldprice: product.oldPrice,\r\n purchase_price: product.purchasePrice,\r\n additional_expenses: product.additionalExpenses,\r\n cofinance_price: product.cofinancePrice,\r\n currencyId: product.currency,\r\n categoryId: product.categoryId,\r\n vendorId: product.vendorId,\r\n vendor: product.vendor,\r\n vendorCode: product.vendorCode,\r\n picture: product.images,\r\n video: product.videos,\r\n available: product.available,\r\n \"time-delivery\": product.timeDelivery\r\n ? {\r\n \"@_min\": product.timeDelivery.min,\r\n \"@_max\": product.timeDelivery.max,\r\n \"#text\": `${product.timeDelivery.min}-${product.timeDelivery.max}`,\r\n }\r\n : undefined,\r\n series: product.seriesName,\r\n \"min-quantity\": product.minQuantity,\r\n \"step-quantity\": product.stepQuantity,\r\n size: product.sizes?.map((size) => ({\r\n \"#text\": size.value,\r\n \"@_name\": size.name,\r\n \"@_delimiter\": size.delimiter,\r\n })),\r\n keyword: product.keywords,\r\n saleDate: product.saleDate,\r\n property: product.properties?.map((property) => ({\r\n \"#text\": property.value,\r\n \"@_name\": property.key,\r\n })),\r\n param: product.params?.map((param) => ({\r\n \"#text\": param.value,\r\n \"@_name\": param.key,\r\n })),\r\n description: {\r\n __cdata: product.description,\r\n },\r\n country_of_origin: product.countryOfOrigin,\r\n barcode: product.barcode,\r\n vat: product.vat,\r\n count: product.count,\r\n \"set-ids\": product.tags?.join(\", \"),\r\n adult: product.adult,\r\n downloadable: product.downloadable,\r\n \"period-of-validity-days\": product.validityPeriod,\r\n \"comment-validity-days\": product.validityComment,\r\n \"service-life-days\": product.serviceLifePeriod,\r\n \"comment-life-days\": product.serviceLifeComment,\r\n \"warranty-days\": product.warrantyPeriod,\r\n \"comment-warranty\": product.warrantyComment,\r\n manufacturer_warranty: product.manufacturerWarranty,\r\n certificate: product.certificate,\r\n url: product.url,\r\n weight: product.weight,\r\n dimensions: product.dimensions,\r\n boxCount: product.boxCount,\r\n disabled: product.disabled,\r\n age: product.age\r\n ? {\r\n \"@_unit\": product.age.unit,\r\n \"#text\": product.age.value,\r\n }\r\n : undefined,\r\n \"tn-ved-codes\": product.codesTN?.length\r\n ? {\r\n \"tn-ved-code\": product.codesTN,\r\n }\r\n : undefined,\r\n relatedProduct: product.relatedProducts,\r\n gender: product.gender,\r\n };\r\n if (product.parentId !== undefined) {\r\n return {\r\n ...result,\r\n \"@_group_id\": product.parentId,\r\n };\r\n }\r\n return result;\r\n }\r\n}\r\n","import { Extension } from \"./formater.types\";\r\nimport { YMLFormatter } from \"./YML.formatter\";\r\n\r\nexport class XMLFormatter extends YMLFormatter {\r\n public formatterName = \"XML\";\r\n public fileExtension = Extension.XML;\r\n}\r\n","import { CSVFormatter } from \"./CSV.formatter\";\r\nimport { ExcelFormatter } from \"./Excel.formatter\";\r\nimport { InsalesFormatter } from \"./Insales.formatter\";\r\nimport { JSONFormatter } from \"./JSON.formatter\";\r\nimport { SimpleJSONFormatter } from \"./SimpleJSON.formatter\";\r\nimport { TgShopFormatter } from \"./TgShop.formatter\";\r\nimport { TildaFormatter } from \"./Tilda.formatter\";\r\nimport { WooCommerceFormatter } from \"./WooCommerce.formatter\";\r\nimport { XMLFormatter } from \"./XML.formatter\";\r\nimport { YMLFormatter } from \"./YML.formatter\";\r\n\r\nexport * from \"./formater.types\";\r\n\r\nexport const Formatters = {\r\n TildaFormatter,\r\n CSVFormatter,\r\n InsalesFormatter,\r\n YMLFormatter,\r\n TgShopFormatter,\r\n ExcelFormatter,\r\n JSONFormatter,\r\n SimpleJSONFormatter,\r\n XMLFormatter,\r\n WooCommerceFormatter,\r\n};\r\n","import {\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n Formatters,\r\n} from \"../formatter\";\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport { type Exporter, type Transformer } from \"./exporter.types\";\r\n\r\nimport fs from \"fs\";\r\n\r\nexport class GoodsExporter<Context extends object | undefined> {\r\n private _context: Context;\r\n\r\n constructor(readonly context: Context) {\r\n this._context = context;\r\n }\r\n\r\n public setContext(context: Context): void {\r\n this._context = context;\r\n }\r\n\r\n private formatter: FormatterAbstract = new Formatters.YMLFormatter();\r\n private exporter: Exporter = () => {\r\n return fs.createWriteStream(\r\n `${this.formatter.formatterName}.output.${this.formatter.fileExtension}`,\r\n );\r\n };\r\n\r\n private transformers = new Array<Transformer<Context>>();\r\n\r\n public setTransformers(transformers: Array<Transformer<Context>>): void {\r\n this.transformers = transformers;\r\n }\r\n\r\n public setFormatter(formatter: FormatterAbstract): void {\r\n this.formatter = formatter;\r\n }\r\n\r\n public setExporter(exporter: Exporter): void {\r\n this.exporter = exporter;\r\n }\r\n\r\n async export(\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n option?: FormatterOptions,\r\n ): Promise<void> {\r\n let transformedProducts: Product[] = products;\r\n\r\n for (const transformer of this.transformers)\r\n transformedProducts = await transformer(\r\n transformedProducts,\r\n this._context,\r\n );\r\n\r\n const writableStream = this.exporter();\r\n\r\n await this.formatter.format(\r\n writableStream,\r\n transformedProducts,\r\n categories,\r\n brands,\r\n option,\r\n );\r\n }\r\n}\r\n","export interface Product {\r\n /**\r\n * **ID товара**\r\n *\r\n * Любая последовательность длиной до 80 знаков. В нее могут входить английские и русские (кроме ё) буквы, цифры и символы . , / \\ ( ) [ ] - = _\r\n *\r\n * Пример: belaya-kofta-12345\r\n */\r\n productId: number;\r\n /**\r\n * **Родительскй SKU**\r\n *\r\n * Любая последовательность длиной до 80 знаков. В нее могут входить английские и русские (кроме ё) буквы, цифры и символы . , / \\ ( ) [ ] - = _\r\n *\r\n * Пример: belaya-kofta-12345\r\n */\r\n parentId?: number;\r\n /**\r\n * **SKU**\r\n *\r\n * Любая последовательность длиной до 80 знаков. В нее могут входить английские и русские (кроме ё) буквы, цифры и символы . , / \\ ( ) [ ] - = _\r\n *\r\n * Пример: belaya-kofta-12345\r\n */\r\n variantId: number;\r\n /**\r\n * **Название**\r\n *\r\n * Составляйте название по схеме: тип + бренд или производитель + модель + особенности, если есть (например, цвет, размер или вес) и количество в упаковке.\r\n *\r\n * Не включайте в название условия продажи (например, «скидка», «бесплатная доставка» и т. д.), эмоциональные характеристики («хит», «супер» и т. д.). Не пишите слова большими буквами — кроме устоявшихся названий брендов и моделей.\r\n *\r\n * Оптимальная длина — 50–60 символов, максимальная — 150.\r\n *\r\n * Составлять хорошие названия помогут [рекомендации](https://yandex.ru/support/marketplace/assortment/fields/title.html).\r\n *\r\n * Пример: Ударная дрель Makita HP1630, 710 Вт\r\n */\r\n title: string;\r\n /**\r\n * **Описание**\r\n *\r\n * Подробное описание товара: например, его преимущества и особенности.\r\n *\r\n * Не давайте в описании инструкций по установке и сборке. Не используйте слова «скидка», «распродажа», «дешевый», «подарок» (кроме подарочных категорий), «бесплатно», «акция», «специальная цена», «новинка», «new», «аналог», «заказ», «хит». Не указывайте никакой контактной информации и не давайте ссылок.\r\n *\r\n * Можно использовать теги:\r\n *\r\n ** <h>, <h1>, <h2> и так далее — для заголовков;\r\n ** <br> и <p> — для переноса строки;\r\n ** <ol> — для нумерованного списка;\r\n ** <ul> — для маркированного списка;\r\n ** <li> — для создания элементов списка (должен находиться внутри <ol> или <ul>);\r\n ** <div> — поддерживается, но не влияет на отображение текста.\r\n * Оптимальная длина — 400–600 символов, максимальная — 6000.\r\n *\r\n * Составить хорошее описание помогут рекомендации.\r\n *\r\n * Пример: В комплекте с детским микроскопом есть все, что нужно вашему ребенку для изучения микромира\r\n */\r\n description: string;\r\n /**\r\n * **Бренд**\r\n *\r\n * Название бренда или производителя.\r\n *\r\n * Записывайте название так, как его пишет сам бренд.\r\n *\r\n * Пример: LEVENHUK\r\n */\r\n vendor?: string;\r\n /**\r\n * **Артикул производителя**\r\n *\r\n * Код товара, который ему присвоил производитель.\r\n *\r\n * Если артикулов несколько, укажите их через запятую.\r\n *\r\n * Пример: VNDR-0005A, VNDR-0005B\r\n */\r\n vendorCode?: string;\r\n /**\r\n * **Дата выхода**\r\n *\r\n * Пример: 01.01.2000\r\n */\r\n saleDate?: string;\r\n /**\r\n * **Вендор в магазине**\r\n *\r\n * Содержит номер вендора, а не ее название.\r\n */\r\n vendorId?: number;\r\n /**\r\n * **Категория в магазине**\r\n *\r\n * Категория, к которой вы относите товар. Она помогает точнее определить для товара категорию на Маркете.\r\n *\r\n * Указывайте конкретные категории — например, набор ножей лучше отнести к категории Столовые приборы, а не просто Посуда.\r\n *\r\n * Выбирайте категории, которые описывают товар, а не абстрактный признак — например, лучше указать Духи, а не Подарки.\r\n *\r\n * Содержит номер категории, а не ее название.\r\n */\r\n categoryId: number;\r\n /**\r\n * **Страна производства**\r\n *\r\n * Страна, где был произведен товар.\r\n *\r\n * Записывайте названия стран так, как они записаны в [списке](https://yastatic.net/s3/doc-binary/src/support/market/ru/countries.xlsx).\r\n *\r\n * Пример: Россия\r\n */\r\n countryOfOrigin?: string;\r\n /**\r\n * **Изображение**\r\n *\r\n * До двадцати изображений, которые показываются на карточке товара.\r\n *\r\n * Принимаются jpg- или png-изображения товара, соответствующие [требованиям](https://yandex.ru/support/marketplace/assortment/fields/images.html).\r\n *\r\n * В кабинете изображения добавляютс