Happy House - Ecommerce Docs
Developer ResourcesCart

Cart API Reference

Complete API contracts for the Cart module, including routes, auth, DTOs, responses, errors, examples, and integration notes.

Cart - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: The eight customer cart routes and the two read-only admin routes.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/cart/customer/cart-customer.controller.ts, admin/cart-admin.controller.tsRoutes, methods, guards, status codes, interceptor wiring
DTOscustomer/dto/*.ts, admin/dto/*.tsValidation, defaults, query enums
Servicescart-write.service.ts, cart-bulk.service.ts, cart-query.service.tsBehavior, errors, version semantics
Schemapackages/db/src/schema/cart/*.tsPartial unique, caps, status enum
Error registryapps/api/src/common/types/error-codes.ts (// CART)CART_* codes

2. Module Summary

FieldValue
Module namecart
Module slugcart
Primary actorscustomer, admin (read-only)
API surfacesmobile (customer), admin
Base route prefixes/api/mobile/cart, /api/admin/carts
Auth modelJwtAuthGuard (customer); JwtAuthGuard + RoleGuard (Cart_READ, admin)
PersistencePostgreSQL (cart, cart_item), Redis (30s summary cache)
Runtime source of truthcart/cart_item rows + live product/inventory rows
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
versionOptimistic concurrency token; optional on single mutations, required on bulk; every response carries itschemaAll writes
statusactive (only one the module writes), checkout_locked, convertedschemaMutations, admin list
totalQuantitySum over every line, valid or not — the badge numberbuilderSummary, cart
subtotalValid lines only (customer); every line (admin)builderPricing
lastKnownUnitPrice / last_known_unit_priceNot a snapshot — "has this changed since you chose it" signalschemapriceChange
Idempotency-KeyOptional on POST /items onlyinterceptorAdd
checkout_lockedAbsorbing state until checkout owns the return edgeschemaMutations

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
MobileGET/api/mobile/cartCustomerJWT + IpThrottleCartCustomerControllerMy cart
MobileGET/api/mobile/cart/summaryCustomerJWT + IpThrottlesameBadge counters
MobileGET/api/mobile/cart/checkout-validationCustomerJWT + IpThrottlesameStrict validation pass
MobilePOST/api/mobile/cart/itemsCustomerJWT + IpThrottlesameAdd (delta)
MobilePOST/api/mobile/cart/items/bulkCustomerJWT + IpThrottlesameBatch edit
MobilePUT/api/mobile/cart/items/:variantPublicIdCustomerJWT + IpThrottlesameSet quantity (absolute)
MobileDELETE/api/mobile/cart/items/:variantPublicIdCustomerJWT + IpThrottlesameRemove line
MobileDELETE/api/mobile/cart/itemsCustomerJWT + IpThrottlesameEmpty cart
AdminGET/api/admin/cartsAdminJWT + RoleCart_READCartAdminControllerRead-only list
AdminGET/api/admin/carts/:cartIdAdminJWT + RoleCart_READsameRead-only detail

{variantPublicId}, {productId} and {cartId} are public uuids (v7), never integers. Literal segments (summary, checkout-validation, items, bulk) are declared before any :param-shaped route on the same verb.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
CustomerJwtAuthGuard, IpThrottlerGuardreq.user.idNoEvery query scoped to the account; guest carts are structurally impossible (customer_id NOT NULL)
AdminJwtAuthGuard, RoleGuard, IpThrottlerGuardreq.userCart_READNoRead-only

Rate limits: customer reads CUSTOMER_READ 60/min (account-keyed); customer mutations CUSTOMER_CART_MUTATION 60/min (account-keyed, three times the ordinary write limit — a stepper on several lines produces a legitimate burst); admin 30/min IP. Idempotency-Key: optional, POST /items only (scope cart-item-add).

6. DTO and Model Reference

6.1 AddCartItemDto

FieldTypeRequiredDefaultValidationNotes
productIdstringYesN/AUUID v7
variantPublicIdstringNoN/AUUID v7The configuration to add. Omitted means the product's default variant. A variant belonging to another product is refused with CART_PRODUCT_NOT_FOUND.
quantitynumberYesN/Aint 1..99Delta — adds to the existing line
versionnumberNo>= 1Optional

6.2 SetCartItemQuantityDto

FieldTypeRequiredValidationNotes
quantitynumberYesint 0..99Absolute; 0 removes
versionnumberNo>= 1Optional

6.3 BulkCartMutationDto

FieldTypeRequiredValidationNotes
versionnumberYes>= 1Required here — only a client that loaded the cart sends a batch
operationsarrayYesmax 50; no duplicate LINE — see belowApplied in order, one transaction, one version bump

Operation shapes: { op: "set", productId, variantPublicId?, quantity: 0..99 } (absolute, 0 removes), { op: "add", productId, variantPublicId?, quantity: 1..99 } (delta), { op: "remove", productId, variantPublicId? }.

variantPublicId is optional on every operation including remove, and omitting it always means the product's default variant.

Two variants of one product in one batch is legal and is the point. What is refused is naming the same LINE twice, and that check runs twice: once on what the client wrote, where {productId: P} and {productId: P, variantPublicId: P's default} are two different keys, and again after resolution, where they are one variant_id. Sending both is CART_BULK_DUPLICATE_PRODUCT. Pick one convention per client — always send the variant, or never send it for single-variant products — and stay with it.

6.4 Query DTOs

CartVersionQueryDto { version? } — used on the two DELETEs, where the concurrency token travels as a query parameter (a body on DELETE is not universally survivable). Admin list: status, customerId, inactiveForDays, createdFrom, createdTo (date-only createdTo is inclusive of the whole day), sort (lastActivityAt default, createdAt, updatedAt, itemCount), order, page, sizeno pagination parameter; sending one is a 400.

6.5 Response DTOs

CartResponseDto (whole cart, returned by every mutation):

{
  "cart": { "id": "019fc6…", "status": "active", "version": 7 },
  "customer": { "id": "019fc6…" },
  "summary": { "totalItems": 3, "totalQuantity": 7, "validItems": 2, "readyForCheckout": false },
  "items": [ /* see below */ ],
  "pricing": { "subtotal": 249900, "savings": 30000, "currency": "NPR", "excludedItemCount": 1 },
  "validation": { "blockingReasons": ["items_unavailable"] },
  "timestamps": { "createdAt": "…", "updatedAt": "…", "lastActivityAt": "…" }
}

