Ecommerce

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/:

AreaPathResponsibility
Modelsdb/definitions, db/modelsWishlist, ProductReview, LastViewedItem, Address
GraphQLgraphql/schema, graphql/resolversAdmin and cp* (Client Portal) operations
HTTP routesroutes.ts (mounted from src/routes.ts)/ecommerce-* summary and bulk endpoints
tRPCtrpc/ecommerce.tsRouter file exists but is not merged into the plugin appRouter
Utilsutils.tsCore 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), 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 }:

RouteParamsReturns
GET /ecommerce-initcustomerIdCustomer summary: reviews + stats, wishlist, last 20 viewed items, addresses, hydrated products
GET /ecommerce-product-summaryproductIdReviews, rating stats, wishlist/last-viewed counts, reviewer customers
GET /ecommerce-last-viewedcustomerId, limit (default 10)Last-viewed items with hydrated products
GET /ecommerce-wishlistcustomerIdWishlist with hydrated products
GET /ecommerce-addressescustomerIdAddress list
GET /ecommerce-product-reviewsproductId, limit (20), skip, sort (-createdAt)Reviews with hydrated customers plus summary
POST /ecommerce-bulk-operationsoperations[]Batch write: addToWishlist, addReview, addLastViewed, addAddress

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

OperationKindNotes
wish(productId, customerId)querySingle wish, hydrated product
wishlist(customerId)queryList, hydrated products
wishlistAdd / wishlistUpdate / wishlistRemovemutationStaff-side mutations
cpWishlist, cpWishlistAdd/Update/Removequery/mutationforClientPortal variants

Product reviews

OperationKindNotes
productReview(productId)queryAverageReview: average + count
productReviews(productIds, customerId, page, perPage)queryPaginated list
productReviewAdd/Update/RemovemutationAll review mutations are skipPermission
cpProductReviews, cpProductReviewqueryforClientPortal
cpProductReviewAdd/Update/RemovemutationskipPermission

Last viewed items

OperationKindNotes
lastViewedItems(customerId, limit)queryStaff-side
lastViewedItemAdd / lastViewedItemRemovemutationStaff-side
cpLastViewedItems, cpLastViewedItemAdd/Removequery/mutationforClientPortal

Addresses

OperationKindNotes
address(_id) / addressList(page, perPage, searchValue, aliasType, customerId, city, district, street)queryaddressList is forClientPortal
addressAdd / addressUpdate / addressRemovemutationaddressAdd/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): 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 for how that context is established.

tRPC

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.
Was this helpful?