mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
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>
728 lines
31 KiB
Markdown
728 lines
31 KiB
Markdown
# 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` |
|