A customer with no cart gets "cart": null, "items": [], zeroed counters, "timestamps": nullGET never creates a cart.

A cart line:

{
  "id": "019fc6…", "quantity": 2, "addedAt": "…", "updatedAt": "…",
  "product": { "basic": {}, "pricing": {}, "classification": { "brand": {} },
               "media": { "thumbnail": {} }, "status": {}, "inventory": {} },
  "pricing": { "unitPrice": 124950, "lineSubtotal": 249900,
               "unitMrp": 139950, "lineSavings": 30000, "currency": "NPR",
               "deal": { "basePrice": 149950, "effectivePrice": 124950,
                         "saving": 25000, "discountBps": 1667,
                         "effectiveMaxPrice": null } },
  "priceChange": { "changed": true, "previousPrice": 119950, "currentPrice": 124950 },
  "validation": { "valid": false, "reason": "insufficient_stock",
                  "requestedQuantity": 2, "availableQuantity": 1 }
}

What pricing.unitPrice is, and what it is not

unitPrice is the LINE'S OWN VARIANT price, with any live special-deal campaign already applied. It is not product.pricing.sellingPrice, which is the product ROLLUP — the minimum across the product's sellable variants.

Never total, charge, or display a line price from product.pricing.sellingPrice.
Always use pricing.unitPrice / pricing.lineSubtotal.

The failure this prevents is silent and complete: a customer who chose the 512GB variant would be billed the 128GB price, and the checkout freeze, the order line and the invoice would all agree with the undercharge, because every one of them takes its figure from this field.

pricing.deal is the campaign applied to this line, or null. unitPrice already carries it — deal exists so the customer can be shown what came off and why. Its effectiveMaxPrice is always null on a cart line: a line is one variant, so there is no band.

lineSavings is measured from the MRP, so a variant markdown and a campaign discount arrive as one "you saved" figure rather than two a client might add together.

The card embedded at product has its pricing.effectivePrice and pricing.deal overwritten with this line's figures, so either is safe to render here. Its sellingPrice, mrp, maxSellingPrice and isPriceRange remain the product rollup — they are the "from" price and the struck-through was-price, and they are never charged.

