# Deals & Pipelines

Boards, pipelines, stages, deals, labels, and checklists


Deal boards, pipelines, stages, deals, labels, and checklists: the core of the Sales plugin.

## Module layout

Backend code lives under `backend/plugins/sales_api/src/modules/sales/`:

| Area | Path | Responsibility |
| --- | --- | --- |
| Models | `db/definitions`, `db/models` | Mongoose schemas for boards, pipelines, stages, deals, labels, checklists |
| GraphQL | `graphql/schemas`, `graphql/resolvers` | Queries, mutations, custom resolvers, loaders |
| tRPC | `trpc/deal.ts`, `trpc/document.ts` | Cross-service procedures |
| Documents | `documents/dealContent.ts`, `documents/replaceBlocks.ts` | Deal document-template content replacement |
| Meta | `meta/automations`, `meta/segments`, `meta/references`, `meta/activity-log`, `meta/payments` | Automation constants/handlers, segment definitions, record references |

The frontend counterpart is `frontend/plugins/sales_ui/src/modules/deals/` (boards, pipelines, deal detail, product/payment UI).

## Data model

The hierarchy is `Board → Pipeline → Stage → Deal`, each collection scoped per tenant:

- **Board** (`Boards`): `name`, `order`, `type` (default `deal`).
- **Pipeline** (`Pipelines`): `name`, `boardId`, `visibility`, `memberIds`, `watchedUserIds`, `departmentIds`, `branchIds`, `paymentTypes` (may carry `scoreCampaignId`), `propertyIds`, plus card-numbering and date-check settings.
- **Stage** (`Stages`): `name`, `pipelineId`, `probability`, `status`, `visibility`, `memberIds`/`canMoveMemberIds`/`canEditMemberIds`, `formId`, `order`.
- **Deal** (`Deals`): `name`, `stageId`, `order`, `assignedUserIds`, `watchedUserIds`, `labelIds`, `tagIds`, `branchIds`/`departmentIds`, `priority`, `status` (`active`/`archived`, `SALES_STATUSES`), `startDate`/`closeDate`/`stageChangedDate`, `productsData` (per-product quantity, prices, `tickUsed`, `discountInfos`), `paymentsData`, `totalAmount`/`unUsedTotalAmount`/`bothTotalAmount`, `mobileAmount`/`mobileAmounts`, `propertiesData`, `customFieldsData`, `number`, `relations` (Gantt links), `timeTrack`.
- **Labels** and **checklists**: pipeline-scoped `SalesPipelineLabel` records and deal-attached `SalesChecklist`/`SalesChecklistItem` records.

A deal stores no `customerId`/`companyId`. Customer and company links are Core relation records read by the platform, not fields on the deal.

### Pipeline property selection

`Pipelines.propertyIds` stores ids of Core `sales:deal` fields. `salesPipelinesAdd`/`salesPipelinesEdit` validate every id through Core's `fields.find` tRPC procedure before writing ([`modules/sales/utils/pipelineProperties.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/sales/utils/pipelineProperties.ts)). Deal detail renders only the selected properties; a separate `isPropertySelectionConfigured` flag preserves legacy show-all behavior for pipelines saved before the feature existed.

## Queries and mutations

