From fb04425df75c5003503fffb262ff1759c4422a54 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Thu, 27 Aug 2026 06:46:28 +0330 Subject: [PATCH] Clarify technical-info docs and ship pending owner/color fixes. Document that product specs need /technical-info for labels, auto-verify admin-created business owners, and expand category color presets with Farsi aliases. Co-authored-by: Cursor --- docs/website-api/AI_PROMPT.md | 23 ++- ...eshkee-Website-API.postman_collection.json | 3 + docs/website-api/index.html | 10 ++ docs/website-api/openapi.json | 26 ++-- src/business-admin/business-admin.service.ts | 11 +- src/business-team/business-team.service.ts | 7 + src/categories/color-presets.ts | 131 ++++++++++++++---- src/users/users.service.ts | 24 ++++ src/website-docs/static/AI_PROMPT.md | 23 ++- ...eshkee-Website-API.postman_collection.json | 3 + src/website-docs/static/index.html | 10 ++ src/website-docs/static/openapi.json | 26 ++-- 12 files changed, 242 insertions(+), 55 deletions(-) diff --git a/docs/website-api/AI_PROMPT.md b/docs/website-api/AI_PROMPT.md index fc648f6..60fedd3 100644 --- a/docs/website-api/AI_PROMPT.md +++ b/docs/website-api/AI_PROMPT.md @@ -27,6 +27,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M 5. Cell numbers are E.164 (`+98912...`). 6. Do not call dashboard/CMS routes (`/businesses/.../products` write APIs, media upload, domain-admin, etc.). 7. **Partner SMS** (`POST /public/sms/send`) is for external partner backends with an issued `X-Api-Key` only — not for normal storefront UI. See https://api.meshkee.com/docs/website/SMS.md +8. **Technical details (labels + values):** Product/user-product detail responses may include `technicalValues` with **values only** (`fieldId` + `textValue` / `optionId` / `optionIds` — **no field labels**). To render a label→value specs table you **must** call the matching `.../technical-info` endpoint and join `form.fields[].id` ↔ `values[].fieldId`. Never invent a separate “variation fields” or “category fields” public route — those do not exist on the website API. ### Typical bootstrap sequence 1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`) @@ -50,10 +51,28 @@ You are building a **Meshkee business website (storefront)**. You must use the M ### 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 +- `GET /tenants/{domain}/user-products/{slug}` — details + gallery (`technicalValues` = values only, no labels) +- `GET /tenants/{domain}/user-products/{slug}/technical-info` — **required for specs UI**: `{ form: { fields: [{ id, label, key, type, ... }] }, 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. +### Technical details / specs table (catalog products + user products) +**Do not** render only `technicalValues` from the detail endpoint — users will see bare values (e.g. `SAMP`, `1992`) with no labels. + +| Detail page | Specs endpoint (labels + values) | +|-------------|----------------------------------| +| `GET .../products/{slug}` or `.../products/by-id/{id}` | `GET .../products/{slug}/technical-info` or `.../products/by-id/{id}/technical-info` | +| `GET .../user-products/{slug}` | `GET .../user-products/{slug}/technical-info` | + +**How to render:** +1. Call `technical-info` (same slug/id as the detail page). +2. For each `values[]` entry, find `form.fields` where `field.id === value.fieldId`. +3. Show `field.label` (or localized label if the site uses FA/EN) next to the value (`textValue`, or resolve option labels from `field.options` when `optionId` / `optionIds` are set). +4. Follow `form.fields` `sortOrder` for display order. +5. If `form` is null/empty or `values` is empty, hide the technical section. + +**Wrong:** `GET .../product-category-variation-fields?categoryId=…` (does not exist → 404). +**Wrong:** expecting `fieldName` / `fieldNameFa` on each `technicalValues` item in the product detail response. + ### Analytics (dashboard charts) Page views are recorded **automatically** when the website calls the normal public list/detail APIs (blogs, products, portfolios, videos, store-items, and homepage sliders). No extra website code is required for those. diff --git a/docs/website-api/Meshkee-Website-API.postman_collection.json b/docs/website-api/Meshkee-Website-API.postman_collection.json index 63ecf17..9283e6b 100644 --- a/docs/website-api/Meshkee-Website-API.postman_collection.json +++ b/docs/website-api/Meshkee-Website-API.postman_collection.json @@ -1237,6 +1237,7 @@ "name": "Get product technical info by slug", "request": { "method": "GET", + "description": "Field labels + values for the product specs UI. Join form.fields[].id to values[].fieldId. Detail alone has no labels.", "url": "{{baseUrl}}/tenants/{{domain}}/products/{{productSlug}}/technical-info" } } @@ -1357,6 +1358,7 @@ "name": "Get user product by slug", "request": { "method": "GET", + "description": "Listing detail + gallery. technicalValues are values only (fieldId + text/option) — no labels. Use technical-info for specs UI.", "url": "{{baseUrl}}/tenants/{{domain}}/user-products/{{userProductSlug}}" } }, @@ -1364,6 +1366,7 @@ "name": "Get user product technical info by slug", "request": { "method": "GET", + "description": "Required for technical-details UI: form.fields (labels) + values. Join field.id ↔ value.fieldId. No separate category-variation-fields public endpoint.", "url": "{{baseUrl}}/tenants/{{domain}}/user-products/{{userProductSlug}}/technical-info" } } diff --git a/docs/website-api/index.html b/docs/website-api/index.html index 213be2e..e2cc549 100644 --- a/docs/website-api/index.html +++ b/docs/website-api/index.html @@ -107,6 +107,16 @@ /tenants/{domain}/user-products (list / details / technical-info). See OpenAPI tag User Products.