product is the storefront card contract — the same shape GET /products returns, not the detail shape. A cart line renders a thumbnail, a name, a price and a stock state, so it carries basic (id/name/slug), pricing, classification.brand, media.thumbnail, status and inventory, and nothing else. There is no gallery, no attributes, no seo, no tags, no timestamps and no sku; read GET /products/{slug} for those.

sku is absent on purpose and it is not missing from the purchase record: the order line still snapshots it, read from the product row under the cart lock in CheckoutAccessService.loadLineIdentity. What a purchase says was bought must not be sourced from a display response.

item.id is for list keying only — no route accepts it; lines are addressed by item.variant.id, which is product_variant.public_id.

A cart line is (cart, variant), enforced by uq_cart_item_cart_id_variant_id. Two configurations of one product are two independent lines, each charged from its own variant's price with its own campaign applied. Every item therefore carries a variant block:

FieldTypeNotes
variant.idstringproduct_variant.public_id. The line's address — the path segment of the set-quantity and remove routes.
variant.namestring | nullnull when the variant IS the product; render the product name alone.
variant.skustring | nullThe variant's own SKU.

pricing.unitPrice is what is charged. product.pricing.sellingPrice is the product ROLLUP — the minimum across sellable variants — correct for a listing card's "from Rs X" and an undercharge on a line. The embedded card's effectivePrice and deal are overwritten with this line's figures; every other field under product.pricing stays the rollup.

CartCountsDto: { totalItems, totalQuantity } (no version, deliberately). CheckoutValidationDto: readyForCheckout + cart + product/inventory/pricing groups + blockingReasons. Admin row: { id, status, version, customer: { id, name }, itemCount, totalQuantity, subtotal, currency, createdAt, updatedAt, lastActivityAt }; detail adds lines with a three-field product reference.

7. Enum Reference

EnumValueMeaningRuntime EffectSource
cart_statusactiveThe only value the module writesMutations require itenums.ts
cart_statuscheckout_lockedCheckout in flightRefuses all mutations (CART_LOCKED_FOR_CHECKOUT)
cart_statusconvertedOrder createdExcluded from the live-cart partial unique
validation.reason (derived)null / removed / archived / draft / out_of_stock / insufficient_stockWhy a line is invalidTop-down precedence: removed firstbuilder
blockingReasonsempty / cart_locked / items_unavailable / insufficient_stockCheckout blockersSubset of the fourquery

8. Endpoint Reference

8.1 GET /api/mobile/cart

Purpose

The cart page. Never cached, never paginated; every line resolves live product, price and stock. Lines whose product was withdrawn, archived or sold out stay with a reason and are excluded from the subtotal.

Auth and Permissions

JwtAuthGuard, IpThrottlerGuard; CUSTOMER_READ 60/min (account-keyed).

Response

200CartResponseDto. Empty cart for a customer who never had one — nothing is created.

Error Cases

None beyond auth/rate-limit.

8.2 GET /api/mobile/cart/summary

Purpose

Badge counters. Two integers, cached 30 seconds, cleared immediately by the customer's own writes. Deliberately carries no version/status.

Response

200{ totalItems, totalQuantity }. totalQuantity counts every line, valid or not — it must not drop when a product goes out of stock.

8.3 GET /api/mobile/cart/checkout-validation

Purpose

Can checkout start? Re-reads product, price and stock and trusts nothing previously loaded — that re-read is the point. Shipping, promotions and address validation are deliberately absent.

Response

200CheckoutValidationDto with readyForCheckout, the re-checked groups and blockingReasonsempty, cart_locked, items_unavailable, insufficient_stock.

8.4 POST /api/mobile/cart/items

Purpose

Add a product — delta, the one non-idempotent verb, for the product card that cannot know the absolute target. Only a currently published or unlisted product can be added; being out of stock does NOT block the add — it is reported on the line and blocks checkout.

Auth and Permissions

CUSTOMER_CART_MUTATION 60/min (account-keyed). Idempotency-Key optional — retry with the same key replays the stored response instead of adding again.

Request

{ "productId": "019fc6…", "variantPublicId": "019fc6…", "quantity": 2, "version": 7 }

Response

200 — whole cart.

Error Cases

HTTPCodeCondition
400validationquantity > 99 in a single request
404CART_PRODUCT_NOT_FOUNDNo such product, one that may not be added (draft, archived, deleted), no such variant, or a variant belonging to a different product — one code covers all four deliberately; telling them apart would let a caller enumerate unpublished product/variant ids
409CART_LOCKED_FOR_CHECKOUTCart locked by checkout
409CART_VERSION_CONFLICTStale version
409CART_ITEM_LIMIT_REACHED50 distinct products
409CART_QUANTITY_LIMIT_EXCEEDEDResulting line quantity > 99 by accumulation