Schema: [`modules/sales/graphql/schemas/deal.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/sales/graphql/schemas/deal.ts).

| Operation | Kind | Purpose |
| --- | --- | --- |
| `deals(stageId, filter…)` / `dealDetail(_id)` | query | Cursor-paginated deal list and detail |
| `dealsTotalCount` / `dealsTotalAmounts` / `archivedDeals` | query | Counts, per-currency totals, archive browsing |
| `dealsAdd` / `dealsEdit` / `dealsRemove` / `dealsCopy` | mutation | Deal CRUD |
| `dealsChange(itemId, destinationStageId, aboveItemId)` | mutation | Move a card between stages or reorder |
| `dealsWatch(_id, isAdd)` / `dealsArchive(stageId)` | mutation | Watch a deal; archive a whole stage |
| `dealsCreateProductsData` / `dealsEditProductData` / `dealsDeleteProductData` | mutation | Product rows on a deal |
| `checkDiscount(_id, products, couponCode, voucherId)` | query | Coupon/voucher price check for deal products; provided by the Enterprise Edition |
| `salesBoards` / `salesBoardDetail` / `salesBoardsAdd` / `salesBoardsEdit` / `salesBoardsRemove` | query/mutation | Boards |
| `salesPipelines` / `salesPipelineDetail` / `salesPipelinesAdd` / `salesPipelinesEdit` / `salesPipelinesUpdateOrder` / `salesPipelinesWatch` / `salesPipelinesRemove` / `salesPipelinesArchive` / `salesPipelinesCopied` | query/mutation | Pipelines, including `propertyIds` |
| `salesStages` / `salesStageDetail` / `salesStagesEdit` / `salesStagesUpdateOrder` / `salesStagesSortItems` / `salesArchivedStages` | query/mutation | Stages and card ordering |
| `salesPipelineLabels` / `salesPipelineLabelsAdd` / `salesPipelineLabelsLabel` | query/mutation | Labels and applying them to deals |
| `salesChecklists` / `salesChecklistsAdd` / `salesChecklistItemsAdd` / `salesChecklistItemsOrder` | query/mutation | Checklists and items |

Most list/read operations also have `cp*` variants (`cpDeals`, `cpDealDetail`, `cpSalesStages`, …) marked `forClientPortal` for storefront use; see [Ecommerce](https://erxes.io/docs/sales-ecommerce) and [Client Portal](https://erxes.io/docs/app-client-portal).

Subscriptions: `salesDealChanged(_id)` and `salesDealListChanged(pipelineId, userId, filter)` push live board updates.

Example: add a deal (signature from `graphql/schemas/deal.ts`):

```graphql
mutation {
  dealsAdd(
    name: "Q3 hardware renewal"
    customerIds: ["cust_1"]
    stageId: "stage_open"
    assignedUserIds: ["user_1"]
    closeDate: "2026-12-31"
    productsData: [{ productId: "prod_7", quantity: 4, unitPrice: 250, tickUsed: true }]
  ) {
    _id
    name
  }
}
```

## tRPC

[`src/trpc/init-trpc.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/trpc/init-trpc.ts) merges the `deal`, `pos` (with `orders`), `documents`, and `fields` routers. Deal-side procedures include `deal.findOne`, `deal.find`, `deal.count`, `deal.aggregate`, `deal.tag`, `deal.createItem`/`editItem`/`removeItem`, `deal.generateAmounts`, `deal.generateProducts`, `deal.replaceContent`, `deal.contentIds`, `deal.subscriptionWrapper`, `deal.create`/`updateOne` (system-user), `stage.findOne`/`stage.find`, `pipeline.findOne`, and `documents.editorAttributes` / `fields.getFieldList`.

Always pass a `limit` to `deal.find`. It forwards `skip`/`limit`/`sort` to MongoDB, and an unbounded `deal.find` over a large tenant can exhaust the service.

## Automations, segments, documents

- **Automations** (`meta/automations.ts` + `modules/sales/meta/automations/`): triggers are "Sales pipeline" (segment enrollment), "Deal reaches stage probability", and "Deal stage changed"; actions are "Create deal" and "Create sales checklist". `setPropertyTargets` let workflows write customer/company deal relations.
- **Segments**: the `sales:sales.deals` content type with filterable fields, member listing/counting, and membership writes; `customer.deals` and `company.deals` relations join through Core relation records. Stage-derived fields (`pipelineId`, `boardId`, `stageProbability`) rewrite their conditions to `stageId`s before evaluating.
- **Documents**: `sales:deal` is a document content type. `documents.editorAttributes` returns merge fields (fixed attributes, schema fields, and Core custom fields as `customFieldsData.<fieldId>`); `deal.replaceContent` renders one processed document per selected deal in the caller's order.
- **References** (`meta/references.ts`): deal display names, links, labels, product amount helpers, and `excludeLoyaltyAmount` (deal total minus payments through types carrying `scoreCampaignId`; used by the Enterprise Edition).

## Permissions

`meta/permissions.ts` registers modules `deal` (`showDeals`, `dealsAdd`, `dealsEdit`, `dealsProductsEdit`, `dealsRemove`, `dealsWatch`, `dealsArchive`), `board` (`boardsAdd/Edit/Remove`, `updateTimeTracking`), `pipeline` (`pipelinesAdd/Edit/Watch`), `stage` (`stagesEdit`, `stagesRemove`, `itemsSort`), `checklist`, `pipelineLabel`, and `pipelineTemplate`, with `own`/`group`/`all` scopes on deals. Default groups: `sales:admin`, `sales:user`, `sales:viewer`.

## Troubleshooting

- **Pipeline edit rejects `propertyIds`**: every id must be a Core `sales:deal` field; validation calls Core `fields.find` and fails closed.
- **Deal list is slow or unsorted**: the unscoped list relies on the `parentId/order/_id/status` compound index; filters outside it can force a collection scan.
- **A segment never updates for deal writes**: the content type must be declared as `sales:sales.deals`; the event dispatcher matches on that string.

