Happy House - Ecommerce Docs
Developer ResourcesProducts

Products Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Products module.

Products Features and Flows

Use this page for the product domain: what it does for users, admins, workers and systems, and how each flow behaves from start to finish.

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Technical designapps/api/src/modules/products/shared/product-lifecycle.service.ts, product-filter.builder.ts, product-storefront-query.service.tsState machine, filter composition, cursor pagination, literal-predicate rule
APIapps/api/src/modules/products/admin/product/*.controller.ts, customer/**/*.controller.ts, admin/job/product-job-admin.controller.ts, catalog/{admin,customer}/tag/*.controller.tsRoute surface, permissions, rate limits, actors
Backendproduct-write.service.ts, product-write-facets.service.ts, product-bulk.service.ts, product-response.builder.ts, import-export/*, workers/*, outbox/*Business rules, side effects, jobs, outbox enqueue
Schemapackages/db/src/schema/products/*.ts, packages/db/src/schema/catalog/tag.tsTables, constraints, composite FK, GENERATED state
Moneyapps/api/src/utils/money/money.util.tsMinor units, no-tax rule, derived discounts

2. Feature Summary

FieldValue
Moduleproducts
Submodulecatalog/tag (tag entity), outbox (enqueue infra), blog (linking)
Primary user valueA published catalogue of sellable products with filters, search and reliable URLs, maintained by admins through CRUD, bulk operations and file import/export
ActorsGuest, admin, worker/system, blog author
Main entry points/api/admin/products, /api/admin/products/jobs, /api/admin/catalog/tags, /api/mobile/products, /api/mobile/products/discovery/:feed, /api/mobile/catalog/tags, blog post create/update/detail
Main outputsProduct responses (grouped shape with a populated inventory group), CSV exports, outbox_events rows, activity log entries, cache invalidations
Related docsAPI, Backend

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
GuestBrowse list, search, suggestions, discovery feeds, product detail by slug (current or retired)Admin mutations, see draft/archived products, see unlisted products outside direct URLNone (@Public())Rate-limited per IP (PUBLIC_READ 60/min, PUBLIC_SEARCH 60/min, PUBLIC_SUGGEST 300/min, PUBLIC_HIGH_FREQUENCY 300/min)
AdminFull product CRUD, facet PATCHes, lifecycle transitions, bulk delete/restore/lifecycle/discovery, tag CRUD, import/export jobs, cancel jobsChange lifecycle of archived products (read-only), edit products of another admin without the current version (optimistic lock), set stock status on tracked productsAdmin JWT + Products_* / Tags_* / Inventory_* permissionEvery mutation writes an activity record and invalidates cache
Worker/systemImport/export product jobs, outbox dispatch, stalled-job sweepsBullMQ workerLease-based claim; cooperative cancellation; at-least-once delivery — consumers idempotent
Blog authorLink products to posts (productIds on create/update)Link non-published products on the storefront view (published-only)Blog permissionsBatched reads, no N+1

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
List productsStorefrontGuestGET /api/mobile/productsPublished rows + filtersAPI
Search productsStorefrontGuestGET /api/mobile/products/searchPublished rows, trigram rankAPI §5
SuggestionsStorefrontGuestGET /api/mobile/products/suggestionsPublished rowsAPI §5
Product detail by slugStorefrontGuestGET /api/mobile/products/:slugSlug ownership + rowAPI §5
Discovery feedsStorefrontGuestGET /api/mobile/products/discovery/:feedPartial index per flagAPI §5
Admin list / detailAdminAdminGET /api/admin/products(/:publicId)All rows incl. deletedAPI §3
Create productAdminAdminPOST /api/admin/productsproduct row, product_slug, inventory row, seoAPI §3
General updateAdminAdminPATCH /api/admin/products/:publicIdRow + versionproduct rowAPI §3
Lifecycle transitionAdminAdminPATCH /:publicId/lifecycleRow + thumbnail + publishedAtproduct.status, seo.robots_indexAPI §3
Facet PATCHes (stock/pricing/media/seo/discovery/tags)AdminAdminPATCH /:publicId/{stock,pricing,media,seo,discovery,tags}Row + inventory row (stock)Facet fields, version bumpAPI §3
Soft delete / restoreAdminAdminDELETE /:publicId, POST /:publicId/restoreRowdeleted_atAPI §3
Bulk opsAdminAdminPOST /api/admin/products/bulk/*RowsBulk state changesAPI §3.4
Tag CRUD + visibilityAdminAdmin/api/admin/catalog/tagsTag rows + link countsTag rowsAPI §4
Tag listing/detailStorefrontGuest/api/mobile/catalog/tagsVisible tagsAPI §6
Import jobAdmin/workerAdmin → systemPOST /api/admin/products/jobs/import + processorproduct_job + live products by SKURows (create/update), job stateAPI §7
Export jobAdmin/workerAdmin → systemPOST /api/admin/products/jobs/export + processorProduct rowsCSV file, job stateAPI §7
Blog linkingBlog authorAuthorblog post create/updatePosts + productsblog_post_product rowsAPI §9

5. User-Facing Flows

5.1 Browse a category landing page

Summary

A guest opens a category page. The frontend calls the storefront list with categorySlug, sorts by newest, and renders page 1; "load more" follows nextCursor.

Preconditions

  • Product rows exist with status = 'published' AND deleted_at IS NULL.
  • The category slug resolves to a live category; its descendants are expanded server-side.

Main Flow

StepActor/SystemActionResultSource
1GuestOpens category URLList request with categorySlug + sortproduct-customer.controller.ts
2BackendResolves slug → descendant category ids, composes filters, inlines the published literalIndex Scan over the partial indexproduct-storefront-filter-resolver.service.ts, product-filter.builder.ts
3BackendKeyset-paginates on (sortKey, id), over-fetches one rowdata + nextCursorproduct-storefront-query.service.ts
4BackendBatches slug, classification and media resolutionGrouped response, one query per concernproduct-response-assembler.service.ts
5GuestFollows nextCursor until nullStable ordering across pages

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Unknown category slugSlug resolves to nothing404 before any queryCATEGORY_NOT_FOUND-style 404
Tag filter resolves emptyRequested tag slugs unknown/hidden/deletedEmpty page, never the whole catalogueEmpty data, no error
Cursor replayed with different sortsort changed between pagesLoud 400, restart paginationPAGINATION_CURSOR_INVALID
Term under 2 charsq too short"No search" — filters still applyFull filtered list
relevance without termsort=relevance, no usable qFalls back to newestNo error

Summary

Search composes with every list filter; ranking is trigram similarity on the product name, ordered by rank then id. relevance is legal only on /search.

5.3 Product detail via retired slug

Summary

GET /api/mobile/products/:slug accepts a current or retired slug. The slug ownership table holds current and retired slugs under one unique constraint, so an old URL can never resolve to a different product. The response's basic.slug is always the current slug; the frontend issues the redirect.

BranchConditionBehavior
Retired slugSlug owned by the product, is_current = false200 + current slug in basic.slug
Unlisted productDirect URL onlyResolves here, nowhere else
Hidden/deleted/draft/archivedNot customer-visible404 on both slug branches

6. Admin Flows

6.1 Create a product

Permission Products_CREATE, rate limit ADMIN_WRITE 10/min, Idempotency-Key required (scope product).

Side effects: product + product_slug + inventory (untracked) rows written in one transaction, activity record, cache invalidation, seo row forced non-indexable unless published.

6.2 Lifecycle transition

PATCH /:publicId/lifecycle — see section 7. Failure branches: 409 PRODUCT_INVALID_STATUS_TRANSITION, PRODUCT_THUMBNAIL_REQUIRED_TO_PUBLISH, PRODUCT_ARCHIVED_NOT_EDITABLE, PRODUCT_VERSION_CONFLICT.

6.3 Stock status (narrowed by inventory)

For a tracked product the column belongs to inventory: non-pre_order requests 409 INVENTORY_STOCK_STATUS_DERIVED; pre_order requires inventory oversell (409 INVENTORY_PRE_ORDER_REQUIRES_OVERSELL); leaving pre_order re-derives from inventory counters. Untracked products keep the free set.

6.4 Bulk operations

POST /api/admin/products/bulk/{delete,restore,lifecycle,discovery} — per-item result (succeeded/failures), cap 100 unique ids (409 PRODUCT_BULK_LIMIT_EXCEEDED), Idempotency-Key required (scope product-bulk). One item failing does not roll back the rest.

6.5 Import / export jobs

See backend §10 — Jobs. Import needs Products_CREATE and Products_UPDATE (route checks the first, the service re-checks both — 403 PRODUCT_JOB_ENTITY_PERMISSION_DENIED otherwise). Both submits require Idempotency-Key; rate limit ADMIN_ASYNC_JOB_SUBMIT 10/hour.

7. Lifecycle and State Transitions

EntityFromEvent/ActionToGuard ConditionSide Effects
productdraftpublishpublishedThumbnail set; publishedAt stamped onceseo.robots_index = true; cache invalidation
productdraftarchivearchivedNone (may lack thumbnail)seo.robots_index = false
productpublishedunlistunlistedrobots_index = false; leaves listings but stays purchasable
productpublished/unlistedarchivearchivedRead-only; only transition out is → draft
productpublished/unlistedunpublishdraftpublishedAt NOT cleared (answered "first live")
productarchivedunarchivedraftAdmin-editable again

published and unlisted both require published_at (CHECK-enforced); archived does not.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Create productproduct, product_slug, inventory, seoproduct/brand/tag/inventory domains
Lifecycle transitionproduct.status, seo.robots_indexproduct domain + search
Import jobproduct*, product_slug, inventory (per row), product_jobproduct/inventory domainsproduct.import_entities via outbox
Export jobproduct_job terminal rowproduct.export_entities via outbox
Blog link writeblog_post_productblog domain
All admin mutationsRow + activity recordowning domain tags + Redis patterns

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Optimistic lock conflictAnother admin changed the row since the caller read it409 PRODUCT_VERSION_CONFLICTRe-read, re-apply, retryproduct-write.service.ts
Import job fails mid-runWorker errorJob failed with errors[]; rows rolled backRetry submit (idempotency key) or fix fileproduct-import.processor.ts
Export too largeFilter set matches > 50,000 rowsJob fails, no fileNarrow filtersproduct-export.service.ts
Cancelled importAdmin cancels a processing jobCooperative abort — transaction rolls backSubmit againproduct-job-state.ts
Outbox dispatch failureRedis/BullMQ down at submitJob row stays queued; enqueue row parked on retryDispatcher retries pending rowsmodules/outbox/

11. Diagrams Required Per Module

  • Actor capability diagram — §3/§4 tables (capability matrix doubles as the map).
  • High-level module flow — see 5.2 search blueprint and 6.1 admin flow.
  • Sequence diagram per major flow — §5.1.
  • State machine diagram — §7.
  • Data side-effect diagram — §9.
  • Error branch diagram — §10 and 6.2/6.3.

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
Storefront listEmpty page for unresolvable tag setGuesttags filterEmpty dataWHERE false predicateproduct-filter.builder.ts
Storefront listCursor-sort mismatchGuestCursor from another sort400Throw before queryproduct-storefront-query.service.ts
Storefront listonSale=falseGuestFilterNon-discounted productsselling_price >= mrpproduct-filter.builder.ts
Storefront listOver-fetch +1GuestAny pagenextCursor presencelimit + 1 rowsproduct-storefront-query.service.ts
DetailThumbnail fallbackGuestNo thumbnailKeyFirst gallery item by orderresolveThumbnailSourceproduct-response.builder.ts
DetailRetired slugGuestOld URL200 + current slugSlug ownership lookupslug-ownership.service.ts
SuggestionsNo termGuestEmpty qUnfiltered suggestionsMIN term checkproduct-customer.service.ts
Admin listDeleted-only viewAdminincludeDeletedRestore workflow listPartial index on deleted_atproduct-admin.service.ts
Admin PATCHDeleted entity guardAdminUpdate on deleted row409PRODUCT_ALREADY_DELETED-style guardproduct-write-support.service.ts
ImportSKU match semanticsAdminFile rowUpdate live / create draftPer-row upsertproduct-import.service.ts
ImportQuery-count guardWorker500 rows / 3 idsExactly 3 warm-up queriesMemoised classification cacheproduct-write-support.service.ts
JobsisStalled signalAdminDetail viewFlag when past claim TTLDerived, no sweep dependencyproduct-job-admin.service.ts

12.2 Business Process Diagram Pack

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
Storefront reads only published, non-deletedDrafts and archived rows must never reach customersGuest sees only live rowsSQL literal in product-filter.builder.tsList/search/feed/detailPartial-index usability (EXPLAIN-proven)product-storefront-query.service.spec.ts
published/unlisted require a thumbnailA customer-visible product needs an image; drafts may not have one yetAdmin gets 409 on publish without imageDB CHECK + lifecycle servicePATCH /lifecycleTransactional guardproduct-lifecycle.service.spec.ts
publishedAt set once, never cleared"First live" is a question worth keepingAdmin sees original publish dateSchema comment + serviceResponse timestamps.publishedAtWritten on first live transitionschema probe
archived is read-onlyArchive is retirement, not editingAdmin must unarchive firstLifecycle service409 on any facet PATCHSingle write pathspec
SKU unique among live rowsSoft-deleting releases the SKU for reuseAdmin can reuse after deletePartial unique index409 PRODUCT_SKU_ALREADY_EXISTSuq_product_sku_liveprobe
Series must belong to the product's brandCross-brand series is unrepresentableAdmin gets 409/23503Composite FK + service checkCreate/update rejectfk_product_brand_seriesconstraint probe 63/63
sellingPrice <= mrpMoney invariant of the whole commerce systemAdmin cannot overpriceDB CHECK409 PRODUCT_SELLING_PRICE_ABOVE_MRPCHECK chk_product_selling_price_not_above_mrpprobe
pre_order requires inventory oversellPre-order freezes the stock status once trackedAdmin gets 409 without oversellproduct-stock-write.util.tsPATCH /stockNarrowed writespec
Optimistic lock on versionConcurrent editors must not silently overwriteAdmin gets 409 on stale writeService + schema409 PRODUCT_VERSION_CONFLICTversion bumped per writespec

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Cursor pagination on (sortKey, id)Stable ordering across pagesKeyset over offset; over-fetch instead of COUNTOffset pagesCursor only meaningful per sortCursor-sort mismatch 400 (fail loud)
Four separate discovery flags + partial indexesOne feed per flag, each index-sizedEach feed is an index scanOne composite flag indexFour indexes on writesWrite amplification accepted
Money in integer minor unitsExact prices on statementsInteger arithmetic, no driftFloats/stringsConversion ceremony at boundariesisSafeInteger boundary documented
pre_order as explicit stock statusBackorder workflow expressibleEnum value today, no later migrationBoolean pairInventory module derives status laterNarrowed product PATCH
promotions/inventory reserved null keysConsumers build against a stable shapeAdditive modules, no breaking changeOmit keys until they existConsumers must not treat null as a bugDocumented as reserved

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
ListDuplicate actionSame cursor twiceSame page re-servedSame data/nextCursorkeyset WHERE
ListEmpty stateNo published rowsEmpty data, nextCursor: nullEmpty UIquery
ListLast itemFinal pageNo further cursornextCursor: nullover-fetch
SearchToo-short termq < 2 charsFilters apply, no searchFull filtered listMIN term
DetailExpired/retired slugRenamed product, old URLCurrent slug returnedFrontend redirectslug ownership
LifecycleDuplicate publishPublish twiceSecond is legal (idempotent state)200state machine
ImportDuplicate submitSame idempotency keyReplay returns original jobSame responseidempotency interceptor
ImportConcurrent cancelCancel during writeCooperative abort, rollbackJob cancelledPRODUCT_JOB_NOT_PROCESSING
Admin PATCHStale versionTwo admins edit409 conflictRe-read and retryoptimistic lock
ExportZero rowsEmpty filter setHeader-only CSV, completedDownload of valid fileexport service
CacheStale readWrite without refetchInvalidation after commitFresh on refetchcache domains

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
List/search/feedproduct, product_slug, product_tag_link, inventoryproduct domainsGrouped shape
Create/updateproduct, classification refsproduct, product_slug, inventory, seoinvalidate domainsactivityGrouped + version/metadata (admin)
Importproduct by SKU, classificationRows + product_jobinvalidateoutbox → product.import_entitiesJob response
Exportproduct rowsproduct_job + CSVoutbox → product.export_entitiesJob response

12.7 Experience Quality Checklist

  • The doc explains what the actor is trying to accomplish.
  • The doc explains what the backend does that the actor does not see (literal-predicate rule, outbox, guarded UPDATEs).
  • The doc covers every minor flow and branch (12.1, 12.5).
  • The doc includes user, admin, worker, and system flows.
  • The doc explains business logic, tradeoffs, and rationale.
  • The doc maps every flow to API routes and backend side effects.
  • The doc includes diagrams appropriate to each flow type.
  • The doc covers edge cases and failure recovery.

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed (§4, §12.1).
  • Every actor has allowed and forbidden behavior (§3).
  • Every major and minor flow includes steps, branches, and diagrams (§5, §6, §12.2).
  • Every lifecycle has a transition table and state diagram (§7).
  • Every flow links to the API and backend docs.
  • TDD dependencies are called out where they shape behavior (no TDD pages published yet).

See Also