Skip to content

Product Catalog System - Deep Dive Documentation

Generated: 2026-04-11 Scope: Full-stack Product Catalog — Backend Product/ProductCategory/Image modules + entities + frontend pages/components/API hooks Files Analyzed: 23 (13 backend + 10 frontend) Lines of Code: ~2,786 Workflow Mode: Exhaustive Deep-Dive Severity Breakdown: 18 Critical, 17 High, 22 Medium, 19 Low — 76 total issues

Overview

The Product Catalog system is a full-stack e-commerce feature consisting of Product CRUD with multi-image support, ProductCategory management, and a standalone Image upload module. The backend is functionally complete (endpoints exist, entities defined, validation schemas present) but the frontend is entirely non-functional — every API call is commented out and replaced with hardcoded mock data and simulated delays.

Purpose: Enable SaaS tenants to manage a product catalog with categories, pricing, discount pricing, multi-image uploads, and search/filter capabilities.

Key Responsibilities: - Product CRUD with multi-image upload (up to 10 images via multer) - Product category management (name uniqueness, CRUD) - Standalone image upload endpoint - Public product browsing (GET endpoints are unauthenticated) - Dashboard product management with RBAC permission gates

Integration Points: - handleFileUpload / saveBufferToFile helpers for image storage (Cloudinary/S3/local) - checkPermissionAndThrow for RBAC enforcement - getDbRepository shared utility for TypeORM access - catchAsync error wrapper on all controllers - sendResponse standardized response formatter - validateRequest Zod middleware for request validation - PermissionWrapper frontend component for UI permission gating


Critical Architecture Finding

The entire Product Catalog frontend is a UI mockup with zero backend integration.

Every product CRUD operation (list, create, edit, delete) uses hardcoded mock data and setTimeout delays to simulate API calls. The only functional frontend integration is the Product Categories page, which makes real API calls via axios. This means:

  • Users cannot create, edit, or delete products through the UI
  • The products list always shows the same 15 hardcoded items
  • The edit page always loads "Wireless Bluetooth Headphones" regardless of product ID
  • Delete operations only remove items from React local state (lost on page refresh)
  • The backend API is fully functional but never called

Known Issues Summary

Critical (18)

# Location Issue Impact
C1 product.validation.ts:15-21 DUAL field mismatch: schema uses originalPrice (z.string()) but entity has discountPrice (decimal(10,2)) — wrong name AND wrong type. Additionally, Zod's default z.object() strips unknown keys and validateRequest overwrites req.body with the stripped result, so even sending the correct field name discountPrice from the frontend would be silently stripped before reaching the controller Data loss at two stages: (1) Zod strips discountPrice as unknown key, (2) TypeORM ignores originalPrice (no matching column)
C2 product.service.ts:103 updateProduct permission check commented out Any authenticated user can update any product
C3 product.service.ts:119-129 deleteProductById has zero permission check — complete RBAC bypass Any authenticated user regardless of role (viewer, editor, subscriber) can soft-delete any product
C4 product.service.ts:35,76,122 All queries filter isDeleted: false but ignore @DeleteDateColumn Products soft-deleted via TypeORM deletedAt still appear in queries
C5 product_category.routes.ts:3,18,24 Routes import BlogCategoryValidation instead of ProductCategoryValidation Wrong validation rules applied to all category create/update requests
C6 product_category.service.ts:104 Type cast to DeepPartial<BlogCategory> instead of ProductCategory TypeScript type error; wrong entity type in update logic
C7 product_category.service.ts:125 Uses repo.remove() (hard delete) despite entity having @DeleteDateColumn Permanent data loss — categories irrecoverably deleted
C8 seed/data/permission.ts Zero product permissions seeded (products.create, product-category.*) All checkPermissionAndThrow calls will deny access for non-super_admin
C9 products-page.jsx:381-408 Product list uses hardcoded mock data; API call commented out Products page non-functional — always shows same 15 items
C10 create/page.js:13-25 Create product API call entirely commented out Product creation non-functional
C11 [id]/page.js:25-35 Edit product fetch entirely commented out; uses mock data Edit page loads same mock product for all IDs
C12 [id]/page.js:84-95 Edit product save API call entirely commented out Product editing non-functional
C13 products-page.jsx:451-455,562-566 Delete/bulk delete only update local React state Products reappear on page refresh
C14 product-performance.jsx Component named "ProductPerformance" but displays project/task data Dashboard card shows irrelevant data ("Minecraf App", "Web App Project")
C15 api/products/index.js File is completely empty (0 exports) No React Query hooks for product CRUD exist
C16 product.controller.ts:122-126 updateProduct returns HTTP 201 with message "Product created successfully" Wrong status code (should be 200) and wrong message (should say "updated")
C17 product.validation.ts:9 Images array validates .url() which rejects local paths /uploads/products/... Local storage mode will always fail Zod validation
C18 product.service.ts:91 Permission string "products.create" uses plural; other modules use singular Inconsistent permission naming; will never match seed data if added

High (17)

# Location Issue
H1 product_category.routes.ts:12 ALL category routes require auth — no public read for product filters
H2 product_category.controller.ts:13,24,38,67,81 5 response messages say "Blog" instead of "Product"
H3 product_category.service.ts:18,30,53 No .view permission check on read operations
H4 image.route.ts:9 Image upload endpoint has no auth middleware — public upload
H5 image.service.ts:11 Storage hardcoded to "cloudinary" — ignores AppConfig setting
H6 Product.ts:24-25 Image array column has no URL format validation or array size limit
H7 Product.ts:45-46 No DB constraint that discountPrice < price
H8 product.controller.ts:39-42 Cloud image uploads not rolled back if DB save fails (orphaned files)
H9 product.controller.ts:57,105 DB save happens BEFORE local file save (non-atomic)
H10 product.validation.ts:13 price field is .optional() but entity column is required (no default)
H11 products-page.jsx:463-505 Sort only sorts current page slice, not entire filtered dataset
H12 product-form.jsx:30-40 Category dropdown uses hardcoded mock options instead of API
H13 product-form.jsx:154-156 Frontend sends removedImageIds but backend doesn't handle it
H15 product-form.jsx:511 Image preview uses /api/images/${filename} — endpoint doesn't exist
H16 Moved to L17 (downgraded — style inconsistency, no functional impact)
H17 products-page.jsx:372-378 Category filter options hardcoded (5 static strings)
H18 products-page.jsx:310-342 Analytics data calculated from mock data; change percentages hardcoded
H19 product_category.validation.ts:31 Export named BlogCategoryValidation instead of ProductCategoryValidation