+

+ Technical details: detail responses include + technicalValues with values only (no field labels). + For a label→value specs table, call + GET /tenants/{domain}/user-products/{slug}/technical-info + (catalog products: + .../products/{slug}/technical-info) and join + form.fields[].id ↔ values[].fieldId. + There is no public product-category-variation-fields route. +

For a new website AI / designer

    diff --git a/docs/website-api/openapi.json b/docs/website-api/openapi.json index d67d88c..829f00b 100644 --- a/docs/website-api/openapi.json +++ b/docs/website-api/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Meshkee Website API", "version": "1.0.0", - "description": "Global storefront API for every Meshkee business website.\n\n**Not domain-specific.** Replace `{domain}` with the website apex (e.g. `sanihome.ir`).\n\n**Base URL:** `https://api.meshkee.com/api/v1` (or `https://api.{domain}/api/v1` if that alias is configured).\n\n**Tenant rule:** public content uses `/tenants/{domain}/...`. After login, cart/orders/favorites use `/businesses/{businessId}/...` with Bearer JWT.\n\n**Docs:** https://api.meshkee.com/docs/website" + "description": "Global storefront API for every Meshkee business website.\n\n**Not domain-specific.** Replace `{domain}` with the website apex (e.g. `sanihome.ir`).\n\n**Base URL:** `https://api.meshkee.com/api/v1` (or `https://api.{domain}/api/v1` if that alias is configured).\n\n**Tenant rule:** public content uses `/tenants/{domain}/...`. After login, cart/orders/favorites use `/businesses/{businessId}/...` with Bearer JWT.\n\n**Technical specs (AI agents):** Detail endpoints may return `technicalValues` with **values only** (`fieldId` + text/option ids — **no labels**). To render label→value technical details, always call the matching `.../technical-info` endpoint and join `form.fields[].id` to `values[].fieldId`. There is no public `product-category-variation-fields` route.\n\n**Docs:** https://api.meshkee.com/docs/website" }, "servers": [ { @@ -34,10 +34,12 @@ "name": "Categories" }, { - "name": "Products" + "name": "Products", + "description": "Catalog products. For a specs/technical-details table on the product page, call `GET .../products/{slug}/technical-info` (or by-id). Do not rely on detail-only payloads for field labels." }, { - "name": "User Products" + "name": "User Products", + "description": "Customer marketplace listings. Detail returns `technicalValues` without labels; use `GET .../user-products/{slug}/technical-info` for form field labels + values." }, { "name": "Store" @@ -1003,6 +1005,7 @@ "Products" ], "summary": "Product technical info by id", + "description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Use this for the product specs / technical-details UI** — join `form.fields[].id` ↔ `values[].fieldId`. Product detail alone does not include field labels.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1018,7 +1021,7 @@ ], "responses": { "200": { - "description": "Technical form + values" + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }. Render label→value; hide section if form/fields/values empty." } } } @@ -1080,7 +1083,8 @@ "tags": [ "Products" ], - "summary": "Product technical specs", + "summary": "Product technical specs (labels + values)", + "description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Required for specs UI.** Join `form.fields[].id` ↔ `values[].fieldId`. Do not invent a separate category-fields public endpoint.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1096,7 +1100,7 @@ ], "responses": { "200": { - "description": "{ form, values }" + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }" } } } @@ -1199,7 +1203,7 @@ "User Products" ], "summary": "User product details by slug", - "description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), technical field values, delivery/technical notes.", + "description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), delivery/technical notes, and `technicalValues`.\n\n**Important:** `technicalValues` items are **values only** (`fieldId` + `textValue` / `optionId` / `optionIds`). They do **not** include field labels (`label`, `fieldName`, etc.). For a label→value technical-details table, call `GET /tenants/{domain}/user-products/{slug}/technical-info` and join on `fieldId`.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1215,7 +1219,7 @@ ], "responses": { "200": { - "description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues, countryId, cityId, countrySlug" + "description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues (values only — no labels), countryId, cityId, countrySlug" }, "404": { "description": "Not found or not published" @@ -1228,8 +1232,8 @@ "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).", + "summary": "User product technical form + values (labels)", + "description": "**Use this for the listing specs / technical-details UI.** Returns the category technical form (field `id`, `label`, `key`, `type`, `sortOrder`, `options`) plus the listing’s submitted `values`.\n\nJoin `form.fields[].id` ↔ `values[].fieldId` and render label→value. Sort by `sortOrder`. Hide the section if `form`/fields/`values` are empty.\n\nThere is **no** public `product-category-variation-fields` (or similar) endpoint — use this path only.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1245,7 +1249,7 @@ ], "responses": { "200": { - "description": "{ form, values }" + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }" } } } diff --git a/src/business-admin/business-admin.service.ts b/src/business-admin/business-admin.service.ts index 6e92556..2b6290e 100644 --- a/src/business-admin/business-admin.service.ts +++ b/src/business-admin/business-admin.service.ts @@ -1129,13 +1129,17 @@ export class BusinessAdminService { // Users can own multiple businesses. If the cell number already exists, // refresh name + password from the create form, then attach as owner. + // Admin-created owners are trusted — always mark the cell verified. if (existing) { if (!existing.isActive) { throw new BadRequestException('Owner user is inactive'); } return tx.user.update({ where: { id: existing.id }, - data: nameData, + data: { + ...nameData, + cellVerifiedAt: existing.cellVerifiedAt ?? new Date(), + }, }); } @@ -1202,6 +1206,11 @@ export class BusinessAdminService { data: { userId: ownerUserId, roleId: businessOwnerRole.id }, }); } + + await tx.user.update({ + where: { id: ownerUserId }, + data: { cellVerifiedAt: new Date() }, + }); } private serializeBusiness( diff --git a/src/business-team/business-team.service.ts b/src/business-team/business-team.service.ts index f83312a..e913c57 100644 --- a/src/business-team/business-team.service.ts +++ b/src/business-team/business-team.service.ts @@ -137,6 +137,13 @@ export class BusinessTeamService { }, })); + if (existingUser && !existingUser.cellVerifiedAt) { + await tx.user.update({ + where: { id: user.id }, + data: { cellVerifiedAt: new Date() }, + }); + } + const businessUser = await tx.businessUser.create({ data: { businessId, diff --git a/src/categories/color-presets.ts b/src/categories/color-presets.ts index 69e325f..7e1308e 100644 --- a/src/categories/color-presets.ts +++ b/src/categories/color-presets.ts @@ -1,47 +1,122 @@ export const COLOR_PRESETS = [ { name: 'Red', hex: '#EF4444' }, - { name: 'Blue', hex: '#3B82F6' }, + { name: 'Dark Red', hex: '#B91C1C' }, + { name: 'Crimson', hex: '#DC143C' }, + { name: 'Maroon', hex: '#7F1D1D' }, + { name: 'Rose', hex: '#F43F5E' }, + { name: 'Pink', hex: '#EC4899' }, + { name: 'Hot Pink', hex: '#DB2777' }, + { name: 'Magenta', hex: '#D946EF' }, + { name: 'Coral', hex: '#FB7185' }, + { name: 'Salmon', hex: '#FDA4AF' }, + { name: 'Orange', hex: '#F97316' }, + { name: 'Dark Orange', hex: '#C2410C' }, + { name: 'Amber', hex: '#F59E0B' }, + { name: 'Yellow', hex: '#EAB308' }, + { name: 'Mustard', hex: '#CA8A04' }, + { name: 'Gold', hex: '#D4AF37' }, + { name: 'Bronze', hex: '#B45309' }, + { name: 'Copper', hex: '#B87333' }, + { name: 'Brown', hex: '#92400E' }, + { name: 'Dark Brown', hex: '#5C3A21' }, + { name: 'Tan', hex: '#D2B48C' }, + { name: 'Beige', hex: '#D4C4A8' }, + { name: 'Cream', hex: '#FFF7ED' }, + { name: 'Ivory', hex: '#FFFFF0' }, + { name: 'Khaki', hex: '#C3B091' }, + { name: 'Olive', hex: '#808000' }, + { name: 'Lime', hex: '#84CC16' }, { name: 'Green', hex: '#22C55E' }, + { name: 'Dark Green', hex: '#166534' }, + { name: 'Forest', hex: '#14532D' }, + { name: 'Emerald', hex: '#059669' }, + { name: 'Mint', hex: '#6EE7B7' }, + { name: 'Teal', hex: '#14B8A6' }, + { name: 'Turquoise', hex: '#2DD4BF' }, + { name: 'Cyan', hex: '#06B6D4' }, + { name: 'Sky Blue', hex: '#38BDF8' }, + { name: 'Light Blue', hex: '#93C5FD' }, + { name: 'Blue', hex: '#3B82F6' }, + { name: 'Royal Blue', hex: '#1D4ED8' }, + { name: 'Navy', hex: '#1E3A5F' }, + { name: 'Indigo', hex: '#4F46E5' }, + { name: 'Purple', hex: '#A855F7' }, + { name: 'Violet', hex: '#7C3AED' }, + { name: 'Lavender', hex: '#C4B5FD' }, + { name: 'Lilac', hex: '#D8B4FE' }, + { name: 'Burgundy', hex: '#9F1239' }, + { name: 'Wine', hex: '#881337' }, + { name: 'Gray', hex: '#6B7280' }, + { name: 'Light Gray', hex: '#D1D5DB' }, + { name: 'Dark Gray', hex: '#374151' }, + { name: 'Charcoal', hex: '#1F2937' }, + { name: 'Silver', hex: '#C0C0C0' }, { name: 'Black', hex: '#111827' }, { name: 'White', hex: '#FFFFFF' }, - { name: 'Silver', hex: '#C0C0C0' }, - { name: 'Gold', hex: '#D4AF37' }, - { name: 'Navy', hex: '#1E3A5F' }, - { name: 'Yellow', hex: '#EAB308' }, - { name: 'Orange', hex: '#F97316' }, - { name: 'Purple', hex: '#A855F7' }, - { name: 'Pink', hex: '#EC4899' }, - { name: 'Brown', hex: '#92400E' }, - { name: 'Gray', hex: '#6B7280' }, - { name: 'Beige', hex: '#D4C4A8' }, ] as const; +type ColorPresetName = (typeof COLOR_PRESETS)[number]['name']; + /** Common Farsi labels from legacy migrations → English preset name */ -const COLOR_ALIASES: Record = { +const COLOR_ALIASES: Record = { قرمز: 'Red', - آبی: 'Blue', - سبز: 'Green', - مشکی: 'Black', - سیاه: 'Black', - سفید: 'White', - نقرهای: 'Silver', - 'نقره ای': 'Silver', - نقره‌ای: 'Silver', + 'قرمز تیره': 'Dark Red', + آلبالویی: 'Crimson', + عنابی: 'Maroon', + صورتی: 'Pink', + 'صورتی تند': 'Hot Pink', + سرخابی: 'Magenta', + مرجانی: 'Coral', + سالمون: 'Salmon', + نارنجی: 'Orange', + کهربایی: 'Amber', + زرد: 'Yellow', + خردلی: 'Mustard', طلایی: 'Gold', طلائی: 'Gold', - سرمهای: 'Navy', - 'سرمه ای': 'Navy', - سرمه‌ای: 'Navy', - زرد: 'Yellow', - نارنجی: 'Orange', - بنفش: 'Purple', - صورتی: 'Pink', + برنزی: 'Bronze', + مسی: 'Copper', قهوهای: 'Brown', 'قهوه ای': 'Brown', قهوه‌ای: 'Brown', + 'قهوه‌ای تیره': 'Dark Brown', + خاکی: 'Tan', + بژ: 'Beige', + کرم: 'Cream', + عاجی: 'Ivory', + زیتونی: 'Olive', + لیمویی: 'Lime', + سبز: 'Green', + 'سبز تیره': 'Dark Green', + زمردی: 'Emerald', + نعنایی: 'Mint', + سبزآبی: 'Teal', + فیروزهای: 'Turquoise', + 'فیروزه ای': 'Turquoise', + فیروزه‌ای: 'Turquoise', + آبی: 'Blue', + آسمانی: 'Sky Blue', + 'آبی روشن': 'Light Blue', + 'آبی سلطنتی': 'Royal Blue', + سرمهای: 'Navy', + 'سرمه ای': 'Navy', + سرمه‌ای: 'Navy', + نیلی: 'Indigo', + بنفش: 'Purple', + یاسی: 'Lilac', + اسطوخودوسی: 'Lavender', + بادمجانی: 'Burgundy', خاکستری: 'Gray', طوسی: 'Gray', - بژ: 'Beige', + 'خاکستری روشن': 'Light Gray', + 'خاکستری تیره': 'Dark Gray', + ذغالی: 'Charcoal', + نقرهای: 'Silver', + 'نقره ای': 'Silver', + نقره‌ای: 'Silver', + مشکی: 'Black', + سیاه: 'Black', + سفید: 'White', }; export const COLOR_PRESET_NAMES = COLOR_PRESETS.map((color) => color.name); diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 1ef923f..5a66092 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -141,6 +141,13 @@ export class UsersService { roleId: targetRole.id, }, }); + + if (dto.roleSlug === 'business_owner') { + await tx.user.update({ + where: { id: userId }, + data: { cellVerifiedAt: user.cellVerifiedAt ?? new Date() }, + }); + } }); const userRoles = await this.prisma.userRole.findMany({ @@ -466,6 +473,20 @@ export class UsersService { } } + const isBusinessOwner = + Boolean( + await this.prisma.businessUser.findFirst({ + where: { userId, isOwner: true }, + select: { id: true }, + }), + ) || + Boolean( + await this.prisma.userRole.findFirst({ + where: { userId, role: { slug: 'business_owner' } }, + select: { id: true }, + }), + ); + const updated = await this.prisma.user.update({ where: { id: userId }, data: { @@ -477,6 +498,9 @@ export class UsersService { dto.lastNameEn !== undefined ? dto.lastNameEn.trim() || null : undefined, email: dto.email !== undefined ? dto.email.trim() || null : undefined, cellNumber: dto.cellNumber?.trim(), + ...(isBusinessOwner + ? { cellVerifiedAt: user.cellVerifiedAt ?? new Date() } + : {}), }, }); diff --git a/src/website-docs/static/AI_PROMPT.md b/src/website-docs/static/AI_PROMPT.md index fc648f6..60fedd3 100644 --- a/src/website-docs/static/AI_PROMPT.md +++ b/src/website-docs/static/AI_PROMPT.md @@ -27,6 +27,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M 5. Cell numbers are E.164 (`+98912...`). 6. Do not call dashboard/CMS routes (`/businesses/.../products` write APIs, media upload, domain-admin, etc.). 7. **Partner SMS** (`POST /public/sms/send`) is for external partner backends with an issued `X-Api-Key` only — not for normal storefront UI. See https://api.meshkee.com/docs/website/SMS.md +8. **Technical details (labels + values):** Product/user-product detail responses may include `technicalValues` with **values only** (`fieldId` + `textValue` / `optionId` / `optionIds` — **no field labels**). To render a label→value specs table you **must** call the matching `.../technical-info` endpoint and join `form.fields[].id` ↔ `values[].fieldId`. Never invent a separate “variation fields” or “category fields” public route — those do not exist on the website API. ### Typical bootstrap sequence 1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`) @@ -50,10 +51,28 @@ You are building a **Meshkee business website (storefront)**. You must use the M ### 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 +- `GET /tenants/{domain}/user-products/{slug}` — details + gallery (`technicalValues` = values only, no labels) +- `GET /tenants/{domain}/user-products/{slug}/technical-info` — **required for specs UI**: `{ form: { fields: [{ id, label, key, type, ... }] }, 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. +### Technical details / specs table (catalog products + user products) +**Do not** render only `technicalValues` from the detail endpoint — users will see bare values (e.g. `SAMP`, `1992`) with no labels. + +| Detail page | Specs endpoint (labels + values) | +|-------------|----------------------------------| +| `GET .../products/{slug}` or `.../products/by-id/{id}` | `GET .../products/{slug}/technical-info` or `.../products/by-id/{id}/technical-info` | +| `GET .../user-products/{slug}` | `GET .../user-products/{slug}/technical-info` | + +**How to render:** +1. Call `technical-info` (same slug/id as the detail page). +2. For each `values[]` entry, find `form.fields` where `field.id === value.fieldId`. +3. Show `field.label` (or localized label if the site uses FA/EN) next to the value (`textValue`, or resolve option labels from `field.options` when `optionId` / `optionIds` are set). +4. Follow `form.fields` `sortOrder` for display order. +5. If `form` is null/empty or `values` is empty, hide the technical section. + +**Wrong:** `GET .../product-category-variation-fields?categoryId=…` (does not exist → 404). +**Wrong:** expecting `fieldName` / `fieldNameFa` on each `technicalValues` item in the product detail response. + ### Analytics (dashboard charts) Page views are recorded **automatically** when the website calls the normal public list/detail APIs (blogs, products, portfolios, videos, store-items, and homepage sliders). No extra website code is required for those. diff --git a/src/website-docs/static/Meshkee-Website-API.postman_collection.json b/src/website-docs/static/Meshkee-Website-API.postman_collection.json index 63ecf17..9283e6b 100644 --- a/src/website-docs/static/Meshkee-Website-API.postman_collection.json +++ b/src/website-docs/static/Meshkee-Website-API.postman_collection.json @@ -1237,6 +1237,7 @@ "name": "Get product technical info by slug", "request": { "method": "GET", + "description": "Field labels + values for the product specs UI. Join form.fields[].id to values[].fieldId. Detail alone has no labels.", "url": "{{baseUrl}}/tenants/{{domain}}/products/{{productSlug}}/technical-info" } } @@ -1357,6 +1358,7 @@ "name": "Get user product by slug", "request": { "method": "GET", + "description": "Listing detail + gallery. technicalValues are values only (fieldId + text/option) — no labels. Use technical-info for specs UI.", "url": "{{baseUrl}}/tenants/{{domain}}/user-products/{{userProductSlug}}" } }, @@ -1364,6 +1366,7 @@ "name": "Get user product technical info by slug", "request": { "method": "GET", + "description": "Required for technical-details UI: form.fields (labels) + values. Join field.id ↔ value.fieldId. No separate category-variation-fields public endpoint.", "url": "{{baseUrl}}/tenants/{{domain}}/user-products/{{userProductSlug}}/technical-info" } } diff --git a/src/website-docs/static/index.html b/src/website-docs/static/index.html index 213be2e..e2cc549 100644 --- a/src/website-docs/static/index.html +++ b/src/website-docs/static/index.html @@ -107,6 +107,16 @@ /tenants/{domain}/user-products (list / details / technical-info). See OpenAPI tag User Products.

    +

    + Technical details: detail responses include + technicalValues with values only (no field labels). + For a label→value specs table, call + GET /tenants/{domain}/user-products/{slug}/technical-info + (catalog products: + .../products/{slug}/technical-info) and join + form.fields[].id ↔ values[].fieldId. + There is no public product-category-variation-fields route. +

    For a new website AI / designer

      diff --git a/src/website-docs/static/openapi.json b/src/website-docs/static/openapi.json index d67d88c..829f00b 100644 --- a/src/website-docs/static/openapi.json +++ b/src/website-docs/static/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Meshkee Website API", "version": "1.0.0", - "description": "Global storefront API for every Meshkee business website.\n\n**Not domain-specific.** Replace `{domain}` with the website apex (e.g. `sanihome.ir`).\n\n**Base URL:** `https://api.meshkee.com/api/v1` (or `https://api.{domain}/api/v1` if that alias is configured).\n\n**Tenant rule:** public content uses `/tenants/{domain}/...`. After login, cart/orders/favorites use `/businesses/{businessId}/...` with Bearer JWT.\n\n**Docs:** https://api.meshkee.com/docs/website" + "description": "Global storefront API for every Meshkee business website.\n\n**Not domain-specific.** Replace `{domain}` with the website apex (e.g. `sanihome.ir`).\n\n**Base URL:** `https://api.meshkee.com/api/v1` (or `https://api.{domain}/api/v1` if that alias is configured).\n\n**Tenant rule:** public content uses `/tenants/{domain}/...`. After login, cart/orders/favorites use `/businesses/{businessId}/...` with Bearer JWT.\n\n**Technical specs (AI agents):** Detail endpoints may return `technicalValues` with **values only** (`fieldId` + text/option ids — **no labels**). To render label→value technical details, always call the matching `.../technical-info` endpoint and join `form.fields[].id` to `values[].fieldId`. There is no public `product-category-variation-fields` route.\n\n**Docs:** https://api.meshkee.com/docs/website" }, "servers": [ { @@ -34,10 +34,12 @@ "name": "Categories" }, { - "name": "Products" + "name": "Products", + "description": "Catalog products. For a specs/technical-details table on the product page, call `GET .../products/{slug}/technical-info` (or by-id). Do not rely on detail-only payloads for field labels." }, { - "name": "User Products" + "name": "User Products", + "description": "Customer marketplace listings. Detail returns `technicalValues` without labels; use `GET .../user-products/{slug}/technical-info` for form field labels + values." }, { "name": "Store" @@ -1003,6 +1005,7 @@ "Products" ], "summary": "Product technical info by id", + "description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Use this for the product specs / technical-details UI** — join `form.fields[].id` ↔ `values[].fieldId`. Product detail alone does not include field labels.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1018,7 +1021,7 @@ ], "responses": { "200": { - "description": "Technical form + values" + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }. Render label→value; hide section if form/fields/values empty." } } } @@ -1080,7 +1083,8 @@ "tags": [ "Products" ], - "summary": "Product technical specs", + "summary": "Product technical specs (labels + values)", + "description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Required for specs UI.** Join `form.fields[].id` ↔ `values[].fieldId`. Do not invent a separate category-fields public endpoint.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1096,7 +1100,7 @@ ], "responses": { "200": { - "description": "{ form, values }" + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }" } } } @@ -1199,7 +1203,7 @@ "User Products" ], "summary": "User product details by slug", - "description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), technical field values, delivery/technical notes.", + "description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), delivery/technical notes, and `technicalValues`.\n\n**Important:** `technicalValues` items are **values only** (`fieldId` + `textValue` / `optionId` / `optionIds`). They do **not** include field labels (`label`, `fieldName`, etc.). For a label→value technical-details table, call `GET /tenants/{domain}/user-products/{slug}/technical-info` and join on `fieldId`.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1215,7 +1219,7 @@ ], "responses": { "200": { - "description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues, countryId, cityId, countrySlug" + "description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues (values only — no labels), countryId, cityId, countrySlug" }, "404": { "description": "Not found or not published" @@ -1228,8 +1232,8 @@ "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).", + "summary": "User product technical form + values (labels)", + "description": "**Use this for the listing specs / technical-details UI.** Returns the category technical form (field `id`, `label`, `key`, `type`, `sortOrder`, `options`) plus the listing’s submitted `values`.\n\nJoin `form.fields[].id` ↔ `values[].fieldId` and render label→value. Sort by `sortOrder`. Hide the section if `form`/fields/`values` are empty.\n\nThere is **no** public `product-category-variation-fields` (or similar) endpoint — use this path only.", "parameters": [ { "$ref": "#/components/parameters/domain" @@ -1245,7 +1249,7 @@ ], "responses": { "200": { - "description": "{ form, values }" + "description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }" } } }