Search for a command to run...
This page collects handlers for common tasks in the 1C exchange. The code of the examples comes from the plugin's tests, which check each of them on a real exchange. The input and result of every hook are described in the Hooks Reference.
Each example registers its own handler, and Medusa allows only one handler per hook. To apply several examples to the same hook, combine them as shown in Several Changes in One Handler.
1C keeps services in the same item catalog as goods, but they don't belong in the store, since they have neither stock nor a product page. The handler removes products whose "ВидНоменклатуры" (item kind) requisite is "Услуга" (service) from the batch. No database queries.
src/workflows/hooks/onec-catalog.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { importOnecProductsWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"3import { readRequisite } from "@gorgo-store/medusa-erp-1c/hooks"45importOnecProductsWorkflow.hooks.setProducts(async ({ products }) => {6 return new StepResponse({7 products: products.filter(({ source }) => readRequisite(source, "ВидНоменклатуры") !== "Услуга"),8 })9})
A removed service isn't created in Medusa. If it got into the store earlier, the handler doesn't delete it, because the import doesn't touch a removed product at all.
1C keeps the full name, weight and material in a product's requisites and properties, while a Medusa product has the , and fields for them. The exchange doesn't fill these fields, so the handler moves the values itself. No database queries.
src/workflows/hooks/onec-catalog.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { importOnecProductsWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"3import { readProperty, readRequisite } from "@gorgo-store/medusa-erp-1c/hooks"45importOnecProductsWorkflow.hooks.setProducts(async ({ products, properties }) => {6 return new StepResponse({7 products: products.map(({ source, existing, product }) => {8 const fullName = readRequisite(source, "Полное наименование")9 const weight = Number.parseFloat((readRequisite(source, "Вес") ?? "").replace(",", "."))10 const [material] = readProperty(source, properties, "Материал")1112 return {13 source,14 existing,15 product: {16 ...product,17 ...(fullName ? { subtitle: fullName } : {}),18 ...(Number.isFinite(weight) ? { weight } : {}),19 ...(material ? { material } : {}),20 },21 }22 }),23 })24})
A field gets into the product only when 1C sent a value. The import would treat a key holding as changed and erase a value set in the Medusa Admin. 1C may send the weight with a decimal comma, so it's replaced with a dot before parsing.
A product without a single image shows up on the storefront as an empty card. The handler keeps such a product as a draft until an image arrives. No database queries.
src/workflows/hooks/onec-catalog.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { importOnecProductsWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"34importOnecProductsWorkflow.hooks.setProducts(async ({ products }) => {5 return new StepResponse({6 products: products.map((item) => {7 const images = item.product.images ?? item.existing?.images ?? []89 return images.length ? item : { ...item, product: { ...item.product, status: "draft" } }10 }),11 })12})
The exchange publishes the product on every import, so the product gets published by itself in the exchange where 1C sends it an image. The flip side is that the next exchange turns a product without images that someone published by hand back into a draft.
Product titles and descriptions often get edited in the Medusa Admin, and the next exchange would bring back the values from 1C. The handler deletes these keys for existing products, and the import leaves the fields as they are. No database queries.
src/workflows/hooks/onec-catalog.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { importOnecProductsWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"34importOnecProductsWorkflow.hooks.setProducts(async ({ products }) => {5 return new StepResponse({6 products: products.map((item) => {7 if (!item.existing) {8 return item9 }1011 const { title: _title, description: _description, ...product } = item.product1213 return { ...item, product }14 }),15 })16})
A new product gets its title and description from 1C, because it can't be created without a title. After that, title and description changes made in 1C no longer reach the store.
1C keeps a product's brand in the "Бренд" (brand) property, while in Medusa it's handy to keep it as a collection. The handler looks up the collections named after the batch's brands and assigns the matching one to each product. One database query per batch.
src/workflows/hooks/onec-catalog.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { ContainerRegistrationKeys } from "@medusajs/framework/utils"3import { importOnecProductsWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"4import { readProperty } from "@gorgo-store/medusa-erp-1c/hooks"56importOnecProductsWorkflow.hooks.setProducts(async ({ products, properties }, { container }) => {7 const brandByOnecId = new Map(products.map(({ source }) => [source.id, readProperty(source, properties, "Бренд")[0]]))8 const titles = [...new Set([...brandByOnecId.values()].filter((title): title is string => Boolean(title)))]910 if (!titles.length) {11 return new StepResponse({ products })12 }1314 const query = container.resolve(ContainerRegistrationKeys.QUERY)15 const { data: collections } = await query.graph({16 entity: "product_collection",17 fields: ["id", "title"],18 filters: { title: titles },19 })20 const collectionIdByTitle = new Map(collections.map((collection) => [collection.title, collection.id]))2122 return new StepResponse({23 products: products.map((item) => {24 const collectionId = collectionIdByTitle.get(brandByOnecId.get(item.source.id) ?? "")2526 return collectionId ? { ...item, product: { ...item.product, collection_id: collectionId } } : item27 }),28 })29})
The handler only looks collections up, and you create them in the Medusa Admin. A product whose brand has no collection stays unchanged. A handler can create collections too, but then it needs a rollback function. The handler rules describe it.
The "Оптовая цена" (wholesale price) price type should apply only to customers in the "Опт" (wholesale) group. The handler activates its price list and gives it the group rule, so a guest still sees the retail price. The price list type is , so that a group customer pays exactly the 1C price rather than the lower of the wholesale and retail ones. One database query per batch, and only when the batch has this price type.
src/workflows/hooks/onec-prices.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { ContainerRegistrationKeys, PriceListStatus, PriceListType } from "@medusajs/framework/utils"3import { importOnecPricesWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"45const WHOLESALE_PRICE_TYPE_NAME = "Оптовая цена"6const WHOLESALE_GROUP_NAME = "Опт"78importOnecPricesWorkflow.hooks.setPriceLists(async ({ priceLists }, { container }) => {9 if (!priceLists.some(({ source }) => source.name === WHOLESALE_PRICE_TYPE_NAME)) {10 return new StepResponse({ priceLists })11 }1213 const query = container.resolve(ContainerRegistrationKeys.QUERY)14 const { data: groups } = await query.graph({15 entity: "customer_group",16 fields: ["id"],17 filters: { name: WHOLESALE_GROUP_NAME },18 })19 const groupId = groups[0]?.id2021 if (!groupId) {22 return new StepResponse({ priceLists })23 }2425 return new StepResponse({26 priceLists: priceLists.map((item) =>27 item.source.name === WHOLESALE_PRICE_TYPE_NAME28 ? {29 ...item,30 priceList: {31 ...item.priceList,32 status: PriceListStatus.ACTIVE,33 type: PriceListType.OVERRIDE,34 rules: { "customer.groups.id": [groupId] },35 },36 }37 : item,38 ),39 })40})
Create the "Опт" group in the Medusa Admin beforehand. Without it, the price list stays a draft, as it would without a handler. The handler sets this price list's status, type and rules on every exchange, so the next exchange reverts any change you make to them in the Medusa Admin.
Orders the team places to check the storefront shouldn't reach 1C. The handler removes orders from addresses from the export. No database queries.
src/workflows/hooks/onec-orders.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { exportOnecOrdersWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"34exportOnecOrdersWorkflow.hooks.setOrders(async ({ orders }) => {5 return new StepResponse({6 orders: orders.filter(({ source }) => !source.email?.endsWith("@test.invalid")),7 })8})
A removed order doesn't get the exported mark. It reaches the hook on every export, and the handler removes it again each time.
A manager in 1C wants to see where an order came from and what the customer wrote. A Medusa order has no separate field for a comment, so the storefront puts it in . The handler moves the comment into the document and adds the "Источник" (source) requisite. No database queries.
src/workflows/hooks/onec-orders.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { exportOnecOrdersWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"34exportOnecOrdersWorkflow.hooks.setOrders(async ({ orders }) => {5 return new StepResponse({6 orders: orders.map(({ source, document }) => {7 const comment = source.metadata?.comment89 return {10 source,11 document: {12 ...document,13 ...(typeof comment === "string" && comment ? { comment } : {}),14 requisites: [...document.requisites, { name: "Источник", value: "Интернет-магазин" }],15 },16 }17 }),18 })19})
Saving the comment to the order's is up to the storefront. The "Источник" requisite gets into the document, and what 1C does with it depends on your configuration.
A customer from the "Юрлица" (companies) group should go to 1C as a counterparty with an INN and KPP (Russian tax IDs), and the manager needs the shipping method. The INN and KPP are stored in the customer's and the shipping method in the order, and the handler gets all of it with one query for the whole export.
src/workflows/hooks/onec-orders.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { ContainerRegistrationKeys } from "@medusajs/framework/utils"3import { exportOnecOrdersWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"45const COMPANY_GROUP_NAME = "Юрлица"67exportOnecOrdersWorkflow.hooks.setOrders(async ({ orders }, { container }) => {8 if (!orders.length) {9 return new StepResponse({ orders })10 }1112 const query = container.resolve(ContainerRegistrationKeys.QUERY)13 const { data } = await query.graph({14 entity: "order",15 fields: ["id", "customer.metadata", "customer.groups.name", "shipping_methods.name"],16 filters: { id: orders.map(({ source }) => source.id) },17 })18 const extraById = new Map(data.map((order) => [order.id, order]))1920 return new StepResponse({21 orders: orders.map(({ source, document }) => {22 const extra = extraById.get(source.id)23 const isCompany = extra?.customer?.groups?.some((group: { name?: string } | null) => group?.name === COMPANY_GROUP_NAME)24 const { inn, kpp } = (extra?.customer?.metadata ?? {}) as { inn?: string; kpp?: string }25 const delivery = extra?.shipping_methods?.[0]?.name2627 return {28 source,29 document: {30 ...document,31 counterparties: document.counterparties.map((counterparty) =>32 isCompany && inn ? { ...counterparty, inn, ...(kpp ? { kpp } : {}) } : counterparty,33 ),34 requisites: delivery35 ? [...document.requisites, { name: "Способ доставки", value: delivery }]36 : document.requisites,37 },38 }39 }),40 })41})
The query goes through the link between the order and the customer, so in the database it's three SQL queries, but for the whole export rather than per order. The plugin writes the INN and KPP after the counterparty's name and before the address. This position hasn't been checked against a real 1C configuration yet, so make sure yours reads them there.
The plugin writes to the root of the file, while an older 1C configuration may expect its own version, for example 2.05. The product and document models have no schema version, so the handler edits the finished XML through , in both the catalog export and the order export. No database queries.
src/workflows/hooks/onec-payload.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { exportOnecCatalogWorkflow, exportOnecOrdersWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"34const OLD_SCHEMA_VERSION = "2.05"56function withSchemaVersion(payload: string): string {7 return payload.replace(/ВерсияСхемы="[^"]*"/, `ВерсияСхемы="${OLD_SCHEMA_VERSION}"`)8}910exportOnecCatalogWorkflow.hooks.setPayload(async ({ payload }) => {11 return new StepResponse({ payload: withSchemaVersion(payload) })12})1314exportOnecOrdersWorkflow.hooks.setPayload(async ({ payload }) => {15 return new StepResponse({ payload: withSchemaVersion(payload) })16})
The handler changes only the version number, and the file layout stays the one the plugin writes. If your configuration also differs in layout, edit the XML in the same handler. A string edit knows nothing about the XML structure, so anything you can express through is more reliable there.
Medusa allows one handler per hook, so your project's changes for one hook go into a single handler. It's convenient to keep each change in its own function and call them in turn. Put filtering first, so the other changes don't spend time on removed elements.
src/workflows/hooks/onec-catalog.ts1import { StepResponse } from "@medusajs/framework/workflows-sdk"2import { importOnecProductsWorkflow } from "@gorgo-store/medusa-erp-1c/workflows"3import { readRequisite, type ProductToImport } from "@gorgo-store/medusa-erp-1c/hooks"45function skipServices(products: ProductToImport[]): ProductToImport[] {6 return products.filter(({ source }) => readRequisite(source, "ВидНоменклатуры") !== "Услуга")7}89function keepManagerEdits(products: ProductToImport[]): ProductToImport[] {10 return products.map((item) => {11 if (!item.existing) {12 return item13 }1415 const { title: _title, description: _description, ...product } = item.product1617 return { ...item, product }18 })19}2021importOnecProductsWorkflow.hooks.setProducts(async ({ products }) => {22 return new StepResponse({ products: keepManagerEdits(skipServices(products)) })23})
Make the functions that need the database async and call them with . Each of them then still makes one query per batch.