mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
Add public user-products storefront API and website docs.
Expose published customer listings under /tenants/:host/user-products (list, search, details, technical-info) and document them in the website API pack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
158523df7b
commit
953b87b616
+40
-5
@@ -1,7 +1,7 @@
|
||||
# Meshkee CMS API — Project Context
|
||||
|
||||
> Living reference for developers and AI assistants working on this codebase.
|
||||
> Last updated: August 5, 2026
|
||||
> Last updated: August 9, 2026
|
||||
|
||||
## What This Project Is
|
||||
|
||||
@@ -80,6 +80,8 @@ src/
|
||||
├── 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)
|
||||
@@ -159,6 +161,10 @@ Example super admin: `+989121111111` / `password`
|
||||
| `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 |
|
||||
| `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` |
|
||||
|
||||
Docker mounts `./database/migrations` into Postgres init — migrations run automatically only on **first** volume creation. Use `migrate.sh` for subsequent migrations.
|
||||
|
||||
@@ -170,22 +176,25 @@ Docker mounts `./database/migrations` into Postgres init — migrations run auto
|
||||
|
||||
| Enum | Values |
|
||||
|------|--------|
|
||||
| `MediaEntityType` | `product`, `blog`, `portfolio` |
|
||||
| `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)
|
||||
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
|
||||
@@ -194,12 +203,16 @@ 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
|
||||
|
||||
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)
|
||||
@@ -295,6 +308,27 @@ Pattern: `/businesses/:businessId/<resource>`
|
||||
| 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 |
|
||||
@@ -583,7 +617,8 @@ See `.env.example` for the full list. Key groups:
|
||||
| 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 | No | No |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -31,10 +31,17 @@ You are building a **Meshkee business website (storefront)**. You must use the M
|
||||
### Typical bootstrap sequence
|
||||
1. `GET /tenants/{domain}` → branding + `businessId`
|
||||
2. Homepage: business-info, sliders, category-groups, brand-groups, store-specials
|
||||
3. Catalog: categories, products, store-items
|
||||
3. Catalog: categories, products, store-items, **user-products** (customer stock listings)
|
||||
4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens).
|
||||
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
|
||||
|
||||
### User products (customer listings)
|
||||
Public marketplace listings owned by customers — not catalog `products`.
|
||||
- `GET /tenants/{domain}/user-products` — list published (`name`/`q`, `categoryId`, `cityId`, `countryId`, `condition`, `promoted`, pagination)
|
||||
- `GET /tenants/{domain}/user-products/{slug}` — details + gallery
|
||||
- `GET /tenants/{domain}/user-products/{slug}/technical-info` — category form + values
|
||||
Use product categories from `GET /tenants/{domain}/categories?entityType=product` for filters. Creating/editing listings is customer-dashboard only (`/businesses/.../my-user-products`), not website-facing.
|
||||
|
||||
If OpenAPI and this brief conflict, **OpenAPI wins**.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
"key": "productSlug",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "userProductId",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "userProductSlug",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "blogId",
|
||||
"value": ""
|
||||
@@ -1101,6 +1109,133 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "User Products",
|
||||
"item": [
|
||||
{
|
||||
"name": "List published user products",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" const json = pm.response.json();",
|
||||
" if (json.items?.[0]?.id) pm.collectionVariables.set('userProductId', json.items[0].id);",
|
||||
" if (json.items?.[0]?.slug) pm.collectionVariables.set('userProductSlug', json.items[0].slug);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/tenants/{{domain}}/user-products?page=1&pageSize=12",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"tenants",
|
||||
"{{domain}}",
|
||||
"user-products"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "pageSize",
|
||||
"value": "12"
|
||||
},
|
||||
{
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "q",
|
||||
"value": "",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "categoryId",
|
||||
"value": "{{categoryId}}",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "cityId",
|
||||
"value": "",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "countryId",
|
||||
"value": "",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "condition",
|
||||
"value": "new",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "promoted",
|
||||
"value": "true",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Search user products",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/tenants/{{domain}}/user-products?q=boiler&page=1&pageSize=12",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"tenants",
|
||||
"{{domain}}",
|
||||
"user-products"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "q",
|
||||
"value": "boiler"
|
||||
},
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "pageSize",
|
||||
"value": "12"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get user product by slug",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/tenants/{{domain}}/user-products/{{userProductSlug}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get user product technical info by slug",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/tenants/{{domain}}/user-products/{{userProductSlug}}/technical-info"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Store Items",
|
||||
"item": [
|
||||
|
||||
@@ -88,10 +88,17 @@
|
||||
<ol>
|
||||
<li>Variable <code>domain</code> = website apex only (no <code>www</code>/<code>api</code>/<code>customer</code>/<code>business</code>).</li>
|
||||
<li><code>GET /tenants/{domain}</code> → <code>businessId</code>.</li>
|
||||
<li>Public pages: <code>/tenants/{domain}/...</code> (no auth).</li>
|
||||
<li>Public pages: <code>/tenants/{domain}/...</code> (no auth) — products, <strong>user-products</strong>, blogs, portfolios, store-items, etc.</li>
|
||||
<li>Cart / orders / favorites: <code>/businesses/{businessId}/...</code> + Bearer JWT.</li>
|
||||
</ol>
|
||||
|
||||
<h2>User products (customer listings)</h2>
|
||||
<p>
|
||||
Marketplace-style stock listings created by customers. Public read-only under
|
||||
<code>/tenants/{domain}/user-products</code> (list / details / technical-info).
|
||||
See OpenAPI tag <strong>User Products</strong>.
|
||||
</p>
|
||||
|
||||
<h2>For a new website AI / designer</h2>
|
||||
<ol>
|
||||
<li>Open <a href="/docs/website/AI_PROMPT.md">AI_PROMPT.md</a> and paste it into the AI chat.</li>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
{ "name": "Homepage" },
|
||||
{ "name": "Categories" },
|
||||
{ "name": "Products" },
|
||||
{ "name": "User Products" },
|
||||
{ "name": "Store" },
|
||||
{ "name": "Blogs" },
|
||||
{ "name": "Portfolios" },
|
||||
@@ -210,6 +211,66 @@
|
||||
"responses": { "200": { "description": "{ form, values }" } }
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/user-products": {
|
||||
"get": {
|
||||
"tags": ["User Products"],
|
||||
"summary": "List published customer / stock listings",
|
||||
"description": "Public marketplace-style listings created by customers (user products). Only `status=published`. Search with `name` or `q` (title/description). Filter by category, city, country, condition, or promoted.",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
|
||||
{ "name": "pageSize", "in": "query", "schema": { "type": "integer", "default": 12 } },
|
||||
{ "name": "name", "in": "query", "description": "Search title/description (alias of q)", "schema": { "type": "string" } },
|
||||
{ "name": "q", "in": "query", "description": "Search title/description (alias of name)", "schema": { "type": "string" } },
|
||||
{ "name": "categoryId", "in": "query", "schema": { "type": "string" } },
|
||||
{ "name": "cityId", "in": "query", "schema": { "type": "string" } },
|
||||
{ "name": "countryId", "in": "query", "schema": { "type": "string" } },
|
||||
{
|
||||
"name": "condition",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["new", "stock", "needs_repair", "scrap"]
|
||||
}
|
||||
},
|
||||
{ "name": "promoted", "in": "query", "schema": { "type": "boolean" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ items: UserProductListItem[], total, page, pageSize }. Each item includes id, slug, titleFa/titleEn, price, priceCurrency, condition, city/country names, imageUrl, category*, promoted, publishedAt."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/user-products/{slug}": {
|
||||
"get": {
|
||||
"tags": ["User Products"],
|
||||
"summary": "User product details by slug",
|
||||
"description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), technical field values, delivery/technical notes.",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues, countryId, cityId, countrySlug"
|
||||
},
|
||||
"404": { "description": "Not found or not published" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/user-products/{slug}/technical-info": {
|
||||
"get": {
|
||||
"tags": ["User Products"],
|
||||
"summary": "User product technical form + values",
|
||||
"description": "Category technical form schema plus the listing’s submitted values (same shape as dashboard technical values).",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": { "200": { "description": "{ form, values }" } }
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/store-items": {
|
||||
"get": {
|
||||
"tags": ["Store"],
|
||||
|
||||
Reference in New Issue
Block a user