Medium (22)

# Location Issue
M1 Product.ts:30-38 categoryId FK is nullable — products can exist without category
M2 Product.ts:41-43 No DB constraint for price >= 0 (only Zod validation)
M3 ProductCategory.ts:13 No explicit @Entity("product_categories") — relies on TypeORM default
M4 ProductCategory.ts:18-20 No max length on category name column
M5 ProductCategory.ts:28-32 Timestamps use "timestamp" not "timestamptz" (inconsistent with Product entity)
M6 product.service.ts:39 Price range filter has no validation minPrice <= maxPrice
M7 product.service.ts:116 Object merge {...product, ...payload} has no explicit ID safety
M8 product.controller.ts:60-71 Promise.all for file saves has no error handling
M9 product.validation.ts:12 categoryId allows 0 or negative (no minimum check)
M10 product_category.validation.ts:31 Export named BlogCategoryValidation instead of ProductCategoryValidation — copy-paste naming artifact only; validated fields (name, description) are correct for ProductCategory
M11 product_category.service.ts:68,72 createCategory uses toLocaleLowerCase() for duplicate check (line 68) but toLowerCase() for name storage (line 72) — locale-sensitive characters could bypass uniqueness check
M12 product_category.routes.ts:28-31 DELETE route has no validateRequest middleware
M13 image.service.ts:7-8 Returns undefined if no file provided; controller doesn't handle
M14 product-categories-page.jsx:8 Hook's error state not destructured (errors silently ignored)
M15 product-form.jsx:94 categoryId stored as string; backend expects number (z.number().int())
M16 product-form.jsx:218-222 Max 10 images check is commented out
M17 product-form.jsx:139 Price type mismatch between form (string) and backend (number)
M18 [id]/page.js:22-38 Mock product has different image structure than backend entity
M19 products-page.jsx:429-434 Price filter uses parseFloat on form strings — NaN if non-numeric
M20 product-performance.jsx:41 Typo: "Minecraf App" (missing 't')
M21 product-performance.jsx:27 Grid uses grid-cols-5 with no responsive breakpoints
M22 product-form.jsx:441-450 Frontend requires description (min 10 chars); backend makes it optional

Low (19)

# Location Issue
L1 Product.ts:48-49 No ISO 4217 validation for currency codes
L2 Product.ts:51-52 Redundant isDeleted boolean alongside @DeleteDateColumn
L3 ProductCategory.ts:34-35 Missing isDeleted boolean (inconsistent with Product pattern)
L4 product.service.ts:81 baseUrl parameter unused in transformImageUrl call
L5 product.routes.ts:20 Redundant router.use(auth(), contextMiddleware) (already applied line 12)
L6 product.validation.ts:23 No ISO 4217 validation for currency field
L7 image.controller.ts:6 Unsafe type cast req.file as Express.Multer.File (no null check)
L8 product_category.controller.ts:20 Inconsistent response structure (nested categories + meta)
L9 products/page.js No Next.js metadata export (missing page title/description)
L10 categories/page.js No Next.js metadata export
L11 products-page.jsx:799 Description truncated with hardcoded slice(0, 40)
L12 products-page.jsx:743 Indeterminate checkbox state not semantic (missing ARIA attribute)
L13 product-form.jsx:206 File type validation only via MIME (can be spoofed)
L14 product-form.jsx:210 5MB file size limit hardcoded (no config)
L15 product-performance.jsx:20-23 Date selector non-functional (no click handler)
L16 All files Zero test coverage across entire Product Catalog system
L17 product-categories-page.jsx:11,14,18 API URLs use relative paths (no NEXT_PUBLIC_API_URL prefix) while hook uses absolute — style inconsistency only, both resolve correctly (axios ignores baseURL for absolute URLs)
L18 Product.ts:60 @DeleteDateColumn({ nullable: true }) has no explicit type: "timestamptz" — inconsistent with createdAt/updatedAt which specify { type: "timestamptz" }. TypeORM defaults to timestamp (no timezone)
L19 product.routes.ts:28-31 Product DELETE route missing validateRequest middleware (same issue as M12 for category DELETE) — :id param has no schema validation

Complete File Inventory

Backend Files (13 files, ~825 LOC)


saas-boilerplate/src/entity/Product.ts (62 LOC)

Purpose: TypeORM entity defining the products table with columns for product data, pricing, images, and category FK.

What Future Contributors Must Know: This entity uses a DUAL soft-delete pattern (isDeleted boolean + @DeleteDateColumn) but the service layer ONLY checks isDeleted — products deleted via TypeORM's softDelete() or softRemove() will still appear in queries. The images column stores an array of URL/path strings (TEXT array type), not file metadata objects.

Exports: - Product (class) — TypeORM entity with 10 columns + 1 relation

Columns: | Column | Type | Constraints | Notes | |--------|------|-------------|-------| | id | int (PK) | auto-increment | Standard | | name | varchar(100) | indexed, not null | Matches validation max | | images | text[] | nullable | Array of URL strings; no individual URL validation | | description | text | nullable | No length constraints | | categoryId | int (FK) | nullable, indexed | FK to ProductCategory; CASCADE delete | | price | decimal(10,2) | indexed, not null | No DB >= 0 constraint | | discountPrice | decimal(10,2) | nullable | No constraint < price | | currency | varchar(3) | default "USD" | No ISO 4217 validation | | isDeleted | boolean | default false | Redundant with deletedAt | | createdAt | timestamptz | auto | Timezone-aware | | updatedAt | timestamptz | auto | Timezone-aware | | deletedAt | timestamp (no timezone — TypeORM default, no explicit type specified) | auto (@DeleteDateColumn) | Never used by service; inconsistent with createdAt/updatedAt which use timestamptz |

