Files
backend/docs/PROJECT_CONTEXT.md
Alireza HassaniandCursor 5734a73c9c Add business-scoped invoices billed to users.
Introduce invoices.user_id, business template/invoice APIs, and tenant-domain public URLs so business dashboards can issue invoices without platform-scope template mismatches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 15:17:03 +03:30

728 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Meshkee CMS API — Project Context
> Living reference for developers and AI assistants working on this codebase.
> Last updated: August 11, 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 (Parspack), 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
├── website-docs/ # Public website API docs pack
├── invoices/ # Platform invoices + item templates (super-admin; business-ready schema)
├── public-sms/ # Partner SMS gateway (API key + domain allowlist → Gama)
├── favorites/ # Customer product favorites
├── user-products/ # Customer self-service stock listings (`my-user-products`)
├── 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`) |
| `036_invoices.sql` | Invoices, invoice items, invoice item templates + permissions |
| `037_invoice_name.sql` | Optional `invoices.name` |
| `038_invoice_templates.sql` | Full invoice templates + key points/accounts on invoices |
| `039_invoice_account_holder.sql` | `account_holder_name` on invoice / template accounts |
| `040_invoice_public_id.sql` | Opaque `public_id` for unguessable public invoice links |
| `041_invoice_status_approved.sql` | Invoice status `approved` |
| `049_user_name_en.sql` | Optional `users.first_name_en` / `last_name_en` for EN display names |
| `052_user_products.sql` | Customer stock listings (`user_products`) + technical values; `cities.level` adds `district`; `media_entity_type` adds `user_product` |
| `053_cities_country_optional_province.sql` | City may hang under country (province optional); seed Iraq/Turkey/UAE + major cities |
| `054_seed_iran_provinces_cities.sql` | Seed Iran provinces + cities when missing (004 seed may never have run) |
| `055_user_products_listing_fields.sql` | User product listing fields: `price_currency`, `delivery_note`, `condition` (`user_product_condition`), `technical_notes` |
| `058_invoice_user_id.sql` | `invoices.user_id` billed user (backfill from business owner) |
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`, `customer`, `user_product` |
| `ContentStatus` | `draft`, `published`, `archived` |
| `VariationType` | `color`, `size`, `custom` |
| `CityLevel` | `country`, `province`, `city`, `district` |
| `OrderStatus` | `pending`, `confirmed`, `processing`, `shipped`, `delivered`, `cancelled` |
| `OrderSource` | `website`, `admin` |
| `InvoiceOwnerScope` | `platform`, `business` |
| `InvoiceStatus` | `draft`, `issued`, `approved`, `paid`, `cancelled` |
| `TechnicalFieldType` | `text`, `textarea`, `select`, `multi_select` |
| `UserProductCondition` | `new`, `stock`, `needs_repair`, `scrap` |
| `MediaType` | `image`, `video` |
### Core relationships
```
Business 1──* Domain
Business 1──* Category (entityType: product|blog|portfolio|customer)
Business 1──* Product
Business 1──* UserProduct (customer stock; no variations; location + technical data)
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──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
UserProduct *── Category (via CategoryAssignment, entityType user_product → product categories)
UserProduct 1──* UserProductTechnicalFieldValue → CategoryTechnicalFormField
UserProduct → City (country, city, optional district)
User 1──* UserProduct
Business 1──* Cart (per customer) 1──* CartItem → ProductVariant
Business 1──* Order 1──* OrderItem → ProductVariant (snapshot on order)
User 1──* Cart, Order (as customer)
Business 1──* Invoice (tenant context) 1──* InvoiceItem
User 1──* Invoice (billed party via user_id)
InvoiceItemTemplate / InvoiceTemplate (platform or per-business)
Invoice.issuedBy → User (issuer)
Invoice.issuerBusinessId → Business (when owner_scope=business)
Media 1──* MediaAttachment (polymorphic: entityType + entityId)
```
### Branding & dashboard locale
`businesses.settings.branding.defaultLocale` is `'fa' | 'en'` (default **`fa`**). Exposed on tenant resolve and editable from super-admin. Dashboards apply it once on load.
Optional EN name fields: `users.first_name_en` / `last_name_en` (migration `049`).
### Dashboard activity (CMS only — not website-facing)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/businesses/:businessId/orders/activity?days=30` | Dual daily series: orders + cart-item creates |
| GET | `/businesses/:businessId/customers/activity?days=30` | Dual daily series: registrations + active logins (`last_login_at`) |
Lightweight `GROUP BY` counts for business home charts. Requires `orders.read`.
---
## 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/login-otp` | Passwordless login with SMS OTP |
| POST | `/auth/reset-password` | Reset password with SMS OTP |
| POST | `/auth/refresh` | Refresh token |
| POST | `/auth/send-otp` | Send OTP (Redis-backed) |
| POST | `/auth/verify-otp` | Verify OTP (marks cell verified; no tokens) |
| 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 |
#### My user products (customer — JWT, must be business customer)
Base: `/businesses/:businessId/my-user-products`
| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | List current user's user products (paginated) |
| POST | `/` | Create draft user product (category, location, condition, optional technical values) |
| GET | `/categories` | Active product categories for picker (`id`, `name`, `nameFa`, `parentId`) |
| GET | `/categories/:categoryId/technical-form` | Category technical form (customer access; no `categories.read`) |
#### Public user products (storefront — no auth)
Base: `/tenants/:host/user-products`
| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | List published listings (`name`/`q`, `categoryId`, `cityId`, `countryId`, `condition`, `promoted`, pagination) |
| GET | `/:slug` | Details + gallery (`images`, `galleryMediaIds`) + technical values |
| GET | `/:slug/technical-info` | Category technical form + values |
#### 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/activity` | Admin `orders.read` | Daily order + cart-add counts |
| 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 (incl. full team manage for editor/viewer) |
| Admin | `admin` | Full content + team manage for **non-admins**; only **super_admin** may assign this role |
| Editor | `editor` | Create/edit/publish content (no team manage) |
| Viewer | `viewer` | Read-only |
Super-admin Users (business filter) and business Customers: change access is **Customer** vs **Manager**, then Admin/Editor/Viewer (Admin option super-admin only). Business owners are locked. API: `PATCH /businesses/:businessId/team/access`.
### 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`
- `POST /auth/send-otp` → SMS code; `POST /auth/verify-otp` marks `cellVerifiedAt` (no tokens)
- `POST /auth/login-otp` → passwordless login (consumes OTP, verifies cell, returns tokens)
- `POST /auth/reset-password` → forgot password (OTP + `newPassword`, verifies cell)
- Password login (`POST /auth/login`) rejects unverified cells when SMS is enabled
- JWT payload: `sub`, `cellNumber`, `roles`, `dashboard`, `type`
- SMS provider: Gama (`sms.igama.ir`) SendQuick via service shortcode (`SMS_GAMA_*`)
- Partner gateway (external sites like Balout): `POST /api/v1/public/sms/send` with `X-Api-Key` + body `{ domain, to, message }`; partners configured in `SMS_PARTNERS` (`domain:apiKey` pairs). Rate limits: 30/partner/min and 5/destination/min. Not part of storefront website-api docs.
---
## 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 Parspack S3 via Sharp processing
- Object keys: `meshkee/businesses/{businessId}/media/{uuid}{ext}` (library), `meshkee/businesses/{businessId}/brand/favicon-*.png` (derived favicons)
- 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`, `SMS_GAMA_BASE_URL`, `SMS_GAMA_USERNAME`, `SMS_GAMA_PASSWORD`, `SMS_GAMA_SOURCE_SERVICE`, `SMS_PARTNERS` |
| S3 | `S3_ENDPOINT`, `S3_BUCKET`, `S3_PUBLIC_URL`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` |
| Legacy MySQL (WillaEngine migrate) | `OLD_MYSQL_HOST`, `OLD_MYSQL_PORT`, `OLD_MYSQL_USER`, `OLD_MYSQL_PASSWORD`, `OLD_MYSQL_DATABASE` |
| Legacy S3 source (media copy) | `OLD_S3_ENDPOINT`, `OLD_S3_BUCKET`, `OLD_S3_PUBLIC_URL`, `OLD_S3_ACCESS_KEY_ID`, `OLD_S3_SECRET_ACCESS_KEY` |
| Media | `MEDIA_MAX_FILE_SIZE_MB` |
---
## Implemented vs Planned
### Implemented
- Multi-tenant auth (register, login, passwordless OTP login, reset password via SMS, profile)
- Super admin: users, businesses, domains, system business categories
- Super admin: selective migrate-from-old + purge-data (portfolio categories + portfolios; oversized images resized to max 1280×1280; purge removes portfolios + images)
- 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
- Auth SMS OTP via Gama SendQuick (`send-otp`, `verify-otp`, `login-otp`, `reset-password`)
- Partner SMS gateway (`POST /public/sms/send`) + Gama SendQuick integration
### Planned / partial
| Feature | DB | Permissions | API | Prisma |
|---------|----|-------------|-----|--------|
| Blogs | Yes | Yes | No | No model |
| Portfolios | Yes | Yes | Partial (migrate-from-old) | Yes |
| Customer dashboard | Partial | No | Register only | Yes |
| Store checkout (cart, orders) | Yes | Yes | Yes | Yes |
| Customer favorites | — | `favorites.*` seeded | Partial | Yes |
| Customer user products | Yes (`052`+`055`) | Admin `user_products.*` + customer JWT | Yes (`my-user-products`, admin, public tenants) | Yes |
---
## Invoices (platform + business)
Invoices bill a **User** (`user_id`). `business_id` is tenant/context. Super-admin issues platform invoices (`owner_scope=platform`, default billed user = business owner). Business admins issue business-scoped invoices (`owner_scope=business`, `issuer_business_id`, required `userId` who is a customer or team member).
### Tables
| Table | Purpose |
|-------|---------|
| `invoice_item_templates` | Predefined line items (`owner_scope` platform \| business) |
| `invoice_templates` | Full blueprints: name, top_text |
| `invoice_template_items` / `_key_points` / `_accounts` | Nested template content |
| `invoices` | Header: `user_id` (billed), `business_id` (context), optional `name`, `top_text`, `notes`, `invoice_template_id`, `status`, `public_id` |
| `invoice_items` / `invoice_key_points` / `invoice_accounts` | Issued invoice nested content |
### API — platform templates (super_admin)
| Method | Path |
|--------|------|
| GET/POST | `/invoice-item-templates` |
| PATCH/DELETE | `/invoice-item-templates/:templateId` |
| GET/POST | `/invoice-templates` |
| GET/PATCH/DELETE | `/invoice-templates/:templateId` |
### API — business-scoped (`BusinessPermissionGuard`)
| Method | Path | Permission |
|--------|------|------------|
| GET/POST | `/businesses/:businessId/invoice-item-templates` | `invoice_templates.read` / `.manage` |
| PATCH/DELETE | `.../invoice-item-templates/:templateId` | `invoice_templates.manage` |
| GET/POST | `/businesses/:businessId/invoice-templates` | `invoice_templates.read` / `.manage` |
| GET/PATCH/DELETE | `.../invoice-templates/:templateId` | read / manage |
| GET/POST | `/businesses/:businessId/invoices` | `invoices.read` / `.create` (`?userId=` filter) |
| GET/PUT/PATCH/DELETE | `.../invoices/:invoiceId` | read / update / delete |
Super-admin calling `/businesses/:id/invoices*` still operates on **platform** invoices for that business (service branches on `isSuperAdmin`).
### Public
| Method | Path |
|--------|------|
| GET | `/public/invoices/:publicId` (issued/approved/paid; platform or business) |
| POST | `/public/invoices/:publicId/approve` (`issued``approved`) |
Serialized invoices include `userId`, `user`, `publicId`, `publicUrl` (both scopes). Business-scoped `publicUrl` uses the business primary domain (`https://{host}/invoices/{publicId}`); platform uses `INVOICE_PUBLIC_DOMAIN` (default `meshkee.com`).
Module: `src/invoices/` · Migrations: `036``041`, `058_invoice_user_id.sql`
---
## 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` (`INVOICE_PUBLIC_DOMAIN` / optional `INVOICE_PUBLIC_BASE_URL`) |
| Invoices module | `src/invoices/` |
| Invoice migrations | `database/migrations/036_invoices.sql``041_invoice_status_approved.sql`, `058_invoice_user_id.sql` |
| Docker services | `docker-compose.yml` |
| Dev seed data | `database/seeds/001_sample_data.sql` |
| Postman | `postman/Meshkee-CMS-Auth.postman_collection.json` |