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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
3d3abeb65b
commit
fb04425df7
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,16 @@
|
||||
<code>/tenants/{domain}/user-products</code> (list / details / technical-info).
|
||||
See OpenAPI tag <strong>User Products</strong>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Technical details:</strong> detail responses include
|
||||
<code>technicalValues</code> with <em>values only</em> (no field labels).
|
||||
For a label→value specs table, call
|
||||
<code>GET /tenants/{domain}/user-products/{slug}/technical-info</code>
|
||||
(catalog products:
|
||||
<code>.../products/{slug}/technical-info</code>) and join
|
||||
<code>form.fields[].id</code> ↔ <code>values[].fieldId</code>.
|
||||
There is no public <code>product-category-variation-fields</code> route.
|
||||
</p>
|
||||
|
||||
<h2>For a new website AI / designer</h2>
|
||||
<ol>
|
||||
|
||||
@@ -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? }] }"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
+103
-28
@@ -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<string, (typeof COLOR_PRESETS)[number]['name']> = {
|
||||
const COLOR_ALIASES: Record<string, ColorPresetName> = {
|
||||
قرمز: '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);
|
||||
|
||||
@@ -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() }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,16 @@
|
||||
<code>/tenants/{domain}/user-products</code> (list / details / technical-info).
|
||||
See OpenAPI tag <strong>User Products</strong>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Technical details:</strong> detail responses include
|
||||
<code>technicalValues</code> with <em>values only</em> (no field labels).
|
||||
For a label→value specs table, call
|
||||
<code>GET /tenants/{domain}/user-products/{slug}/technical-info</code>
|
||||
(catalog products:
|
||||
<code>.../products/{slug}/technical-info</code>) and join
|
||||
<code>form.fields[].id</code> ↔ <code>values[].fieldId</code>.
|
||||
There is no public <code>product-category-variation-fields</code> route.
|
||||
</p>
|
||||
|
||||
<h2>For a new website AI / designer</h2>
|
||||
<ol>
|
||||
|
||||
@@ -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? }] }"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user