Relations: - @ManyToOne(() => ProductCategory, { onDelete: "CASCADE" }) — Deleting a category cascades to delete all its products

Dependencies: - typeorm — Entity, Column, PrimaryGeneratedColumn, ManyToOne, DeleteDateColumn decorators - ./ProductCategory — relation target

Used By: - product.service.ts — all CRUD operations - product_category.service.ts — relation reference - dbConfig.ts — auto-loaded via glob src/entity/**/*.{ts,js}

Issues: C4 (dual soft-delete bypass), H6 (no image validation), H7 (no discountPrice constraint), M1 (nullable FK), M2 (no price constraint), L1 (no ISO 4217), L2 (redundant isDeleted), L18 (deletedAt missing timestamptz)


saas-boilerplate/src/entity/ProductCategory.ts (36 LOC)

Purpose: TypeORM entity defining product categories with name, description, and one-to-many relation to Product.

What Future Contributors Must Know: Despite having @DeleteDateColumn, the service uses repo.remove() (hard delete), not repo.softRemove(). Timestamps use "timestamp" (no timezone) while Product uses "timestamptz" — potential timezone comparison bugs.

Exports: - ProductCategory (class) — TypeORM entity with 5 columns + 1 relation

Columns: | Column | Type | Constraints | Notes | |--------|------|-------------|-------| | id | int (PK) | auto-increment | Standard | | name | varchar | indexed, not null | No max length defined | | description | text | nullable | No constraints | | createdAt | timestamp | auto | Missing timezone (inconsistent) | | updatedAt | timestamp | auto | Missing timezone | | deletedAt | timestamp | auto (@DeleteDateColumn) | Never used — service hard-deletes |

Relations: - @OneToMany(() => Product, product => product.category) — inverse of Product.category

Dependencies: - typeorm — decorators - ./Product — relation target

Used By: - product_category.service.ts, product.service.ts (category validation)

Issues: C7 (hard delete despite @DeleteDateColumn), M3 (no explicit table name), M4 (no name length), M5 (timezone mismatch), L3 (missing isDeleted)


saas-boilerplate/src/app/modules/v1/Product/product.service.ts (139 LOC)

Purpose: Business logic for Product CRUD — list (paginated, filterable), get by ID with similar products, create with permission check, update (permission commented out), soft-delete (no permission check).

What Future Contributors Must Know: Only createProduct has an active permission check ("products.create"). The updateProduct permission check is commented out (line 103), and deleteProductById has none. All queries filter by isDeleted: false but ignore the deletedAt column entirely, creating a soft-delete bypass.

Exports: - getProductList(query, baseUrl) — paginated list with filters (category, price range, search) - getProductById(id, baseUrl) — single product + 4 similar products from same category - createProduct(payload) — creates product with category validation; checks "products.create" permission - updateProduct(id, payload) — updates product; permission check commented out - deleteProductById(id) — soft-deletes via isDeleted = true; no permission check

Permission Strings Used: - "products.create" (line 91) — plural, dot-notation - "product.update" (line 103, commented out) — singular, dot-notation - None for delete

Dependencies: - ../../shared/getDbRepository — database access - ../../shared/sendResponse — response formatting - ../../helper/checkPermissionAndThrow — RBAC enforcement - ../../shared/getPaginationParams — pagination parsing - ../../shared/getQueryBuilder — TypeORM query builder factory - ../../helper/transformImageUrl — image URL transformation - ../../entity/Product, ../../entity/ProductCategory

Used By: - product.controller.ts — all 5 controller handlers

Side Effects: - Database reads/writes via TypeORM - Permission check via checkPermissionAndThrow (reads from permission cache/DB)

Issues: C2 (update perm commented out), C3 (delete no perm), C4 (ignores deletedAt), C18 (plural permission string), H10 (price optional), M6 (no min<=max validation), M7 (unsafe merge), L4 (unused baseUrl)


saas-boilerplate/src/app/modules/v1/Product/product.controller.ts (154 LOC)

Purpose: Express request handlers for Product endpoints — handles multipart file uploads (multer), delegates to service, manages image storage flow (cloud vs local).

What Future Contributors Must Know: The image upload flow has a critical ordering bug: the database save happens BEFORE local file saves. If saveBufferToFile fails after the DB record is created, you get a product with invalid image paths. Cloud uploads are also not rolled back on DB failure (orphaned files). The updateProduct handler returns HTTP 201 with message "Product created successfully" — both wrong.

Exports: - getProductList(req, res) — GET /products - getProductById(req, res) — GET /products/:id - createProduct(req, res) — POST /products (multipart) - updateProduct(req, res) — PATCH /products/:id (multipart) - deleteProduct(req, res) — DELETE /products/:id

Image Upload Flow:

1. multer parses files (up to 10)
2. If cloud storage: uploadImage() → assigns URLs to payload.images
3. If local storage: map files to /uploads/products/{timestamp}-{name}
4. ProductService.createProduct(payload) — DB SAVE
5. If local storage: saveBufferToFile() for each file — FILE SAVE (after DB!)

Dependencies: - ../Product/product.service — business logic - ../../helper/uploadImage — cloud upload - ../../helper/saveBufferToFile — local file save - ../../shared/sendResponse — response - ../../helper/ApiError — error handling - http-status — status codes

Used By: - product.routes.ts — route binding

Issues: C16 (wrong status/message on update), H8 (no cloud rollback), H9 (DB before files), M8 (no file save error handling)


saas-boilerplate/src/app/modules/v1/Product/product.routes.ts (36 LOC)

