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:
Alireza Hassani
2026-08-10 00:19:13 +03:30
co-authored by Cursor
parent 158523df7b
commit 953b87b616
32 changed files with 3326 additions and 138 deletions
+204 -99
View File
@@ -14,16 +14,17 @@ model User {
email String? @db.VarChar(255)
firstName String? @map("first_name") @db.VarChar(100)
lastName String? @map("last_name") @db.VarChar(100)
firstNameEn String? @map("first_name_en") @db.VarChar(100)
lastNameEn String? @map("last_name_en") @db.VarChar(100)
isActive Boolean @default(true) @map("is_active")
cellVerifiedAt DateTime? @map("cell_verified_at") @db.Timestamptz(6)
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(6)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
profile Json @default("{}")
firstNameEn String? @map("first_name_en") @db.VarChar(100)
lastNameEn String? @map("last_name_en") @db.VarChar(100)
oldId BigInt? @map("old_id")
addresses Address[]
authoredBlogs blogs[] @relation("BlogAuthor")
businessCustomers BusinessCustomer[]
businessUsersInvited BusinessUser[] @relation("BusinessInviter")
businessUsers BusinessUser[] @relation("BusinessMember")
@@ -35,72 +36,74 @@ model User {
mediaUploaded Media[]
ordersCreated Order[] @relation("OrderCreator")
orders Order[] @relation("OrderCustomer")
authoredPortfolios portfolios[] @relation("PortfolioAuthor")
shoppingCardsCreated ShoppingCard[] @relation("ShoppingCardCreator")
shoppingCards ShoppingCard[] @relation("ShoppingCardCustomer")
transactionsCreated Transaction[] @relation("TransactionCreator")
transactions Transaction[] @relation("TransactionCustomer")
userProducts UserProduct[]
userRoles UserRole[]
authoredBlogs blogs[] @relation("BlogAuthor")
authoredPortfolios portfolios[] @relation("PortfolioAuthor")
@@index([cellNumber], map: "idx_users_cell_number")
@@map("users")
}
model Business {
id BigInt @id @default(autoincrement())
name String @db.VarChar(255)
slug String @unique(map: "businesses_slug_unique") @db.VarChar(100)
description String?
settings Json @default("{}")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
nameFa String? @map("name_fa") @db.VarChar(255)
about String?
vision String?
emails Json @default("[]")
phoneNumbers Json @default("[]") @map("phone_numbers")
socialMedia Json @default("{}") @map("social_media")
logoMediaId BigInt? @map("logo_media_id")
faviconMediaId BigInt? @map("favicon_media_id")
oldBusinessId BigInt? @map("old_business_id")
addresses Address[]
blogs blogs[]
brands Brand[]
categoryAssignments BusinessCategoryAssignment[]
businessCustomers BusinessCustomer[]
businessUsers BusinessUser[]
faviconMedia Media? @relation("BusinessFavicon", fields: [faviconMediaId], references: [id], onUpdate: NoAction)
logoMedia Media? @relation("BusinessLogo", fields: [logoMediaId], references: [id], onUpdate: NoAction)
carts Cart[]
categories Category[]
contentCategoryAssignments CategoryAssignment[]
categoryTechnicalForms CategoryTechnicalForm[]
categoryVariations CategoryVariation[]
comments Comment[]
contactSubmissions ContactSubmission[]
domains Domain[]
expertReviews ExpertReview[]
favorites Favorite[]
invoiceItemTemplates InvoiceItemTemplate[]
invoiceTemplates InvoiceTemplate[]
invoices Invoice[] @relation("InvoiceBusiness")
invoicesIssued Invoice[] @relation("InvoiceIssuerBusiness")
media Media[]
mediaAttachments MediaAttachment[]
orders Order[]
portfolios portfolios[]
productTechnicalFieldValues ProductTechnicalFieldValue[]
products Product[]
shoppingCards ShoppingCard[]
storeItemVariants StoreItemVariant[]
storeItems StoreItem[]
storeSpecials StoreSpecial[]
transactions Transaction[]
website_brand_groups website_brand_groups[]
website_category_groups website_category_groups[]
website_sliders website_sliders[]
id BigInt @id @default(autoincrement())
name String @db.VarChar(255)
slug String @unique(map: "businesses_slug_unique") @db.VarChar(100)
description String?
settings Json @default("{}")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
nameFa String? @map("name_fa") @db.VarChar(255)
about String?
vision String?
emails Json @default("[]")
phoneNumbers Json @default("[]") @map("phone_numbers")
socialMedia Json @default("{}") @map("social_media")
logoMediaId BigInt? @map("logo_media_id")
faviconMediaId BigInt? @map("favicon_media_id")
oldBusinessId BigInt? @map("old_business_id")
addresses Address[]
blogs blogs[]
brands Brand[]
categoryAssignments BusinessCategoryAssignment[]
businessCustomers BusinessCustomer[]
businessUsers BusinessUser[]
faviconMedia Media? @relation("BusinessFavicon", fields: [faviconMediaId], references: [id], onUpdate: NoAction)
logoMedia Media? @relation("BusinessLogo", fields: [logoMediaId], references: [id], onUpdate: NoAction)
carts Cart[]
categories Category[]
contentCategoryAssignments CategoryAssignment[]
categoryTechnicalForms CategoryTechnicalForm[]
categoryVariations CategoryVariation[]
comments Comment[]
contactSubmissions ContactSubmission[]
domains Domain[]
expertReviews ExpertReview[]
favorites Favorite[]
invoiceItemTemplates InvoiceItemTemplate[]
invoiceTemplates InvoiceTemplate[]
invoices Invoice[] @relation("InvoiceBusiness")
invoicesIssued Invoice[] @relation("InvoiceIssuerBusiness")
media Media[]
mediaAttachments MediaAttachment[]
orders Order[]
portfolios portfolios[]
productTechnicalFieldValues ProductTechnicalFieldValue[]
products Product[]
shoppingCards ShoppingCard[]
storeItemVariants StoreItemVariant[]
storeItems StoreItem[]
storeSpecials StoreSpecial[]
transactions Transaction[]
userProductTechnicalFieldValues UserProductTechnicalFieldValue[]
userProducts UserProduct[]
website_brand_groups website_brand_groups[]
website_category_groups website_category_groups[]
website_sliders website_sliders[]
@@map("businesses")
}
@@ -280,6 +283,7 @@ model Media {
attachments MediaAttachment[]
portfolios portfolios[]
featuredProducts Product[] @relation("ProductFeaturedMedia")
featuredUserProducts UserProduct[] @relation("UserProductFeaturedMedia")
website_slider_slides website_slider_slides[]
@@index([businessId], map: "idx_media_business_id")
@@ -306,7 +310,7 @@ model MediaAttachment {
@@map("media_attachments")
}
model Category {
model Category {
id BigInt @id @default(autoincrement())
businessId BigInt @map("business_id")
entityType MediaEntityType @map("entity_type")
@@ -385,6 +389,7 @@ model Brand {
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
imageMedia Media? @relation(fields: [imageMediaId], references: [id], onUpdate: NoAction)
products Product[]
userProducts UserProduct[]
website_brand_group_items website_brand_group_items[]
@@unique([businessId, slug], map: "brands_business_slug_unique")
@@ -477,18 +482,19 @@ model CategoryTechnicalForm {
}
model CategoryTechnicalFormField {
id BigInt @id @default(autoincrement())
formId BigInt @map("form_id")
label String @db.VarChar(255)
fieldKey String @map("field_key") @db.VarChar(255)
fieldType TechnicalFieldType @map("field_type")
isRequired Boolean @default(false) @map("is_required")
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
options CategoryTechnicalFormFieldOption[]
form CategoryTechnicalForm @relation(fields: [formId], references: [id], onDelete: Cascade, onUpdate: NoAction)
values ProductTechnicalFieldValue[]
id BigInt @id @default(autoincrement())
formId BigInt @map("form_id")
label String @db.VarChar(255)
fieldKey String @map("field_key") @db.VarChar(255)
fieldType TechnicalFieldType @map("field_type")
isRequired Boolean @default(false) @map("is_required")
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
options CategoryTechnicalFormFieldOption[]
form CategoryTechnicalForm @relation(fields: [formId], references: [id], onDelete: Cascade, onUpdate: NoAction)
values ProductTechnicalFieldValue[]
userProductFieldValues UserProductTechnicalFieldValue[]
@@unique([formId, fieldKey], map: "category_technical_form_fields_unique_key")
@@index([formId], map: "idx_category_technical_form_fields_form_id")
@@ -496,15 +502,17 @@ model CategoryTechnicalFormField {
}
model CategoryTechnicalFormFieldOption {
id BigInt @id @default(autoincrement())
fieldId BigInt @map("field_id")
label String @db.VarChar(255)
value String @db.VarChar(255)
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
field CategoryTechnicalFormField @relation(fields: [fieldId], references: [id], onDelete: Cascade, onUpdate: NoAction)
multiSelectValues ProductTechnicalFieldValueOption[]
selectedValues ProductTechnicalFieldValue[]
id BigInt @id @default(autoincrement())
fieldId BigInt @map("field_id")
label String @db.VarChar(255)
value String @db.VarChar(255)
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
field CategoryTechnicalFormField @relation(fields: [fieldId], references: [id], onDelete: Cascade, onUpdate: NoAction)
multiSelectValues ProductTechnicalFieldValueOption[]
selectedValues ProductTechnicalFieldValue[]
userProductMultiSelectValues UserProductTechnicalFieldValueOption[]
userProductSelectedValues UserProductTechnicalFieldValue[]
@@unique([fieldId, value], map: "category_technical_form_field_options_unique_value")
@@index([fieldId], map: "idx_category_technical_form_field_options_field_id")
@@ -634,10 +642,7 @@ model blogs {
model portfolios {
id BigInt @id @default(autoincrement())
business_id BigInt
author_id BigInt?
title String @db.VarChar(255)
title_fa String? @db.VarChar(255)
title_en String? @db.VarChar(255)
slug String @db.VarChar(255)
description String?
content Json @default("{}")
@@ -651,7 +656,10 @@ model portfolios {
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
old_id BigInt?
author User? @relation("PortfolioAuthor", fields: [author_id], references: [id], onUpdate: NoAction, onDelete: SetNull)
title_fa String? @db.VarChar(255)
title_en String? @db.VarChar(255)
author_id BigInt?
author User? @relation("PortfolioAuthor", fields: [author_id], references: [id], onUpdate: NoAction)
businesses Business @relation(fields: [business_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
media Media? @relation(fields: [featured_media_id], references: [id], onUpdate: NoAction)
@@ -684,19 +692,22 @@ model Address {
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model City {
id BigInt @id @default(autoincrement())
parentId BigInt? @map("parent_id")
level CityLevel
nameFa String @map("name_fa") @db.VarChar(255)
nameEn String @map("name_en") @db.VarChar(255)
landlineCode String? @map("landline_code") @db.VarChar(10)
slug String @unique(map: "cities_slug_unique") @db.VarChar(100)
sortOrder Int @default(0) @map("sort_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
parent City? @relation("CityTree", fields: [parentId], references: [id], onDelete: Cascade, onUpdate: NoAction)
children City[] @relation("CityTree")
id BigInt @id @default(autoincrement())
parentId BigInt? @map("parent_id")
level CityLevel
nameFa String @map("name_fa") @db.VarChar(255)
nameEn String @map("name_en") @db.VarChar(255)
landlineCode String? @map("landline_code") @db.VarChar(10)
slug String @unique(map: "cities_slug_unique") @db.VarChar(100)
sortOrder Int @default(0) @map("sort_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
parent City? @relation("CityTree", fields: [parentId], references: [id], onDelete: Cascade, onUpdate: NoAction)
children City[] @relation("CityTree")
userProductsAsCity UserProduct[] @relation("UserProductCity")
userProductsAsCountry UserProduct[] @relation("UserProductCountry")
userProductsAsDistrict UserProduct[] @relation("UserProductDistrict")
@@index([level], map: "idx_cities_level")
@@index([level, parentId, sortOrder], map: "idx_cities_level_parent")
@@ -959,11 +970,11 @@ model StoreSpecial {
id BigInt @id @default(autoincrement())
businessId BigInt @map("business_id")
title String @db.VarChar(255)
key String @db.VarChar(100)
sortOrder Int @default(0) @map("sort_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
key String @db.VarChar(100)
items StoreSpecialItem[]
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@ -989,11 +1000,11 @@ model website_brand_groups {
id BigInt @id @default(autoincrement())
business_id BigInt
title String @db.VarChar(255)
key String @db.VarChar(100)
sort_order Int @default(0)
is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
key String @db.VarChar(100)
website_brand_group_items website_brand_group_items[]
businesses Business @relation(fields: [business_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@ -1018,11 +1029,11 @@ model website_category_groups {
id BigInt @id @default(autoincrement())
business_id BigInt
title String @db.VarChar(255)
key String @db.VarChar(100)
sort_order Int @default(0)
is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
key String @db.VarChar(100)
website_category_group_items website_category_group_items[]
businesses Business @relation(fields: [business_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@ -1230,6 +1241,88 @@ model InvoiceAccount {
@@map("invoice_accounts")
}
model UserProductTechnicalFieldValueOption {
fieldValueId BigInt @map("field_value_id")
optionId BigInt @map("option_id")
fieldValue UserProductTechnicalFieldValue @relation(fields: [fieldValueId], references: [id], onDelete: Cascade, onUpdate: NoAction)
option CategoryTechnicalFormFieldOption @relation(fields: [optionId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@id([fieldValueId, optionId])
@@index([optionId], map: "idx_user_product_technical_field_value_options_option_id")
@@map("user_product_technical_field_value_options")
}
model UserProductTechnicalFieldValue {
id BigInt @id @default(autoincrement())
businessId BigInt @map("business_id")
userProductId BigInt @map("user_product_id")
fieldId BigInt @map("field_id")
textValue String? @map("text_value")
optionId BigInt? @map("option_id")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
selectedOptions UserProductTechnicalFieldValueOption[]
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
field CategoryTechnicalFormField @relation(fields: [fieldId], references: [id], onDelete: Cascade, onUpdate: NoAction)
option CategoryTechnicalFormFieldOption? @relation(fields: [optionId], references: [id], onUpdate: NoAction)
userProduct UserProduct @relation(fields: [userProductId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@unique([userProductId, fieldId], map: "user_product_technical_field_values_unique")
@@index([businessId], map: "idx_user_product_technical_field_values_business_id")
@@index([userProductId], map: "idx_user_product_technical_field_values_user_product_id")
@@map("user_product_technical_field_values")
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model UserProduct {
id BigInt @id @default(autoincrement())
businessId BigInt @map("business_id")
userId BigInt @map("user_id")
title String @db.VarChar(255)
slug String @db.VarChar(255)
description String?
content Json @default("{}")
price Decimal? @db.Decimal(12, 2)
compareAtPrice Decimal? @map("compare_at_price") @db.Decimal(12, 2)
sku String? @db.VarChar(100)
stockQuantity Int? @map("stock_quantity")
status ContentStatus @default(draft)
featuredMediaId BigInt? @map("featured_media_id")
brandId BigInt? @map("brand_id")
countryId BigInt @map("country_id")
cityId BigInt @map("city_id")
districtId BigInt? @map("district_id")
sortOrder Int @default(0) @map("sort_order")
publishedAt DateTime? @map("published_at") @db.Timestamptz(6)
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
priceCurrency String @default("IRT") @map("price_currency") @db.VarChar(8)
deliveryNote String? @map("delivery_note")
condition UserProductCondition @default(new)
technicalNotes String? @map("technical_notes")
technicalFieldValues UserProductTechnicalFieldValue[]
brand Brand? @relation(fields: [brandId], references: [id], onUpdate: NoAction)
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
city City @relation("UserProductCity", fields: [cityId], references: [id], onUpdate: NoAction)
country City @relation("UserProductCountry", fields: [countryId], references: [id], onUpdate: NoAction)
district City? @relation("UserProductDistrict", fields: [districtId], references: [id], onUpdate: NoAction)
featuredMedia Media? @relation("UserProductFeaturedMedia", fields: [featuredMediaId], references: [id], onUpdate: NoAction)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@unique([businessId, slug], map: "user_products_business_slug_unique")
@@index([brandId], map: "idx_user_products_brand_id")
@@index([businessId], map: "idx_user_products_business_id")
@@index([businessId, publishedAt(sort: Desc)], map: "idx_user_products_business_published")
@@index([businessId, status], map: "idx_user_products_business_status")
@@index([businessId, userId], map: "idx_user_products_business_user")
@@index([cityId], map: "idx_user_products_city_id")
@@index([countryId], map: "idx_user_products_country_id")
@@index([districtId], map: "idx_user_products_district_id")
@@index([userId], map: "idx_user_products_user_id")
@@map("user_products")
}
enum MediaType {
image
video
@@ -1242,6 +1335,7 @@ enum MediaEntityType {
blog
portfolio
customer
user_product
@@map("media_entity_type")
}
@@ -1258,6 +1352,7 @@ enum ContentStatus {
draft
published
archived
rejected
@@map("content_status")
}
@@ -1283,6 +1378,7 @@ enum CityLevel {
country
province
city
district
@@map("city_level")
}
@@ -1339,3 +1435,12 @@ enum InvoiceStatus {
@@map("invoice_status")
}
enum UserProductCondition {
new
stock
needs_repair
scrap
@@map("user_product_condition")
}