8.5 POST /api/mobile/cart/items/bulk

Purpose

Apply several changes at once — what a debouncing client flushes after multi-line edits. One transaction, one version bump, applied in order. version required; no Idempotency-Key path.

Request

{
  "version": 7,
  "operations": [
    { "op": "set",    "productId": "019fc6…", "variantPublicId": "019fc6…", "quantity": 6 },
    { "op": "add",    "productId": "019fc6…", "variantPublicId": "019fc6…", "quantity": 2 },
    { "op": "remove", "productId": "019fc6…" }
  ]
}

Error Cases

HTTPCodeCondition
400CART_BULK_DUPLICATE_PRODUCTSame product named twice
400CART_VERSION_CONFLICT-style validationMissing version
409CART_VERSION_CONFLICT / capsAny failure rolls the whole batch back; the version does not advance

8.6 PUT /api/mobile/cart/items/:variantPublicId

Purpose

The debounce target. Absolute quantity — retrying sets the same number twice, which is the same number. Upsert: setting a quantity on a product not in the cart adds it. quantity: 0 removes the line, including for a product since archived.

Request

{ "quantity": 6, "version": 7 }

Response

200 — whole cart.

Error Cases

Same as §8.4 minus the idempotency-related ones (structurally idempotent).

8.7 DELETE /api/mobile/cart/items/:variantPublicId

Purpose

Remove a line. Never a 404, and never gated on the product's lifecycle — removing a variant that is not in the cart, or that names nothing, is a success; telling those apart would let anyone enumerate variant ids. version travels as a query parameter.

A no-op removal does not bump version. Both a product id and a variant id are uuid7, so @IsUUID("7") accepts either — a client still sending the old-style product id resolves to no line, removes nothing, and gets 200 with the cart unchanged, including its version. This closed a gap where the old-style id used to invalidate every other device's optimistic-concurrency token for a change that never happened.

Response

200 — whole cart, version unchanged when nothing was removed.

8.8 DELETE /api/mobile/cart/items

Purpose

Empty the cart — removes every line, keeps the cart itself. Idempotent. version as a query parameter.

Response

200 — whole cart.

8.9 GET /api/admin/carts

Purpose

Read-only list, always paginated — the one list endpoint in the API that refuses pagination=false (sending it is a 400), because cart grows one row per customer plus one per order and the list aggregates over the full join. inactiveForDays + status=active is the abandoned-cart list. A date-only createdTo is inclusive of that whole day.

Auth and Permissions

Cart_READ; ADMIN_READ 30/min.

Response

200 — paginated rows with aggregates. Admin subtotal is computed at live prices over every line, including unbuyable ones.

8.10 GET /api/admin/carts/:cartId

Purpose

Read-only detail. Adds lines with a three-field product reference (id, name, sku) — not the storefront contract — and lastKnownUnitPrice.

Error Cases

HTTPCodeCondition
404CART_NOT_FOUNDNo cart with that id

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence (set quantity)

9.3 Error Branch (add)

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /api/admin/cartsoffset, always20100lastActivityAt (default), createdAt, updatedAt, itemCountstatus, customerId, inactiveForDays, createdFrom, createdTo
Customer cart readsnone50 lines / 99 per line

pagination=false is refused with a 400 on the admin list — no such parameter exists there. The customer cart is never paginated.

11. Caching, Jobs, and External Integrations

