# Products

Catalog, categories, units, and pricing helpers


Maintain the shared catalog of products and services, categories, units of measure, bundles, and pricing rules.

For POS and deal flows that use the catalog, see [Sales](https://erxes.io/docs/plugin-sales) and [POS Client](https://erxes.io/docs/app-pos-client).

## Module layout

```text
backend/core-api/src/modules/products/
  graphql/schemas/{product,category,uom,package,rule,similarity,config,index}.ts
  graphql/resolvers/{queries,mutations,customResolvers}/
  db/definitions/{products,categories,uoms,packages,rules,similarities,configs}.ts
  db/models/{Products,Categories,Uoms,Packages,Rules,Similarities,Configs}.ts
  trpc/{product,category,uom,similarity,config,index}.ts
  meta/automations.ts            # AI knowledge provider for automations
  meta/document/productReplacer.ts
  meta/import-export/{import,export}/
  meta/template/                 # category templates
  utils/productRule.ts
```

Frontend: `frontend/core-ui/src/modules/products/` (record table, categories, UoM, packages) with shared product pickers in `frontend/libs/ui-modules/src/modules/products/`.

## Data model

`Product` ([products.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/products/db/definitions/products.ts)):

| Field | Notes |
| ----- | ----- |
| `name`, `shortName`, `code` | `code` is unique; the stable external key |
| `categoryId` | Links to `ProductCategory` (tree via `parentId`, `order`) |
| `type` | `product`, `service`, `unique`, `subscription` |
| `status` | `active` or `deleted` (soft delete) |
| `unitPrice`, `currency` | Base price; `discounts[]` and `inventories` hold extras |
| `barcodes`, `variants`, `barcodeDescription`, `sameMasks`, `sameDefault` | Barcode/variant matching for POS |
| `uom`, `subUoms` | Unit of measure and convertible sub-units |
| `vendorId`, `scopeBrandIds`, `tagIds`, `mergedIds` | Ownership and dedup |
| `customFieldsData`, `propertiesData` | Custom properties |
| `attachment`, `attachmentMore`, `videos`, `pdfAttachment` | Media |
| `duration`, `durationType` | For service-type items |
| `similarityId` | Links to `Similarity` groups |

`Uom` (`db/definitions/uoms.ts`): `name`, `code` (unique), `isForSubscription`, `subscriptionConfig` (`period`, `rule`, `specificDay`, `subsRenewable`). `ProductRule` prices products by conditions; `ProductPackage` bundles items; `Similarity` groups mask-matched products; `ProductsConfig` stores module-wide settings (`productsConfigsUpdate`).

## GraphQL API

### Products ([graphql/schemas/product.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/products/graphql/schemas/product.ts))

| Operation | Key arguments |
| --------- | ------------- |
| `productsMain` | `type`, `status`, `categoryId`, `categoryIds`, `searchValue`, `vendorId`, `brandIds`, `tag`, `tagIds`, `excludeTagIds`, `tagWithRelated`, `ids`, `excludeIds`, `pipelineId`, `boardId`, `segment`, `segmentIds`, `propertiesData`, `groupedSimilarity`, `similarity`, `image`, `brand`, `branchId`, `departmentId`, `minRemainder`/`maxRemainder`, `minPrice`/`maxPrice`, discount range args, `discountConditions`, `sortField`, `sortDirection`, cursor params; returns `ProductsListResponse` |
| `products` | same filters with `page`/`perPage`; returns a plain `[Product]` |
| `productsTotalCount` | same filters |
| `productDetail` | `_id` |
| `productSimilarities` | `_id!`, `groupedSimilarity` |
| `productCountByTags` | none |
| `productsAdd` / `productsEdit` | `name`, `shortName`, `categoryId`, `type`, `description`, `barcodes`, `variants`, `barcodeDescription`, `unitPrice`, `code`, `propertiesData`, `attachment`, `attachmentMore`, `videos`, `vendorId`, `scopeBrandIds`, `uom`, `subUoms`, `currency`, `duration`, `durationType`, `pdfAttachment` |
| `productsMerge` | `productIds`, `productFields` |
| `productsDuplicate` / `productsRemove` | `_id!` / `productIds: [String!]` |

### Categories, units, packages, rules

| Operation | Key arguments |
| --------- | ------------- |
| `productCategories` / `productCategoriesTotalCount` / `productCategoryDetail` | `parentId`, `searchValue`, `status`, `ids` / `_id` |
| `categoriesWithChilds` | `ids: [String!]!` |
| `productCategoriesAdd` / `productCategoriesEdit` / `productCategoriesRemove` | `name`, `code`, `parentId`, `description`, `meta`, `attachment`, `status` / `_id!` |
| `uoms` / `uomsTotalCount` | none |
| `uomsAdd` / `uomsEdit` / `uomsRemove` | `name`, `code`, `isForSubscription`, `subscriptionConfig`, `timely` / `uomIds: [String!]` |
| `productPackages` / `productPackageDetail` | cursor params / `_id!` |
| `productPackagesAdd` / `productPackagesEdit` / `productPackagesChangeStatus` / `productPackagesRemove` | package fields / `_ids`, `status!` |
| `productRules` / `productRulesWithCount` | none |
| `productRulesAdd` / `productRulesEdit` / `productRulesRemove` | rule fields / `_ids` |
| `productBulkSimilarity` / `productBulkSimilarities` | `_id!` / `page`, `perPage`, `searchValue` |
| `productsConfigs` / `productsConfigsUpdate` | none / `configsMap: JSON!` |

## Example

```graphql
query ProductList($categoryId: String) {
  productsMain(categoryId: $categoryId, status: "active", limit: 20) {
    list {
      _id
      name
      code
      unitPrice
      uom
      cursor
    }
    totalCount
    pageInfo { hasNextPage endCursor }
  }
}

mutation AddProduct {
  productsAdd(
    name: "Espresso blend 1kg"
    code: "COF-ESP-1KG"
    type: "product"
    unitPrice: 18.5
    currency: "USD"
    uom: "bag"
    barcodes: ["4006381333931"]
  ) { _id code }
}
```

## How plugins use products

Services do not read the `Products` collection directly. They call the tRPC router in [trpc/product.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/products/trpc/product.ts) through `sendTRPCMessage({ pluginName: 'core', module: 'products', action: ... })`. Procedures: `find` (with automatic `categoryId` expansion to child categories), `findOne`, `createProduct`, `updateProduct`, `updateProducts`, `removeProducts`, `count`. Example from the POS order route in `sales_api`:

```ts
// backend/plugins/sales_api/src/modules/pos/routes.ts
const products = await sendTRPCMessage({
  subdomain,
  pluginName: 'core',
  module: 'products',
  action: 'find',
  input: { query: { status: { $ne: 'deleted' }, categoryId: { $in: categoryIds } } },
  defaultValue: [],
});
```

The same pattern appears in the sales pipeline product picker and ecommerce module. Products also show up through `meta/document/productReplacer.ts` (document templates), `meta/automations.ts` (AI knowledge provider), and the segment content type `core:products.products`.

## Permissions

Core permission module `products` (`meta/permissions.ts`): `productsRead` (`always: true`), `productsCreate`, `productsUpdate`, `productsDelete`, `productsMerge`, `productCategoriesManage`, `productsConfigsManage`, `uomsManage`, `productRulesManage`, `productsImportManage`, `productsExportManage`. Scope is `all` only; there is no per-owner product scope.

