# Ecommerce

Storefront helpers, carts, and order endpoints


Storefront-facing customer data (wishlists, product reviews, last-viewed items, and addresses) inside the Sales plugin.

The `ecommerce` module lives inside `sales_api` (port 3305). It exposes authenticated GraphQL operations for staff, `cp*` operations marked `forClientPortal` for storefront customers, and unauthenticated HTTP summary routes for storefront bootstrapping.

## Module layout

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

| Area | Path | Responsibility |
| --- | --- | --- |
| Models | `db/definitions`, `db/models` | `Wishlist`, `ProductReview`, `LastViewedItem`, `Address` |
| GraphQL | `graphql/schema`, `graphql/resolvers` | Admin and `cp*` (Client Portal) operations |
| HTTP routes | `routes.ts` (mounted from `src/routes.ts`) | `/ecommerce-*` summary and bulk endpoints |
| tRPC | `trpc/ecommerce.ts` | Router file exists but is **not merged** into the plugin `appRouter` |
| Utils | `utils.ts` | Core `products.find`, `customers.find`, `productCategories.find` fetchers |

## Data model

- **`Wishlist`**: `{ productId, customerId }` pairs.
- **`ProductReview`**: `{ productId, customerId, review: Float, description, info: JSON }`.
- **`LastViewedItem`**: `{ productId, customerId, modifiedAt }` with a `{ customerId, productId }` index.
- **`Address`**: `{ alias, customerId, coordinate: {lat,lng}, address1, address2, city, district, street, detail, more: JSON, w3w, note, phone }` plus timestamps.

`productId`/`customerId` reference Core records; the module never stores product or customer data. It hydrates them over tRPC (`core.products.find`, `core.customers.find`, `core.productCategories.find`).

## HTTP routes

Mounted on the plugin Express router ([`modules/ecommerce/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/ecommerce/routes.ts)), reached through the gateway as `/pl:sales/<path>`. All are `GET` unless noted; all take `customerId`/`productId` as query params and return `{ status, data }` or `{ status: 'error', errorMessage }`:

| Route | Params | Returns |
| --- | --- | --- |
| `GET /ecommerce-init` | `customerId` | Customer summary: reviews + stats, wishlist, last 20 viewed items, addresses, hydrated products |
| `GET /ecommerce-product-summary` | `productId` | Reviews, rating stats, wishlist/last-viewed counts, reviewer customers |
| `GET /ecommerce-last-viewed` | `customerId`, `limit` (default 10) | Last-viewed items with hydrated products |
| `GET /ecommerce-wishlist` | `customerId` | Wishlist with hydrated products |
| `GET /ecommerce-addresses` | `customerId` | Address list |
| `GET /ecommerce-product-reviews` | `productId`, `limit` (20), `skip`, `sort` (`-createdAt`) | Reviews with hydrated customers plus summary |
| `POST /ecommerce-bulk-operations` | `operations[]` | Batch write: `addToWishlist`, `addReview`, `addLastViewed`, `addAddress` |

> [!WARNING]
> **These routes are unauthenticated**
>
> `/ecommerce-*` routes check no token; any caller who knows a `customerId` can read that customer's wishlist, addresses, and review history. Front them with your own gateway auth if the storefront is public-facing.

## Queries and mutations

### Wishlist

| Operation | Kind | Notes |
| --- | --- | --- |
| `wish(productId, customerId)` | query | Single wish, hydrated `product` |
| `wishlist(customerId)` | query | List, hydrated products |
| `wishlistAdd` / `wishlistUpdate` / `wishlistRemove` | mutation | Staff-side mutations |
| `cpWishlist`, `cpWishlistAdd/Update/Remove` | query/mutation | `forClientPortal` variants |

### Product reviews

| Operation | Kind | Notes |
| --- | --- | --- |
| `productReview(productId)` | query | `AverageReview`: average + count |
| `productReviews(productIds, customerId, page, perPage)` | query | Paginated list |
| `productReviewAdd/Update/Remove` | mutation | All review mutations are `skipPermission` |
| `cpProductReviews`, `cpProductReview` | query | `forClientPortal` |
| `cpProductReviewAdd/Update/Remove` | mutation | `skipPermission` |

### Last viewed items

| Operation | Kind | Notes |
| --- | --- | --- |
| `lastViewedItems(customerId, limit)` | query | Staff-side |
| `lastViewedItemAdd` / `lastViewedItemRemove` | mutation | Staff-side |
| `cpLastViewedItems`, `cpLastViewedItemAdd/Remove` | query/mutation | `forClientPortal` |

### Addresses

| Operation | Kind | Notes |
| --- | --- | --- |
| `address(_id)` / `addressList(page, perPage, searchValue, aliasType, customerId, city, district, street)` | query | `addressList` is `forClientPortal` |
| `addressAdd` / `addressUpdate` / `addressRemove` | mutation | `addressAdd`/`addressUpdate` are `forClientPortal` |

## Permissions and context

The module registers no permission actions of its own. Standard operations run under the platform's default resolver wrapper, which only requires a logged-in user (`checkLogin`). There are no per-action ecommerce permissions.

Operations marked `forClientPortal: true` run under `wrapPublicResolver` instead ([`wrapperResolvers.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/wrapperResolvers.ts)): they require a Client Portal request context (`context.clientPortal`) and skip staff login. No ecommerce operation sets `cpUserRequired`, so a CP user session is not enforced; pass `customerId` explicitly. See [Client Portal app](https://erxes.io/docs/app-client-portal) for how that context is established.

## tRPC

[`trpc/ecommerce.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/modules/ecommerce/trpc/ecommerce.ts) defines `productReview`, `wishlist`, `lastViewedItem`, and `address` sub-routers with `findOne`/`find`/`aggregate`/`count`/`create`/`update`/`remove` helpers, but the file is **not imported by `src/trpc/init-trpc.ts`**, so none of these procedures are reachable through the plugin's `/trpc` mount. Treat the router as dead code; use the HTTP routes or GraphQL instead.

## Troubleshooting

- **`Customer ID is required` / `Product ID is required`**: the routes read query params, not a body; use `?customerId=`.
- **Hydrated `product` is `null`**: the referenced Core product was deleted or belongs to another tenant; list resolvers drop wishes whose product no longer exists.
- **tRPC call to `productReview.find` fails**: the ecommerce router isn't mounted; nothing to fix in config, use HTTP/GraphQL.