IntegrationUsed?Details
Redis cacheYesBadge counters only — 30s (CACHE_TTL.VOLATILE), cleared on the customer's own writes, deliberately version-less; the cart itself is never cached
BullMQNo
External APINo

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsDocumented?
GET /api/mobile/cartgetCartQueryService.buildResponseJWT+IpThrottlecart, items, products, inventoryYes
GET /api/mobile/cart/summarysummary…getCountsJWT+IpThrottle30scart, itemsYes
GET /api/mobile/cart/checkout-validationcheckoutValidation…buildCheckoutValidationJWT+IpThrottleeverything freshYes
POST /api/mobile/cart/itemsaddAddCartItemDtoCartCustomerService.addJWT+IpThrottle+Idempotencyclearcart, cart_item, products400/404/409Yes
POST /api/mobile/cart/items/bulkbulkBulkCartMutationDtoCartBulkService.applyJWT+IpThrottleclearcart, many cart_item, products400/409Yes
PUT /items/:variantPublicIdsetQuantitySetCartItemQuantityDtoCartCustomerService.setQuantityJWT+IpThrottleclearcart, cart_item400/404/409Yes
DELETE /items/:variantPublicIdremoveCartVersionQueryDto…removeJWT+IpThrottleclearcart, cart_itemYes
DELETE /itemsclearCartVersionQueryDto…clearJWT+IpThrottleclearcart, cart_itemYes
GET /api/admin/cartslistListCartsQueryDtoCartAdminService.listJWT+Role+IpThrottleCart_READcart ⋈ item ⋈ product400 (no pagination param)Yes
GET /api/admin/carts/:cartIdfindOneCartAdminParamsDtoCartAdminService.findOneJWT+Role+IpThrottleCart_READcart + lines404Yes

13.2 Request/Response Exhaustiveness

Covered in §8: minimal/full request bodies (§6.1–6.3/8.4–8.6), success responses (§8.1, §8.6), empty-cart response (§6.5/8.1), the invalid-line-with-reason shape (§6.5), the two shapes of "too many units" (400 vs 409, §8.4), domain errors per endpoint (§8 error tables), rate-limit behavior (account-keyed; mutation budget 60/min), admin 400-on-pagination (8.9).

13.3 API Diagram Pack

Route ownership (§9.1), sequence per endpoint family (§9.2, backend §7), error decision tree (§9.3), cache flow (backend §8), async/job flow — none.

13.4 Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Web frontendDebounce with absolute PUT; whole-cart responses; version for steppers409 CART_VERSION_CONFLICT → refetch and reapply; 404 on add → refresh productStable
Mobile appIdempotency-Key if retrying POST /items; account-keyed rate limits429 → back offStable
Admin panelAlways-paginated list; date-only createdTo inclusive; read-only400 on pagination paramStable
QAExact vs approximate caps, oversell lines valid, price-change acknowledgementReproduce via exact codesStable
Checkout (future)Must own the checkout_locked return edge; snapshot nothing from the cart for charging409 CART_LOCKED_FOR_CHECKOUT semanticsStable

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
Absolute PUTRetry-safe stepperDelta PUTCoalesced flush cannot double-applyClient must know the targetUpsert removes the guess
One non-idempotent POSTDelta add with optional keyAll-deltaProduct card works blindRetry doubles without headerOptional Idempotency-Key
Version optional except bulkTrust modelAlways requiredProduct-card add has no cartSilent clobberDocumented; bulk enforced
Live data on readNever staleSnapshot on addCart is intent, not a chargeRead costBatched seam
Admin always paginatedRefuse unboundedAccept pagination=falseAggregate list is unboundedLoud 400
Admin read-onlyNo cart tamperingAdmin editsNo audit trail existsOperators can't fixAccepted
Mutation budget 60/minStepper bursts allowed20/minBeing rate-limited out of your own cart is an outageAbuse windowAccount-keyed

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
checkout_locked written (future)Cart clientsStatus guardNoneNoNothing writes it today; contract frozen

14. Zero-Omission API Checklist

  • Every controller route is documented (§4, §8, §13.1).
  • Every parent route prefix and runtime URL is documented (§2, §4).
  • Every DTO field, enum, default, transform and validator is documented (§6, §7).
  • Every response field and nullable field is documented (§6.5, §8).
  • Every auth, guard, permission and guest identity branch is documented (§5).
  • Every success, validation, not-found, conflict, rate-limit and server-error branch is documented (§8).
  • Every DB read/write, cache hit/miss/invalidation and external call is documented (§11, backend §8).
  • Every route has examples for minimal request, success response and representative failures (§8).
  • Every endpoint family has route, sequence and error diagrams (§9, backend §7).
  • Every tradeoff and compatibility risk is documented (§13.5, §13.6).
  • The API doc links to backend and features/flows (§1, See Also).

15. Integration Checklist

  • Every route from controllers is documented.
  • Every DTO field is documented.
  • Every enum value is documented.
  • Every response envelope is documented.
  • Every error code is documented.
  • Every auth guard and permission is documented.
  • Every cache key, queue job and external call is documented.
  • Every diagram matches the current code.
  • The API doc links to backend and features/flows.

See Also