Initial commit: Meshkee CMS API

NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
Ali Reza
2026-07-21 17:52:36 +03:30
commit bb59d5e9ba
254 changed files with 37031 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
# Deploy Meshkee CMS API (Debian VM)
Stack: Docker (Postgres + Redis) → Node build on server → PM2 → Nginx + Let's Encrypt.
App path on server: `/opt/meshkee/app`
API domain: `api.meshkee.com``https://api.meshkee.com/api/v1`
> **Note:** Until the Git remote is accessible from the VM (deploy key / credentials), updates can be synced with `rsync` from your laptop. Pin `sharp@0.33.5` — this VM CPU lacks x64-v2 required by sharp 0.35+.
## Prerequisites
- Debian VM with SSH access
- Domain `A` record pointing at the VM (for HTTPS)
- Git remote with this codebase (private repo → deploy key)
- Production secrets (JWT, Postgres password, S3 keys)
## 1. Server packages
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg git nginx ufw
# Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
# log out/in (or newgrp docker) so docker works without sudo
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
# Certbot (after Nginx is installed)
sudo apt install -y certbot python3-certbot-nginx
```
Firewall:
```bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
```
## 2. Clone the app
```bash
sudo mkdir -p /opt/meshkee
sudo chown "$USER:$USER" /opt/meshkee
cd /opt/meshkee
git clone <YOUR_GIT_REMOTE_URL> app
cd app
```
Private repo: create an SSH deploy key on the VM (`ssh-keygen -t ed25519 -C "meshkee-deploy"`), add the public key as a read-only deploy key on GitHub/GitLab, clone via SSH URL.
## 3. Production env
```bash
cp .env.example .env
nano .env # set strong secrets — never commit this file
```
Required production values:
- Strong `POSTGRES_PASSWORD` and matching `DATABASE_URL`
- Long random `JWT_ACCESS_SECRET` / `JWT_REFRESH_SECRET`
- Real `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY`
- `PORT=3000`
- `SMS_ENABLED` as needed
## 4. Database + Redis
```bash
cd /opt/meshkee/app
docker compose up -d
docker compose ps
```
First Postgres volume init runs SQL under `database/migrations/` automatically.
Later schema updates:
```bash
./database/migrate.sh
```
## 5. Build and run (on the server)
```bash
cd /opt/meshkee/app
npm ci
npm run prisma:generate
npm run build
```
Production seed (super admin only — skip sample data):
```bash
./database/seed.sh database/seeds/002_super_admin_user.sql
# optional reference data:
# ./database/seed.sh database/seeds/004_iran_cities.sql
# ./database/seed.sh database/seeds/005_business_categories.sql
```
Start with PM2:
```bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup # run the command it prints (usually with sudo)
```
Health check locally on the VM:
```bash
curl -s http://127.0.0.1:3000/api/v1/ | head
# or hit a known public route such as tenant resolve
```
## 6. Nginx + HTTPS
Create `/etc/nginx/sites-available/meshkee-api`:
```nginx
server {
listen 80;
server_name api.example.com; # replace with your domain
client_max_body_size 15M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Enable and get a certificate:
```bash
sudo ln -sf /etc/nginx/sites-available/meshkee-api /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.example.com
```
API base URL: `https://api.example.com/api/v1`
## Ongoing updates
```bash
cd /opt/meshkee/app
git pull
./database/migrate.sh # if there are new SQL migrations
npm ci
npm run prisma:generate
npm run build
pm2 restart meshkee-api
```
## Useful commands
```bash
pm2 status
pm2 logs meshkee-api
docker compose logs -f postgres
```
+601
View File
@@ -0,0 +1,601 @@
# Meshkee CMS API — Project Context
> Living reference for developers and AI assistants working on this codebase.
> Last updated: July 2026
## What This Project Is
**Meshkee CMS API** (`meshkee-cms-api`) is a multi-tenant backend for Meshkee business websites. Each business gets its own domain, content (products, categories, media), team members, and customer registrations. Platform super admins manage businesses and domains; business owners and staff manage per-business content through a permission-based dashboard.
**API base URL:** `/api/v1`
---
## Tech Stack
| Layer | Technology |
|-------|------------|
| Runtime | Node.js, TypeScript (ES2021, strict) |
| Framework | NestJS 11 |
| ORM | Prisma 6 (`prisma db pull` — schema is introspected, not migrated via Prisma) |
| Database | PostgreSQL 16 |
| Cache | Redis 7 (OTP storage) |
| Auth | JWT (access + refresh), Passport, bcrypt |
| Validation | class-validator + class-transformer |
| File storage | S3-compatible (Parmin), Sharp for image processing |
---
## Architecture
### Multi-tenancy model
```
Platform (super admin)
└── Business (tenant root)
├── Domains (host → business resolution)
├── Team members (BusinessUser + Role)
├── Customers (BusinessCustomer)
├── Categories (per entityType: product | blog | portfolio)
├── Products
│ └── Store items (product variants — price & stock per variation combo)
├── Media library
└── Settings (JSON)
```
- **Tenant resolution:** `GET /tenants/:host` resolves a domain to a business (public, no auth).
- **Business-scoped APIs:** Most CMS routes use `businesses/:businessId/...` with JWT + business permission checks.
- **Platform APIs:** User/business/domain management requires `super_admin` (checked in services).
### Two "category" concepts
| Concept | Table | Purpose |
|---------|-------|---------|
| **Content Category** | `categories` | Per-business taxonomy for products, blogs, portfolios |
| **Business Category** | `business_categories` | Platform-wide taxonomy classifying businesses (Retail, Creative, etc.) |
Do not confuse them when reading or writing code.
---
## Directory Structure
```
src/
├── main.ts # Bootstrap, global prefix, pipes, interceptors
├── app.module.ts # Root module wiring
├── auth/ # JWT, OTP/SMS, permissions, guards, decorators
├── users/ # Super-admin user management
├── roles/ # Global + team role listing
├── business-admin/ # Super-admin business CRUD + domains
├── business-team/ # Per-business team invite/manage
├── tenant/ # Domain → business resolution (public)
├── domain-admin/ # Super-admin domain management
├── categories/ # Content categories, variations, technical forms
├── products/ # Products, variants, technical info
├── cart/ # Customer shopping cart
├── orders/ # Customer checkout + admin order management
├── media/ # Upload/list/update/delete media
├── storage/ # S3 driver abstraction
├── prisma/ # PrismaModule + PrismaService
├── redis/ # Redis client + OTP helpers
└── common/ # Shared interceptors (BigInt serialization)
prisma/schema.prisma # Introspected from PostgreSQL (source of truth after migrations)
database/
├── migrations/ # Raw SQL migrations (numbered)
├── seeds/ # Dev sample data + production super-admin
├── migrate.sh, seed.sh, setup.sh
postman/ # API collection for manual testing
```
### NestJS module pattern
Each feature follows: `*.module.ts``*.controller.ts``*.service.ts``dto/`
---
## Getting Started
```bash
cp .env.example .env
docker compose up -d # Postgres (auto-runs migrations on first init) + Redis
./database/seed.sh # Sample dev data
npm install
npm run prisma:generate
npm run start:dev # http://localhost:3000/api/v1
```
**Dev credentials** (after `001_sample_data.sql` seed): any seeded user, password `password`.
Example super admin: `+989121111111` / `password`
---
## Database & Migrations
### Workflow
1. Write a new SQL file in `database/migrations/` (e.g. `012_feature.sql`).
2. Apply via `./database/migrate.sh` or `docker exec` into Postgres.
3. Run `npm run prisma:pull` to sync `prisma/schema.prisma`.
4. Run `npm run prisma:generate` to regenerate the client.
**Important:** This project does **not** use Prisma Migrate. SQL migrations are the authoritative schema history. Prisma schema is maintained separately via introspection.
### Migration files
| File | Purpose |
|------|---------|
| `001_initial_schema.sql` | Users, businesses, domains |
| `002_phone_permissions_content_media.sql` | RBAC, media, products, blogs, portfolios |
| `003_categories.sql` | Content categories + assignments |
| `004_user_types_and_business_members.sql` | Super admin, business owner, customer roles |
| `005_business_team_roles.sql` | Team roles (admin, editor, viewer) |
| `006_business_categories.sql` | Platform business category taxonomy |
| `006_user_profile.sql` | User profile JSON |
| `007_business_i18n_fields.sql` | Business Persian name |
| `007_domain_expiry_active.sql` | Domain expiry/active flags |
| `008_remove_name_en.sql` | Schema cleanup |
| `009_categories_name_fa.sql` | Category `name_fa` |
| `010_category_variations.sql` | Category variations, product variants (store items) |
| `011_category_technical_forms.sql` | Technical forms + product values |
| `012_comments.sql` | Product comments |
| `013_expert_reviews.sql` | Expert reviews |
| `014_addresses.sql` | User/business street addresses |
| `015_business_profile.sql` | Business profile fields |
| `015_cities.sql` | Location cities (country → province → city) |
| `016_product_variation_values.sql` | Product-level variation option selections (input for store items) |
| `017_product_variant_festival.sql` | Festival flag on store items |
| `018_product_variant_reward_points.sql` | Reward points on store items |
| `019_cart_and_orders.sql` | Shopping cart, orders, order items + order permissions |
| `020_store_items_and_variants.sql` | `store_items` + `store_item_variants` (replaces `product_variants`) |
| `030_website_homepage.sql` | Website category/brand groups, sliders, brand `sort_order` |
Docker mounts `./database/migrations` into Postgres init — migrations run automatically only on **first** volume creation. Use `migrate.sh` for subsequent migrations.
---
## Domain Model
### Enums
| Enum | Values |
|------|--------|
| `MediaEntityType` | `product`, `blog`, `portfolio` |
| `ContentStatus` | `draft`, `published`, `archived` |
| `VariationType` | `color`, `size`, `custom` |
| `OrderStatus` | `pending`, `confirmed`, `processing`, `shipped`, `delivered`, `cancelled` |
| `OrderSource` | `website`, `admin` |
| `TechnicalFieldType` | `text`, `textarea`, `select`, `multi_select` |
| `MediaType` | `image`, `video` |
### Core relationships
```
Business 1──* Domain
Business 1──* Category (entityType: product|blog|portfolio)
Business 1──* Product
Business 1──* Media
Category 1──* CategoryVariation 1──* CategoryVariationOption
Category 1──0..1 CategoryTechnicalForm 1──* CategoryTechnicalFormField
CategoryTechnicalFormField 1──* CategoryTechnicalFormFieldOption
Product *──0..1 Category (via CategoryAssignment)
Product 1──* ProductVariationValue → CategoryVariationOption (which options this product offers)
Product 1──* ProductVariationValue → CategoryVariationOption (which options this product offers)
Product 1──0..1 StoreItem (one shop listing per product)
StoreItem 1──* StoreItemVariant (purchasable SKUs: price, stock, variation combo)
StoreItemVariant 1──* StoreItemVariantSelection → CategoryVariationOption
Product 1──* ProductTechnicalFieldValue → CategoryTechnicalFormField
Business 1──* Cart (per customer) 1──* CartItem → ProductVariant
Business 1──* Order 1──* OrderItem → ProductVariant (snapshot on order)
User 1──* Cart, Order (as customer)
Media 1──* MediaAttachment (polymorphic: entityType + entityId)
```
### Schema gap: blogs & portfolios
SQL migrations create `blogs` and `portfolios` tables and seed data populates them. Permissions exist (`blogs.*`, `portfolios.*`). However:
- No Prisma models for Blog/Portfolio
- No NestJS modules or API endpoints
Categories and media attachments already support `blog` and `portfolio` entity types — infrastructure is ready, API is not.
---
## API Reference
All routes are prefixed with `/api/v1`.
### Public
| Method | Path | Description |
|--------|------|-------------|
| POST | `/auth/register` | Customer registration by domain |
| POST | `/auth/login` | Cell + password |
| POST | `/auth/refresh` | Refresh token |
| POST | `/auth/send-otp` | Send OTP (Redis-backed) |
| POST | `/auth/verify-otp` | Verify OTP |
| GET | `/tenants/:host` | Resolve business from domain |
| GET | `/tenants/:host/store-specials` | Active store specials |
| GET | `/tenants/:host/website/category-groups` | Homepage category rows |
| GET | `/tenants/:host/website/brand-groups` | Homepage brand rows |
| GET | `/tenants/:host/website/sliders` | Homepage sliders with slides |
### Authenticated (JWT)
| Method | Path | Access |
|--------|------|--------|
| GET | `/auth/me` | Any user |
| PATCH | `/auth/profile` | Any user |
| POST | `/auth/change-password` | Any user |
| GET | `/roles?scope=global\|team` | Super admin / team.read |
| GET | `/business-categories` | Super admin or `business_categories.read` |
### Super admin only (service-level check)
| Module | Base path |
|--------|-----------|
| Users | `/users` |
| Businesses | `/businesses` |
| Domains | `/domains` |
### Business-scoped (JWT + `BusinessPermissionGuard`)
Pattern: `/businesses/:businessId/<resource>`
| Module | Base path | Key permissions |
|--------|-----------|-----------------|
| Team | `/team` | `business.team.*` |
| Media | `/media` | `media.*` |
| Categories | `/categories` | `categories.*` |
| Products | `/products` | `products.*` |
| Website | `/website/category-groups`, `/website/brand-groups`, `/website/sliders` | `website.*` |
#### Categories — notable sub-routes
| Method | Path | Description |
|--------|------|-------------|
| GET | `/categories/color-presets` | Predefined color palette |
| GET/PUT | `/categories/:id/variations` | Category variation options |
| GET/PUT | `/categories/:id/technical-form` | Technical form definition |
#### Products — notable sub-routes
| Method | Path | Description |
|--------|------|-------------|
| GET/PUT | `/products/:id/variations` | Which category variation options apply to this product |
| GET/POST/PATCH/DELETE | `/products/:id/variants` | Removed — use `/store-items` |
| GET/PUT | `/products/:id/technical-info` | Product technical data |
#### Cart (customer — JWT, must be business customer)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/cart` | Get current user's cart |
| POST | `/cart/items` | Add store item variant to cart (`storeItemVariantId`, `quantity`) |
| PATCH | `/cart/items/:itemId` | Update cart item quantity |
| DELETE | `/cart/items/:itemId` | Remove cart item |
| DELETE | `/cart` | Clear cart |
| POST | `/cart/checkout` | Place order from cart (requires `addressId` or `shippingAddress`) |
#### Orders
| Method | Path | Access | Description |
|--------|------|--------|-------------|
| GET | `/orders` | Customer (own) or `orders.read` (all) | List orders |
| GET | `/orders/:orderId` | Customer (own) or `orders.read` | Order detail |
| POST | `/orders` | `orders.create` | Admin: create order for a customer |
| PATCH | `/orders/:orderId` | `orders.update` | Admin: update status / admin notes |
---
## Auth & RBAC
### Global user roles
| Role slug | Dashboard | Notes |
|-----------|-----------|-------|
| `super_admin` | `super_admin` | Full platform access |
| `business_owner` | `business` | Assigned to business owners |
| `business_staff` | `business` | Legacy; team uses per-business roles |
| `customer` | `customer` | Website registrants |
### Per-business team roles
Assigned via `business_users.role_id`:
| Role | Slug | Typical access |
|------|------|----------------|
| Owner | `isOwner=true` | All `business_owner` permissions |
| Admin | `admin` | Full content + team read |
| Editor | `editor` | Create/edit/publish content |
| Viewer | `viewer` | Read-only |
### Permission groups (seeded)
`business.*`, `domains.*`, `products.*`, `blogs.*`, `portfolios.*`, `media.*`, `categories.*`, `users.*`, `roles.manage`, `business.team.*`, `business_categories.*`
Each resource typically has: `read`, `create`, `update`, `delete` (+ `publish` for content).
### Guards & decorators
```typescript
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
@RequireBusinessPermission('products.read')
```
- `JwtAuthGuard` — validates Bearer JWT (`type: 'access'`)
- `BusinessPermissionGuard` — checks permission for `businessId` route param
- `PermissionsService``isSuperAdmin()`, `hasBusinessPermission()`
- Super admins bypass business permission checks
- `@CurrentUser()` injects `AuthUser` into handlers
### Auth flow notes
- Registration resolves tenant by `domain` → creates/links user → assigns `customer` role
- OTP stored in Redis (`otp:{cellNumber}`), 5-min TTL; disabled when `SMS_ENABLED=false`
- JWT payload: `sub`, `cellNumber`, `roles`, `dashboard`, `type`
---
## Key Features
### Products
- CRUD with slug, status, featured media, gallery attachments
- One category per product (via `CategoryAssignment`)
- Content JSON: `{ nameFa, html }`; metadata JSON: `{ tags }`
- i18n: Persian name in `content.nameFa`, summary in `description`
### Category variations (product categories)
- Types: `color` (preset palette), `size`, `custom`
- One color + one size per category (DB partial unique indexes)
- `PUT /categories/:id/variations` replaces all variations (delete-all + recreate)
- Color presets: `GET /categories/color-presets` (15 named colors with hex)
### Store (shop listings)
The **store** turns CMS products into purchasable items using a two-level model:
```
Product variations → which options this product offers (product_variation_values)
Store item → one shop listing per product (store_items)
Store item variants → purchasable SKUs with price/stock per combination (store_item_variants)
```
| Layer | Table | API | Purpose |
|-------|-------|-----|---------|
| Product variations | `product_variation_values` | `PUT /products/:id/variations` | Which category variation options apply to this product |
| Store item | `store_items` | `GET /store-items/by-product/:productId` | One listing per product in the shop |
| Store item variants | `store_item_variants` + `store_item_variant_selections` | `POST /store-items`, `PUT /store-items/sync` | Purchasable combinations with price & stock |
**Store item variant fields** (`store_item_variants`):
| Field | DB column | Notes |
|-------|-----------|-------|
| `sku` | `sku` | Optional merchant SKU |
| `price` | `price` | Selling price (`NUMERIC(12,2)`) |
| `compareAtPrice` | `compare_at_price` | Optional strike-through / was-price |
| `stockQuantity` | `stock_quantity` | Integer ≥ 0; `null` = untracked |
| `isActive` | `is_active` | Whether variant is available for purchase |
| `selections` | `store_item_variant_selections` | Exactly one `CategoryVariationOption` per category variation |
**Rules:**
- One `store_items` row per product per business.
- Create variants under the store item after setting product variation values.
- Each variant picks one option per variation; the combination must be unique per store item.
- Cart and orders reference `storeItemVariantId` (not the product directly).
**Example — batch create variants for a product:**
```json
POST /businesses/:businessId/store-items
{
"productId": "1",
"items": [
{
"selections": [
{ "variationId": "1", "optionId": "1" },
{ "variationId": "2", "optionId": "5" }
],
"price": 99.99,
"stockQuantity": 10
}
]
}
```
**Example — update a variant:**
```json
PATCH /businesses/:businessId/store-items/variants/:variantId
{
"price": 89.99,
"stockQuantity": 25
}
```
### Technical forms (product categories)
- One form per product category
- Field types: `text`, `textarea`, `select`, `multi_select`
- `PUT /categories/:id/technical-form` — define/replace form fields
- `PUT /products/:id/technical-info` — fill product values (validated against category form)
- Product must have a category assignment
**Example — define form:**
```json
PUT /businesses/:businessId/categories/:categoryId/technical-form
{
"fields": [
{ "label": "Weight", "type": "text", "isRequired": true },
{ "label": "Material", "type": "select", "options": ["Cotton", "Polyester"] },
{ "label": "Features", "type": "multi_select", "options": ["Waterproof", "Breathable"] }
]
}
```
**Example — fill product data:**
```json
PUT /businesses/:businessId/products/:productId/technical-info
{
"values": [
{ "fieldKey": "weight", "value": "500g" },
{ "fieldKey": "material", "value": "cotton" },
{ "fieldKey": "features", "value": ["waterproof", "breathable"] }
]
}
```
Field keys are auto-slugified from labels (e.g. `"Weight"``"weight"`).
### Media
- Multipart upload to S3 via Sharp processing
- Polymorphic attachments to products (and future blog/portfolio entities)
- Featured image on products via `featuredMediaId`
---
## Coding Conventions
### DTOs
- `class-validator` decorators on all request bodies and query params
- `@Type(() => Number)` for query param coercion
- Slug format: `^[a-z0-9]+(?:-[a-z0-9]+)*$`
- Cell numbers: E.164 `^\+[1-9]\d{6,14}$`
### Services
- Inject `PrismaService`, `PermissionsService`
- Convert route param IDs with `BigInt(idRaw)`
- Private `assertPermission()` / `assertSuperAdmin()` helpers
- Use `$transaction` for multi-step writes
- **Replace semantics** for nested resources (variations, technical forms) — delete-all then recreate
### Error handling
Use Nest exceptions: `NotFoundException`, `ForbiddenException`, `BadRequestException`, `ConflictException`, `UnauthorizedException`.
### Serialization
- Global `BigIntSerializerInterceptor` converts BigInt → Number in JSON responses
- Services use private `serialize()` methods for consistent output shapes
- IDs returned as strings in API responses
### Global validation pipe (`main.ts`)
```typescript
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
```
---
## Environment Variables
See `.env.example` for the full list. Key groups:
| Group | Variables |
|-------|-----------|
| Database | `DATABASE_URL`, `POSTGRES_*` |
| Redis | `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` |
| API | `PORT` |
| JWT | `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `JWT_*_EXPIRES_IN` |
| SMS | `SMS_ENABLED` |
| S3 | `S3_ENDPOINT`, `S3_BUCKET`, `S3_PUBLIC_URL`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` |
| Media | `MEDIA_MAX_FILE_SIZE_MB` |
---
## Implemented vs Planned
### Implemented
- Multi-tenant auth (register, login, OTP, profile)
- Super admin: users, businesses, domains, system business categories
- Business team management
- Media upload (S3 + Sharp)
- Categories (all entity types in DB; API supports `entityType` filter)
- Products CRUD
- Product variation values (which options a product offers)
- Store items / product variants (price, stock, SKU)
- Shopping cart + checkout + orders (customer + admin)
- Product technical info
- Category variations & technical forms
- Tenant resolution by domain
- RBAC with granular permissions
### Planned / partial
| Feature | DB | Permissions | API | Prisma |
|---------|----|-------------|-----|--------|
| Blogs | Yes | Yes | No | No model |
| Portfolios | Yes | Yes | No | No model |
| Customer dashboard | Partial | No | Register only | Yes |
| SMS provider | — | — | Stub | — |
| Store checkout (cart, orders) | Yes | Yes | Yes | Yes |
| Customer favorites | — | `favorites.*` seeded | No | No |
---
## Testing
**Postman collection:** `postman/Meshkee-CMS-Auth.postman_collection.json`
Variables: `baseUrl`, `accessToken`, `refreshToken`, `domain`, `businessId`, `categoryId`, `productId`, `variantId`, `variationId`, `optionId`, `cartItemId`, `orderId`
**Store workflow in Postman:** `Business Categories` → set variations → `Business Products` → create product → set product variations → create store item (variant) with price & stock → `Website - Cart` (login as customer) → add to cart → checkout → `Business Orders` (login as owner) to manage.
---
## Common Tasks for Contributors
### Add a new API feature
1. Write SQL migration in `database/migrations/`
2. Apply migration, then `npm run prisma:pull && npm run prisma:generate`
3. Create module: `src/<feature>/` with controller, service, DTOs
4. Register in `app.module.ts`
5. Add permissions to migration if business-scoped
6. Update this document
### Add a business-scoped endpoint
1. Controller: `@Controller('businesses/:businessId/...')`
2. Guards: `@UseGuards(JwtAuthGuard, BusinessPermissionGuard)`
3. Permission: `@RequireBusinessPermission('resource.action')`
4. Service: `assertPermission(businessId, actor.id, 'resource.action')` (defense in depth)
### Add a nested replace resource (like variations)
Follow the pattern in `CategoryVariationsService` / `CategoryTechnicalFormService`:
- `GET` returns current state
- `PUT` validates input, deletes all existing, recreates in a transaction
---
## Related Files
| Purpose | Path |
|---------|------|
| Prisma schema | `prisma/schema.prisma` |
| Env template | `.env.example` |
| Docker services | `docker-compose.yml` |
| Dev seed data | `database/seeds/001_sample_data.sql` |
| Postman | `postman/Meshkee-CMS-Auth.postman_collection.json` |