Purpose: Express router defining Product endpoints with auth middleware and multer file upload configuration.

What Future Contributors Must Know: GET routes (/ and /:id) are PUBLIC — they appear before the router.use(auth(), contextMiddleware) middleware. POST/PATCH/DELETE require authentication. There's a redundant second router.use(auth(), contextMiddleware) on line 20. No permission middleware exists at the route level — permissions are checked in the service layer only.

Route Table:

Method Path Auth Middleware Handler
GET / No getProductList
GET /:id No getProductById
POST / Yes upload.array("images", 10), validateRequest createProduct
PATCH /:id Yes upload.array("images", 10), validateRequest updateProduct
DELETE /:id Yes deleteProduct

Dependencies: - express.Router - ../../middlewares/auth — JWT authentication - ../../middlewares/contextMiddleware — request context (AsyncLocalStorage) - ../../middlewares/validateRequest — Zod schema validation - multer — file upload parsing - ./product.controller - ./product.validation

Used By: - routes/v1/index.ts — mounted at /products

Issues: L5 (redundant middleware), L19 (DELETE missing validateRequest)


saas-boilerplate/src/app/modules/v1/Product/product.validation.ts (40 LOC)

Purpose: Zod validation schemas for Product create/update requests.

What Future Contributors Must Know: The schema has a CRITICAL dual field mismatch: it defines originalPrice as z.string() but the Product entity has discountPrice as decimal(10,2) — both the name AND type are wrong. Zod's z.object() strips unknown keys by default, so even sending the correct field name discountPrice from the frontend would be silently stripped by validateRequest before reaching the controller. The images field validates as z.string().url() which rejects local file paths like /uploads/products/... — local storage mode will always fail validation. The price field is marked optional but the entity requires it.

Exports: - ProductValidation.createProduct — Zod schema wrapping productSchema in body - ProductValidation.updateProduct — Zod schema with .partial() on productSchema

Schema Fields:

Field Zod Type Entity Column Match?
name string().min(1).max(100) varchar(100) Yes
images array(string().url()).optional() text[] Partial — rejects local paths
description string().optional() text nullable Yes
categoryId number().int() int FK nullable Yes
price number().nonnegative().optional() decimal(10,2) NOT NULL No — optional vs required
originalPrice string().regex(...) NO MATCH No — entity has discountPrice as decimal(10,2) (wrong name AND wrong type: string vs number)
currency string().length(3) varchar(3) default "USD" Yes

Issues: C1 (dual originalPrice name+type mismatch + Zod strip), C17 (.url() rejects local paths), H10 (price optional), M9 (categoryId allows <=0)


saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.service.ts (138 LOC)

Purpose: Business logic for ProductCategory CRUD — list all, paginated list, get by ID, create with uniqueness check, update with uniqueness check, delete (hard delete).

What Future Contributors Must Know: This file was copy-pasted from BlogCategory service. Line 2 imports BlogCategory from the wrong entity module solely for the wrong type cast to DeepPartial<BlogCategory> on line 104. Uses repo.remove() for deletion which hard-deletes despite the entity having @DeleteDateColumn. Category names are lowercased and trimmed before storage, but createCategory uses toLocaleLowerCase() (line 68) for the duplicate check while using toLowerCase() (line 72) for storage — a locale-sensitive inconsistency. All mutation operations check permissions (product-category.create/update/delete) but these permissions are NOT seeded in the database.

Exports: - getAllCategories() — returns all categories (no pagination, no permission check) - categoryList(query) — paginated list (no permission check) - getCategoryById(id) — single category (no permission check) - createCategory(payload) — creates with name uniqueness check; requires "product-category.create" - updateCategory(id, payload) — updates with uniqueness check; requires "product-category.update" - deleteCategory(id)HARD DELETE via repo.remove(); requires "product-category.delete"

Permission Strings Used: - "product-category.create" (line 65) - "product-category.update" (line 81) - "product-category.delete" (line 116)

Issues: C5 (wrong validation import in routes), C6 (BlogCategory import + type cast), C7 (hard delete), C8 (permissions not seeded), H3 (no read permissions), M11 (toLocaleLowerCase vs toLowerCase)


saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.controller.ts (97 LOC)

Purpose: Express request handlers for ProductCategory CRUD.

What Future Contributors Must Know: Every response message says "Blog" instead of "Product" (5 instances). This is a copy-paste artifact from BlogCategory that was never updated.

Exports: - getAllCategories, getCategoryList, getCategoryById, createProductCategory, updateProductCategory, deleteProductCategory

Issues: H2 (5 wrong "Blog" messages)


saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.routes.ts (35 LOC)

Purpose: Express router for ProductCategory endpoints.

What Future Contributors Must Know: ALL routes require authentication (including GET). The validation middleware imports BlogCategoryValidation instead of ProductCategoryValidation. The DELETE route has no validateRequest middleware.

Route Table:

Method Path Auth Middleware Handler
GET /all Yes getAllCategories
GET / Yes getCategoryList
GET /:id Yes getCategoryById
POST / Yes validateRequest(BlogCategoryValidation) createProductCategory
PATCH /:id Yes validateRequest(BlogCategoryValidation) updateProductCategory
DELETE /:id Yes deleteProductCategory

Issues: C5 (wrong validation import), H1 (no public read), M12 (no DELETE validation)


saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.validation.ts (36 LOC)

Purpose: Zod schemas for ProductCategory validation — but exported as BlogCategoryValidation.

What Future Contributors Must Know: The export name is BlogCategoryValidation, not ProductCategoryValidation. This is a copy-paste naming artifact only — the actual validated fields (name, description) are correct for ProductCategory. No Blog-specific fields exist in the schema.

Issues: H19 (wrong export name), M10 (copy-paste naming artifact)


saas-boilerplate/src/app/modules/v1/Image/image.service.ts (21 LOC)

Purpose: Single-file image upload via handleFileUpload helper.

What Future Contributors Must Know: Storage provider is hardcoded to "cloudinary" (line 11) — ignores whatever AppConfig specifies. Returns undefined if no file provided, and the controller doesn't handle this case.

Exports: - GetImgURL(file) — uploads single file to Cloudinary, returns URL

Issues: H5 (hardcoded cloudinary), M13 (undefined return)


saas-boilerplate/src/app/modules/v1/Image/image.controller.ts (22 LOC)

Purpose: Express handler for image upload endpoint.

Issues: L7 (unsafe type cast)


saas-boilerplate/src/app/modules/v1/Image/image.route.ts (14 LOC)

Purpose: Express router mounting POST /image with multer single-file upload.

What Future Contributors Must Know: This route has NO authentication middleware. Anyone can upload images without logging in. No rate limiting.

Route Table:

Method Path Auth Middleware Handler
POST /image No multer.single("image") GetImgURL

Registered at: /api/v1/uploads/image (via routes/v1/index.ts)

Issues: H4 (unauthenticated upload)


Frontend Files (10 files, ~1,961 LOC)


Sass-boilerplate-frontend-v1/src/api/products/index.js (0 LOC)

Purpose: Intended barrel export for Product API hooks.

What Future Contributors Must Know: This file is completely empty. There are zero React Query hooks for any product CRUD operation. All product data fetching in the frontend uses either hardcoded mock data or raw axios calls. This is the primary blocker for frontend-backend integration.

Issues: C15 (empty — no API hooks)


Sass-boilerplate-frontend-v1/src/api/products/product-categories.js (37 LOC)

Purpose: Custom hook useProductCategories() for fetching product categories via raw axios.

What Future Contributors Must Know: This is the ONLY real API integration in the entire Product Catalog frontend. However, it uses raw axios + useState/useEffect instead of React Query (inconsistent with the rest of the codebase which uses React Query v5). The refetch mechanism uses a boolean toggle state, not React Query's built-in refetch.

Exports: - useProductCategories() — returns { categories, loading, error, setRefetch }

API Call: GET ${NEXT_PUBLIC_API_URL}/product-categories?limit=1000

Issues: Uses raw axios instead of React Query (inconsistent with codebase pattern)


Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/page.js (8 LOC)

Purpose: Next.js route page — renders ProductsPage component.

Issues: L9 (no metadata export)


Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/categories/page.js (8 LOC)

Purpose: Next.js route page — renders ProductCategoriesPage component.

Issues: L10 (no metadata export)


Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/create/page.js (51 LOC)

Purpose: Create product page with ProductForm in create mode.

What Future Contributors Must Know: The entire onSubmit handler is mocked. The real API call (POST to /api/products) is commented out (lines 13-25). Instead, a 1000ms setTimeout simulates the delay, then shows a success toast and redirects. The form data is completely discarded.

Issues: C10 (API call commented out)


Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/[id]/page.js (173 LOC)

Purpose: Edit product page — fetches product by ID and renders ProductForm in edit mode.

What Future Contributors Must Know: Both the fetch (GET) and save (PATCH) API calls are commented out. The page always loads the same mock product ("Wireless Bluetooth Headphones", price $12.00, 2 placeholder images) regardless of the productId parameter. The mock data has a different image structure (objects with id, url, filename) than the backend entity (array of path strings).

Mock Product Shape:

{
  id: productId,
  name: "Wireless Bluetooth Headphones",
  categoryId: "1",        // String, backend expects number
  price: "12.00",         // String, backend expects number
  discountPrice: "10.00", // String
  images: [
    { id: "img1", url: "https://via.placeholder.com/...", filename: "headphones-1.jpg" }
  ]
}

Issues: C11 (mock fetch), C12 (mock save), M18 (image structure mismatch)


Sass-boilerplate-frontend-v1/src/components/pages/products/products-page.jsx (~891 LOC)

Purpose: Main products dashboard — table with search, filters, sorting, pagination, analytics cards, bulk operations, RBAC permission gates.

What Future Contributors Must Know: This is a fully-built UI with zero backend integration. The generateProductsData() function returns 15 hardcoded products. All CRUD operations (delete, bulk delete) only modify local React state. The analytics cards show calculated values from mock data with hardcoded change percentages ("+12%", "+18%"). Permission wrappers use product.view/create/update/delete strings that are not seeded in the backend.

State Management: 11 useState hooks — all local, no Redux/React Query/Context.

Permission Strings Referenced: - product.view (wraps entire page) - product.create (wraps "Add New Product" button) - product.update (wraps edit button per row) - product.delete (wraps delete button per row)

Mock Data: 15 products across 2 categories (Electronics, Cosmetics) with realistic names, descriptions, prices, and timestamps.

TODOs Found: - Line 391: // TODO: Replace with actual API call

Issues: C9 (all mock data), C13 (local-only delete), H11 (broken sort), H17 (hardcoded categories), H18 (fake analytics), M19 (NaN on non-numeric filter)


Sass-boilerplate-frontend-v1/src/components/pages/products/product-categories-page.jsx (37 LOC)

Purpose: Category management page using CustomizeManager component with actual API calls.

What Future Contributors Must Know: This is the ONLY product page with real backend integration. Uses useProductCategories hook for fetching and raw axios for mutations. The CustomizeManager component handles the UI for add/edit/delete. However, API URLs in the mutation functions are missing the NEXT_PUBLIC_API_URL prefix (the axios interceptor base URL covers this, but it's inconsistent with the hook).

Issues: L17 (URL style inconsistency — no functional impact), M14 (error state ignored)


Sass-boilerplate-frontend-v1/src/components/pages/products/product-form.jsx (~535 LOC)

Purpose: Reusable form for creating and editing products — React Hook Form with image upload, discount calculation, FormData serialization.

What Future Contributors Must Know: The form is fully built with validation, image handling, and FormData construction — but the onSubmit handler from parent pages is always mocked. Categories are hardcoded (8 options). The removedImageIds feature (for removing existing images on edit) is implemented in the frontend but the backend updateProduct controller doesn't process it. Image preview uses a non-existent /api/images/ endpoint.

Frontend vs Backend Validation Mismatches:

Field Frontend Backend Mismatch
name required, 2-100 chars required, 1-100 chars Min length differs
description required, 10-1000 chars optional, no limit Required vs optional
price required, 0.01-999999.99 optional, non-negative Required vs optional
categoryId string number (int) Type mismatch
discountPrice optional, < price originalPrice (WRONG NAME) Field name mismatch

TODOs Found: - Line 13: // TODO: Replace with actual API call when backend is ready - Line 30: "Mock categories data - replace with actual API call" - Line 83: // TODO: Replace with actual API call when backend is ready

Issues: C1 (dual field mismatch via parent), H12 (mock categories), H13 (removedImageIds unsupported), H15 (non-existent endpoint), M15 (type mismatch), M16 (image limit disabled), M17 (price type mismatch), M22 (required vs optional)


Sass-boilerplate-frontend-v1/src/components/pages/dashboard/product-performance.jsx (~217 LOC)

Purpose: Dashboard widget intended to show product performance metrics.

What Future Contributors Must Know: This component has NOTHING to do with the Product Catalog. It displays hardcoded project management data — "Minecraf App" (typo), "Web App Project", "Modernize Dashboard", "Dashboard Co" with team member names, budget, and progress bars. It should be renamed to ProjectPerformance or rewritten to display actual product sales metrics.

Issues: C14 (wrong data type), M20 (typo "Minecraf"), M21 (no responsive grid), L15 (non-functional date selector)


Contributor Checklist

Risks & Gotchas

  • The entire frontend Product CRUD is mocked — DO NOT assume any product page works in production
  • Permission strings are NOT seeded — adding them requires a seed migration
  • originalPrice in validation schema silently loses discount data — fix the field name to discountPrice first
  • Image validation rejects local paths — if using local storage, the .url() Zod check must be removed or made conditional
  • ProductCategory uses hard delete — data is permanently lost; no restore capability
  • BlogCategoryValidation is imported instead of ProductCategoryValidation — changing validation will change behavior

Pre-change Verification Steps

  1. Check if product permissions exist in DB: SELECT * FROM permission WHERE route LIKE '%product%'
  2. Verify image storage config: check IMAGE_STORAGE env var and AppConfig
  3. Test with non-super_admin user to confirm permission behavior
  4. Check if deletedAt column is populated for any products (indicates TypeORM soft-delete was used somewhere)

Suggested Tests Before PR

  • Create product with all fields including images → verify DB record and file storage
  • Create product with local storage → verify Zod validation passes for file paths
  • Update product with commented-out permission → verify access control
  • Delete product → verify only isDeleted flag changes
  • Create category → verify no Blog fields accepted
  • Delete category → verify CASCADE doesn't destroy products unintentionally

Architecture & Design Patterns

Code Organization

Standard module pattern per feature:

modules/v1/{Feature}/
├── {feature}.service.ts    — Business logic + DB operations
├── {feature}.controller.ts — HTTP request handling
├── {feature}.routes.ts     — Express router definition
└── {feature}.validation.ts — Zod schema definitions

Design Patterns

  • Repository Pattern: Via getDbRepository() shared utility (wraps TypeORM's DataSource)
  • Service Layer Pattern: Controllers delegate to services; services own business logic
  • Middleware Chain: auth() → contextMiddleware → upload → validateRequest → controller
  • Factory Pattern: Image upload uses handleFileUpload which dispatches to storage provider
  • Soft Delete: Dual mechanism (manual isDeleted boolean + TypeORM @DeleteDateColumn)

State Management Strategy

  • Backend: Stateless — all state in PostgreSQL via TypeORM
  • Frontend (Products): 100% local useState — no Redux, no React Query, no Context
  • Frontend (Categories): Raw axios + useState/useEffect custom hook — no React Query

Error Handling Philosophy

  • Backend: catchAsync wrapper on all controllers; ApiError class for typed errors; global error handler
  • Frontend: Minimal — mock data never fails; product-categories-page has no try-catch on mutations

Testing Strategy

  • Current: 0% coverage. Zero test files across all 23 files.
  • Backend: No unit tests, no integration tests
  • Frontend: No component tests, no E2E tests

Data Flow

Product Create Flow (If Connected)

[Frontend Form] → FormData (multipart)
    → [multer middleware] parses files
    → [validateRequest] Zod validation
        ← FAILS for local paths (.url() check)
        ← STRIPS unknown keys (discountPrice removed if sent; only originalPrice defined)
        ← req.body OVERWRITTEN with stripped result (validateRequest.ts:53-56)
    → [product.controller.ts]
        → uploadImage() (cloud) OR map to local paths
        → ProductService.createProduct(payload)
            → checkPermissionAndThrow("products.create") ← FAILS (not seeded)
            → categoryRepo.findOneBy({ id }) ← validates category exists
            → productRepo.create(payload) ← originalPrice ignored (wrong name + wrong type: string vs decimal)
            → productRepo.save(product) ← DB insert (discountPrice always NULL)
        → saveBufferToFile() (local) ← AFTER DB save (non-atomic)
    → sendResponse(201)

Data Entry Points

  • GET /api/v1/products — Public, no auth, paginated product list
  • GET /api/v1/products/:id — Public, no auth, single product + similar products
  • POST /api/v1/products — Auth required, multipart form with images
  • PATCH /api/v1/products/:id — Auth required, multipart form with images
  • DELETE /api/v1/products/:id — Auth required
  • GET /api/v1/product-categories/all — Auth required, all categories
  • GET /api/v1/product-categories — Auth required, paginated categories
  • POST /api/v1/product-categories — Auth required
  • PATCH /api/v1/product-categories/:id — Auth required
  • DELETE /api/v1/product-categories/:id — Auth required
  • POST /api/v1/uploads/imageNo auth, single image upload

Data Transformations

  • Image URLs: transformImageUrl() prepends base URL to relative paths
  • Category names: Lowercased and trimmed before storage
  • Pagination: getPaginationParams() extracts page/limit from query
  • Product filters: Price range parsed from strings; category name matched via join

Data Exit Points

  • JSON responses via sendResponse() wrapper
  • Image files stored to Cloudinary, S3, or local uploads/products/ directory

Integration Points

APIs Consumed (Frontend → Backend)

Endpoint Method Auth Frontend Status
/products GET No NOT CALLED (mock data)
/products POST Yes NOT CALLED (mock delay)
/products/:id GET No NOT CALLED (mock data)
/products/:id PATCH Yes NOT CALLED (mock delay)
/products/:id DELETE Yes NOT CALLED (local state)
/product-categories GET Yes CALLED via useProductCategories
/product-categories POST Yes CALLED via axios
/product-categories/:id PATCH Yes CALLED via axios
/product-categories/:id DELETE Yes CALLED via axios
/uploads/image POST No NOT CALLED

Shared Dependencies (Backend)

  • getDbRepository — DB access (all service files)
  • checkPermissionAndThrow — RBAC (product.service, product_category.service)
  • catchAsync — error wrapping (all controllers)
  • sendResponse — response formatting (all controllers)
  • validateRequest — Zod middleware (product.routes, product_category.routes)
  • handleFileUpload — file upload dispatch (image.service)
  • saveBufferToFile — local file storage (product.controller)
  • uploadImage — cloud upload (product.controller)
  • transformImageUrl — URL transformation (product.service)

Database Access

Entity Table Operations Indexes
Product products CRUD + filter + pagination name, categoryId, price
ProductCategory product_category CRUD + uniqueness check name

Events

  • None — Product Catalog has no event system integration

Cron Jobs

  • None — no scheduled tasks for products

Dependency Graph

Entry Points (routes):
  product.routes.ts ──→ product.controller.ts ──→ product.service.ts ──→ Product.ts
                                                                      └─→ ProductCategory.ts
  product_category.routes.ts ──→ product_category.controller.ts ──→ product_category.service.ts ──→ ProductCategory.ts
  image.route.ts ──→ image.controller.ts ──→ image.service.ts

Shared Dependencies:
  product.service.ts ──→ getDbRepository, checkPermissionAndThrow, getPaginationParams, transformImageUrl
  product.controller.ts ──→ uploadImage, saveBufferToFile, sendResponse, ApiError
  product_category.service.ts ──→ getDbRepository, checkPermissionAndThrow, ApiError
  product_category.controller.ts ──→ sendResponse
  image.service.ts ──→ handleFileUpload

Frontend:
  products/page.js ──→ products-page.jsx ──→ (NO API — mock data)
  products/create/page.js ──→ product-form.jsx ──→ (NO API — mock delay)
  products/[id]/page.js ──→ product-form.jsx ──→ (NO API — mock delay)
  products/categories/page.js ──→ product-categories-page.jsx ──→ useProductCategories() ──→ axios ──→ /product-categories

Entry Points (Not Imported by Others in Scope)

  • product.routes.ts — route definition
  • product_category.routes.ts — route definition
  • image.route.ts — route definition
  • products/page.js — Next.js page
  • products/create/page.js — Next.js page
  • products/[id]/page.js — Next.js page
  • products/categories/page.js — Next.js page
  • product-performance.jsx — dashboard widget

Leaf Nodes (Don't Import Others in Scope)

  • Product.ts — entity (imported by services)
  • ProductCategory.ts — entity (imported by services)
  • product.validation.ts — Zod schemas (imported by routes)
  • product_category.validation.ts — Zod schemas (imported by routes)

Circular Dependencies

No circular dependencies detected within scope.


Testing Analysis

Test Coverage Summary

  • Statements: 0%
  • Branches: 0%
  • Functions: 0%
  • Lines: 0%

Test Files

None. Zero test files exist for any Product Catalog code.

Testing Gaps

  • No unit tests for any service function
  • No integration tests for API endpoints
  • No component tests for React components
  • No E2E tests for user flows
  • Permission check behavior untested
  • Image upload flow untested
  • Soft-delete behavior untested
  • Category cascade delete untested

Similar Features Elsewhere

  • BlogCategory (modules/v1/BlogCategory/) — Direct template source for ProductCategory. Has bulk delete, restore, permanent delete, trash functionality that ProductCategory is missing.
  • Blog (modules/v1/Blog/) — Similar CRUD pattern with ownership-based permissions, image handling, and rich text. Blog has checkPermissionAndThrow calls (though commented out) and slug generation.
  • Package (modules/v1/package/) — Similar catalog pattern with categories, pricing. Uses "package.create" (singular) permission format.

Reusable Utilities Available

  • React Query hooks pattern (api/blogs/index.js) — Use as template for creating api/products/index.js hooks with useQuery and useMutation
  • CustomizeManager (components/pages/dashboard/customization/customize-manager.jsx) — Already used for categories; could potentially be used for simpler product management
  • DataTable (components/custom/data-table.jsx or advance-table.jsx) — Existing table components with sorting/pagination

Patterns to Follow

  • Blog API hooks: Reference api/blogs/index.js for React Query hook implementation pattern
  • Blog CRUD controller: Reference Blog/blog.controller.ts for correct image handling with rollback
  • Permission seeding: Reference seed/data/permission.ts blog section for seeding product permissions

User Flow Analysis

Flow 1: View Products List — BROKEN

User navigates to /dashboard/productsProductsPage renders with 15 hardcoded mock products → API is never called → Delete/edit only affect local state

Flow 2: Create Product — BROKEN

User clicks "Add New Product" → navigates to /dashboard/products/createProductForm renders with mock categories → Form submission discarded (mock delay + toast) → Redirects to list → No product created

Flow 3: Edit Product — BROKEN

User clicks edit on product row → navigates to /dashboard/products/{id} → Mock "Wireless Bluetooth Headphones" loaded (ignores actual ID) → Form submission discarded → No product updated

Flow 4: Delete Product — BROKEN

User clicks delete → Product removed from local React state only → Page refresh restores all products → Backend never contacted

Flow 5: Manage Categories — PARTIALLY WORKING

User navigates to /dashboard/products/categoriesuseProductCategories() fetches real data from backend → CustomizeManager renders categories → Add/edit/delete make real API calls → BUT: All routes require auth, permissions not seeded (only super_admin works), response messages say "Blog"


Permission Matrix

Action Frontend Guard Backend Auth Backend Permission Seeded? Status
View Products product.view None (public) None N/A Mismatch — frontend gates, backend public
Create Product product.create Yes products.create No Broken — perm check fails
Update Product product.update Yes product.update (commented) No Broken — no perm check
Delete Product product.delete Yes None N/A Broken — no perm check
View Categories None Yes None N/A Works (any authenticated)
Create Category None Yes product-category.create No Broken — perm check fails
Update Category None Yes product-category.update No Broken — perm check fails
Delete Category None Yes product-category.delete No Broken — perm check fails

Note: super_admin role bypasses all permission checks, so categories work for super_admin only.


Implementation Notes

Code Quality Observations

  • ProductCategory module is a low-effort copy-paste from BlogCategory with 5+ unresolved copy bugs
  • Frontend Product pages are well-designed UI with zero backend wiring — appears to be a design-first approach
  • Backend Product module is more complete than frontend suggests — endpoints work, validation exists, image handling is implemented
  • Image upload module is the simplest in the codebase (21 LOC service) but has a security gap (no auth)
  • product-performance.jsx is a template remnant that was never adapted for actual product metrics

TODOs and Future Work

File Line TODO
product-form.jsx 13 Replace with actual API call when backend is ready
product-form.jsx 30 Mock categories data - replace with actual API call
product-form.jsx 83 Replace with actual API call when backend is ready
products-page.jsx 391 Replace with actual API call
create/page.js 13 Replace with actual API call when backend is ready
[id]/page.js 25 Replace with actual API call when backend is ready
[id]/page.js 83 Replace with actual API call when backend is ready

Dead Code

File Lines Description LOC
api/products/index.js All Completely empty file 0
products-page.jsx 19-308 generateProductsData() mock generator ~289
products-page.jsx 391-407 Commented-out API call ~16
create/page.js 13-25 Commented-out API call ~12
[id]/page.js 25-35 Commented-out fetch call ~10
[id]/page.js 84-95 Commented-out save call ~11
product-form.jsx 162-180 Commented-out API call ~18
product-form.jsx 218-222 Commented-out image limit check ~4
product-performance.jsx All Template remnant — no relation to products ~217
Total Dead Code ~577 LOC

Technical Debt

  • 7 commented-out API call blocks that need to be wired up
  • BlogCategoryValidation copy-paste must be replaced with proper ProductCategoryValidation
  • Dual soft-delete mechanism needs consolidation (pick isDeleted OR @DeleteDateColumn, not both)
  • Frontend needs migration from raw axios to React Query for consistency
  • Permission strings need standardization (singular vs plural: products.create vs product.update)
  • Image structure mismatch between frontend (objects with id/url) and backend (string array)
  • product-performance.jsx needs complete rewrite or removal

Modification Guidance

To Wire Up Frontend API Integration

  1. Create React Query hooks in api/products/index.js following api/blogs/index.js pattern
  2. Replace generateProductsData() with useQuery call to GET /products
  3. Replace mock handleSubmit in create/edit pages with useMutation calls
  4. Replace local-state delete with useMutation call to DELETE /products/:id
  5. Replace mock categories in ProductForm with useProductCategories() hook
  6. Fix categoryId type: send as number, not string
  7. Fix field name: discountPrice not originalPrice

To Fix Backend Permission Issues

  1. Add product permissions to seed/data/permission.ts:
    { route: "product.view", description: "View products" },
    { route: "product.create", description: "Create products" },
    { route: "product.update", description: "Update products" },
    { route: "product.delete", description: "Delete products" },
    { route: "product-category.view", description: "View product categories" },
    { route: "product-category.create", description: "Create product categories" },
    { route: "product-category.update", description: "Update product categories" },
    { route: "product-category.delete", description: "Delete product categories" },
    
  2. Standardize permission strings (pick singular product.* not plural products.*)
  3. Uncomment updateProduct permission check in service
  4. Add deleteProductById permission check in service
  5. Run seed to populate permissions

To Fix Copy-Paste Bugs in ProductCategory

  1. Rename BlogCategoryValidation export to ProductCategoryValidation
  2. Update import in product_category.routes.ts
  3. Fix type cast in product_category.service.ts:104 from BlogCategory to ProductCategory
  4. Update all 5 controller messages from "Blog" to "Product"
  5. Replace repo.remove() with repo.softRemove() in delete function
  6. Remove Blog-specific fields from validation schema if present

Testing Checklist for Changes

  • Product create with all fields + images succeeds (DB record + files saved)
  • Product create with local storage passes Zod validation
  • Product update with active permission check enforces RBAC
  • Product delete with permission check enforces RBAC
  • Product list excludes soft-deleted products (both isDeleted and deletedAt)
  • Category create validates as ProductCategory (not BlogCategory)
  • Category delete uses soft delete (not hard delete)
  • Image upload requires authentication
  • discountPrice field properly maps from frontend to backend
  • Frontend permission wrappers match backend permission strings
  • Category cascade delete doesn't unexpectedly destroy products
  • Frontend delete calls backend API and refreshes list
  • Price filter handles edge cases (NaN, min > max)

Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-04-11 Analysis Mode: Exhaustive Files Read: 23 (13 backend + 10 frontend) Total Issues: 76 (18 Critical, 17 High, 22 Medium, 19 Low) Dead Code: ~577 LOC across 10 locations