feat(backend): implement Phase A database models and controllers for cross-boundary dependencies
Some checks failed
Deploy Canina / deploy (push) Failing after 28s
Some checks failed
Deploy Canina / deploy (push) Failing after 28s
This commit is contained in:
parent
74fcaa6a4e
commit
1ae4c99ffc
@ -9,23 +9,25 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
firstName String @map("first_name") @db.VarChar(100)
|
firstName String @map("first_name") @db.VarChar(100)
|
||||||
lastName String @map("last_name") @db.VarChar(100)
|
lastName String @map("last_name") @db.VarChar(100)
|
||||||
email String? @unique @db.VarChar(150)
|
email String? @unique @db.VarChar(150)
|
||||||
mobile String @unique @db.VarChar(15)
|
mobile String @unique @db.VarChar(15)
|
||||||
password String? @db.VarChar(255)
|
password String? @db.VarChar(255)
|
||||||
role String @default("User_PetOwner") @db.VarChar(30)
|
role String @default("User_PetOwner") @db.VarChar(30)
|
||||||
walletBalance Decimal @default(0.00) @map("wallet_balance") @db.Decimal(15, 2)
|
walletBalance Decimal @default(0.00) @map("wallet_balance") @db.Decimal(15, 2)
|
||||||
charityDonationTotal Decimal @default(0.00) @map("charity_donation_total") @db.Decimal(15, 2)
|
charityDonationTotal Decimal @default(0.00) @map("charity_donation_total") @db.Decimal(15, 2)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz()
|
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
addresses UserAddress[]
|
addresses UserAddress[]
|
||||||
walletTransactions WalletTransaction[]
|
walletTransactions WalletTransaction[]
|
||||||
pets Pet[]
|
pets Pet[]
|
||||||
orders Order[]
|
orders Order[]
|
||||||
blogs Blog[]
|
blogs Blog[]
|
||||||
|
prescriptions Prescription[]
|
||||||
|
partnerAccount PartnerAccount?
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
@ -43,7 +45,7 @@ model UserAddress {
|
|||||||
isDefault Boolean @default(false) @map("is_default")
|
isDefault Boolean @default(false) @map("is_default")
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@map("user_addresses")
|
@@map("user_addresses")
|
||||||
@ -59,7 +61,7 @@ model WalletTransaction {
|
|||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@map("wallet_transactions")
|
@@map("wallet_transactions")
|
||||||
}
|
}
|
||||||
@ -91,9 +93,9 @@ model Category {
|
|||||||
imageUrl String? @map("image_url") @db.Text
|
imageUrl String? @map("image_url") @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
parent Category? @relation("SubCategories", fields: [parentId], references: [id])
|
parent Category? @relation("SubCategories", fields: [parentId], references: [id])
|
||||||
children Category[] @relation("SubCategories")
|
children Category[] @relation("SubCategories")
|
||||||
products Product[]
|
products Product[]
|
||||||
|
|
||||||
@@map("categories")
|
@@map("categories")
|
||||||
}
|
}
|
||||||
@ -112,7 +114,7 @@ model Blog {
|
|||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz()
|
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
author User @relation(fields: [authorId], references: [id])
|
author User @relation(fields: [authorId], references: [id])
|
||||||
|
|
||||||
@@map("blogs")
|
@@map("blogs")
|
||||||
}
|
}
|
||||||
@ -139,6 +141,7 @@ model Product {
|
|||||||
unit String @db.VarChar(50)
|
unit String @db.VarChar(50)
|
||||||
packageSize Decimal @map("package_size") @db.Decimal(10, 2)
|
packageSize Decimal @map("package_size") @db.Decimal(10, 2)
|
||||||
dosageLogic String? @map("dosage_logic") @db.Text
|
dosageLogic String? @map("dosage_logic") @db.Text
|
||||||
|
dosageConfig Json? @map("dosage_config")
|
||||||
suitableFor String @map("suitable_for") @db.VarChar(15) // سگ, گربه, هر دو
|
suitableFor String @map("suitable_for") @db.VarChar(15) // سگ, گربه, هر دو
|
||||||
imageUrl String @map("image_url") @db.Text
|
imageUrl String @map("image_url") @db.Text
|
||||||
images String[] @default([])
|
images String[] @default([])
|
||||||
@ -151,12 +154,12 @@ model Product {
|
|||||||
keywords String? @db.Text
|
keywords String? @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
category Category @relation(fields: [categoryId], references: [id])
|
||||||
ingredientList ProductIngredient[]
|
ingredientList ProductIngredient[]
|
||||||
symptoms ProductSymptom[]
|
symptoms ProductSymptom[]
|
||||||
reminders Reminder[]
|
reminders Reminder[]
|
||||||
orderItems OrderItem[]
|
orderItems OrderItem[]
|
||||||
advisorRules SmartAdvisorRule[]
|
advisorRules SmartAdvisorRule[]
|
||||||
|
|
||||||
@@index([categorySlug])
|
@@index([categorySlug])
|
||||||
@@index([suitableFor])
|
@@index([suitableFor])
|
||||||
@ -197,21 +200,22 @@ model ProductSymptom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Pet {
|
model Pet {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
type String @db.VarChar(10) // سگ, گربه
|
type String @db.VarChar(10) // سگ, گربه
|
||||||
breed String @db.VarChar(100)
|
breed String @db.VarChar(100)
|
||||||
age Int
|
age Int
|
||||||
weight Decimal @db.Decimal(5, 2)
|
weight Decimal @db.Decimal(5, 2)
|
||||||
activityLevel String @map("activity_level") @db.VarChar(15) // کم, متوسط, زیاد
|
activityLevel String @map("activity_level") @db.VarChar(15) // کم, متوسط, زیاد
|
||||||
imageUrl String? @map("image_url") @db.Text
|
imageUrl String? @map("image_url") @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
medicalConditions PetMedicalCondition[]
|
medicalConditions PetMedicalCondition[]
|
||||||
reminders Reminder[]
|
reminders Reminder[]
|
||||||
healthLogs HealthLog[]
|
healthLogs HealthLog[]
|
||||||
|
prescriptions Prescription[]
|
||||||
|
|
||||||
@@map("pets")
|
@@map("pets")
|
||||||
}
|
}
|
||||||
@ -226,13 +230,13 @@ model PetMedicalCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Reminder {
|
model Reminder {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
petId String @map("pet_id") @db.Uuid
|
petId String @map("pet_id") @db.Uuid
|
||||||
productId String? @map("product_id") @db.Uuid
|
productId String? @map("product_id") @db.Uuid
|
||||||
title String @db.VarChar(150)
|
title String @db.VarChar(150)
|
||||||
time String @db.VarChar(5) // 08:30
|
time String @db.VarChar(5) // 08:30
|
||||||
frequency String @db.VarChar(20) // روزانه, هفتگی
|
frequency String @db.VarChar(20) // روزانه, هفتگی
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
||||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||||
@ -248,7 +252,7 @@ model ReminderCompletion {
|
|||||||
completedDate DateTime @map("completed_date") @db.Date
|
completedDate DateTime @map("completed_date") @db.Date
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
reminder Reminder @relation(fields: [reminderId], references: [id], onDelete: Cascade)
|
reminder Reminder @relation(fields: [reminderId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([reminderId, completedDate])
|
@@unique([reminderId, completedDate])
|
||||||
@@index([reminderId, completedDate])
|
@@index([reminderId, completedDate])
|
||||||
@ -256,68 +260,68 @@ model ReminderCompletion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model HealthLog {
|
model HealthLog {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
petId String @map("pet_id") @db.Uuid
|
petId String @map("pet_id") @db.Uuid
|
||||||
appetite String @db.VarChar(15)
|
appetite String @db.VarChar(15)
|
||||||
energy String @db.VarChar(15)
|
energy String @db.VarChar(15)
|
||||||
digestion String @db.VarChar(15)
|
digestion String @db.VarChar(15)
|
||||||
note String? @db.Text
|
note String? @db.Text
|
||||||
loggedDate DateTime @default(now()) @map("logged_date") @db.Date
|
loggedDate DateTime @default(now()) @map("logged_date") @db.Date
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@map("health_logs")
|
@@map("health_logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Coupon {
|
model Coupon {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
code String @unique @db.VarChar(50)
|
code String @unique @db.VarChar(50)
|
||||||
type String @default("percent") @db.VarChar(20) // fixed, percent
|
type String @default("percent") @db.VarChar(20) // fixed, percent
|
||||||
value Decimal @db.Decimal(15, 2)
|
value Decimal @db.Decimal(15, 2)
|
||||||
minCartValue Decimal? @map("min_cart_value") @db.Decimal(15, 2)
|
minCartValue Decimal? @map("min_cart_value") @db.Decimal(15, 2)
|
||||||
maxCartValue Decimal? @map("max_cart_value") @db.Decimal(15, 2)
|
maxCartValue Decimal? @map("max_cart_value") @db.Decimal(15, 2)
|
||||||
maxUses Int? @map("max_uses")
|
maxUses Int? @map("max_uses")
|
||||||
usedCount Int @default(0) @map("used_count")
|
usedCount Int @default(0) @map("used_count")
|
||||||
expiresAt DateTime? @map("expires_at") @db.Timestamptz()
|
expiresAt DateTime? @map("expires_at") @db.Timestamptz()
|
||||||
isActive Boolean @default(true) @map("is_active")
|
isActive Boolean @default(true) @map("is_active")
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
orders Order[]
|
orders Order[]
|
||||||
targets CouponTarget[]
|
targets CouponTarget[]
|
||||||
|
|
||||||
@@map("coupons")
|
@@map("coupons")
|
||||||
}
|
}
|
||||||
|
|
||||||
model CouponTarget {
|
model CouponTarget {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
couponId String @map("coupon_id") @db.Uuid
|
couponId String @map("coupon_id") @db.Uuid
|
||||||
targetType String @map("target_type") @db.VarChar(50) // USER, PET, ROLE, PRODUCT, CATEGORY
|
targetType String @map("target_type") @db.VarChar(50) // USER, PET, ROLE, PRODUCT, CATEGORY
|
||||||
targetId String @map("target_id") @db.VarChar(100) // UUID or role name
|
targetId String @map("target_id") @db.VarChar(100) // UUID or role name
|
||||||
modifierType String @default("override") @map("modifier_type") @db.VarChar(20) // override, add, subtract
|
modifierType String @default("override") @map("modifier_type") @db.VarChar(20) // override, add, subtract
|
||||||
modifierValue Decimal? @map("modifier_value") @db.Decimal(15, 2)
|
modifierValue Decimal? @map("modifier_value") @db.Decimal(15, 2)
|
||||||
|
|
||||||
coupon Coupon @relation(fields: [couponId], references: [id], onDelete: Cascade)
|
coupon Coupon @relation(fields: [couponId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([targetType, targetId])
|
@@index([targetType, targetId])
|
||||||
@@map("coupon_targets")
|
@@map("coupon_targets")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Order {
|
model Order {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
couponId String? @map("coupon_id") @db.Uuid
|
couponId String? @map("coupon_id") @db.Uuid
|
||||||
totalAmount Decimal @map("total_amount") @db.Decimal(15, 2)
|
totalAmount Decimal @map("total_amount") @db.Decimal(15, 2)
|
||||||
charityDonation Decimal @default(0.00) @map("charity_donation") @db.Decimal(15, 2)
|
charityDonation Decimal @default(0.00) @map("charity_donation") @db.Decimal(15, 2)
|
||||||
isRefill Boolean @default(false) @map("is_refill")
|
isRefill Boolean @default(false) @map("is_refill")
|
||||||
refillIntervalDays Int? @map("refill_interval_days")
|
refillIntervalDays Int? @map("refill_interval_days")
|
||||||
status String @default("processing") @db.VarChar(30) // processing, shipped, delivered
|
status String @default("processing") @db.VarChar(30) // processing, shipped, delivered
|
||||||
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
|
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
coupon Coupon? @relation(fields: [couponId], references: [id])
|
coupon Coupon? @relation(fields: [couponId], references: [id])
|
||||||
orderItems OrderItem[]
|
orderItems OrderItem[]
|
||||||
|
|
||||||
@@map("orders")
|
@@map("orders")
|
||||||
}
|
}
|
||||||
@ -330,8 +334,8 @@ model OrderItem {
|
|||||||
doseQty Decimal? @map("dose_qty") @db.Decimal(10, 2)
|
doseQty Decimal? @map("dose_qty") @db.Decimal(10, 2)
|
||||||
doseUnit String? @map("dose_unit") @db.VarChar(50)
|
doseUnit String? @map("dose_unit") @db.VarChar(50)
|
||||||
|
|
||||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
@@map("order_items")
|
@@map("order_items")
|
||||||
}
|
}
|
||||||
@ -344,8 +348,8 @@ model UiText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ScientificTerm {
|
model ScientificTerm {
|
||||||
key String @id @db.VarChar(100)
|
key String @id @db.VarChar(100)
|
||||||
term String @db.VarChar(150)
|
term String @db.VarChar(150)
|
||||||
definition String @db.Text
|
definition String @db.Text
|
||||||
wikiId String @map("wiki_id") @db.VarChar(50)
|
wikiId String @map("wiki_id") @db.VarChar(50)
|
||||||
metaTitle String? @map("meta_title") @db.VarChar(200)
|
metaTitle String? @map("meta_title") @db.VarChar(200)
|
||||||
@ -356,29 +360,29 @@ model ScientificTerm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model HeroBanner {
|
model HeroBanner {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
title String @db.VarChar(200)
|
title String @db.VarChar(200)
|
||||||
subtitle String? @db.Text
|
subtitle String? @db.Text
|
||||||
imageUrl String @map("image_url") @db.Text
|
imageUrl String @map("image_url") @db.Text
|
||||||
buttonText String? @map("button_text") @db.VarChar(100)
|
buttonText String? @map("button_text") @db.VarChar(100)
|
||||||
buttonLink String? @map("button_link") @db.Text
|
buttonLink String? @map("button_link") @db.Text
|
||||||
isActive Boolean @default(true) @map("is_active")
|
isActive Boolean @default(true) @map("is_active")
|
||||||
order Int @default(0)
|
order Int @default(0)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
@@map("hero_banners")
|
@@map("hero_banners")
|
||||||
}
|
}
|
||||||
|
|
||||||
model VetTestimonial {
|
model VetTestimonial {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
vetName String @map("vet_name") @db.VarChar(150)
|
vetName String @map("vet_name") @db.VarChar(150)
|
||||||
clinicName String? @map("clinic_name") @db.VarChar(150)
|
clinicName String? @map("clinic_name") @db.VarChar(150)
|
||||||
imageUrl String? @map("image_url") @db.Text
|
imageUrl String? @map("image_url") @db.Text
|
||||||
quote String @db.Text
|
quote String @db.Text
|
||||||
rating Int @default(5)
|
rating Int @default(5)
|
||||||
isActive Boolean @default(true) @map("is_active")
|
isActive Boolean @default(true) @map("is_active")
|
||||||
order Int @default(0)
|
order Int @default(0)
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
@@map("vet_testimonials")
|
@@map("vet_testimonials")
|
||||||
}
|
}
|
||||||
@ -391,7 +395,7 @@ model SmartAdvisorRule {
|
|||||||
reason String @db.Text
|
reason String @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
|
||||||
product Product @relation(fields: [recommendedProduct], references: [id], onDelete: Cascade)
|
product Product @relation(fields: [recommendedProduct], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@map("smart_advisor_rules")
|
@@map("smart_advisor_rules")
|
||||||
}
|
}
|
||||||
@ -427,14 +431,122 @@ model ContactSubmission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ContactInfo {
|
model ContactInfo {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
key String @unique @db.VarChar(100)
|
key String @unique @db.VarChar(100)
|
||||||
title String @db.VarChar(200)
|
title String @db.VarChar(200)
|
||||||
value String @db.Text
|
value String @db.Text
|
||||||
icon String? @db.VarChar(100)
|
icon String? @db.VarChar(100)
|
||||||
order Int @default(0)
|
order Int @default(0)
|
||||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
@@map("contact_info")
|
@@map("contact_info")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Banner {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
title String @db.VarChar(200)
|
||||||
|
subtitle String? @db.Text
|
||||||
|
imageUrl String @map("image_url") @db.Text
|
||||||
|
linkUrl String? @map("link_url") @db.Text
|
||||||
|
position String @default("home_hero") @db.VarChar(50)
|
||||||
|
isActive Boolean @default(true) @map("is_active")
|
||||||
|
order Int @default(0)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
@@map("banners")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Testimonial {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
authorName String @map("author_name") @db.VarChar(150)
|
||||||
|
roleTitle String? @map("role_title") @db.VarChar(150)
|
||||||
|
avatarUrl String? @map("avatar_url") @db.Text
|
||||||
|
content String @db.Text
|
||||||
|
rating Int @default(5)
|
||||||
|
isFeatured Boolean @default(false) @map("is_featured")
|
||||||
|
isActive Boolean @default(true) @map("is_active")
|
||||||
|
order Int @default(0)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
@@map("testimonials")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Ingredient {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
nameFa String @map("name_fa") @db.VarChar(150)
|
||||||
|
nameEn String @map("name_en") @db.VarChar(150)
|
||||||
|
slug String @unique @db.VarChar(150)
|
||||||
|
description String? @db.Text
|
||||||
|
scientificName String? @map("scientific_name") @db.VarChar(200)
|
||||||
|
imageUrl String? @map("image_url") @db.Text
|
||||||
|
benefits String[] @default([])
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
@@map("ingredients")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Prescription {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
userId String @map("user_id") @db.Uuid
|
||||||
|
petId String? @map("pet_id") @db.Uuid
|
||||||
|
fileUrl String @map("file_url") @db.Text
|
||||||
|
status String @default("PENDING") @db.VarChar(30)
|
||||||
|
notes String? @db.Text
|
||||||
|
adminNotes String? @map("admin_notes") @db.Text
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
pet Pet? @relation(fields: [petId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@map("prescriptions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model B2BInquiry {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
companyName String @map("company_name") @db.VarChar(200)
|
||||||
|
contactName String @map("contact_name") @db.VarChar(150)
|
||||||
|
email String @db.VarChar(150)
|
||||||
|
phone String @db.VarChar(20)
|
||||||
|
businessType String @map("business_type") @db.VarChar(50)
|
||||||
|
estimatedVolume String? @map("estimated_volume") @db.VarChar(100)
|
||||||
|
message String @db.Text
|
||||||
|
status String @default("PENDING") @db.VarChar(30)
|
||||||
|
adminNotes String? @map("admin_notes") @db.Text
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
@@map("b2b_inquiries")
|
||||||
|
}
|
||||||
|
|
||||||
|
model PartnerAccount {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
userId String @unique @map("user_id") @db.Uuid
|
||||||
|
companyName String @map("company_name") @db.VarChar(200)
|
||||||
|
taxId String? @map("tax_id") @db.VarChar(50)
|
||||||
|
creditLimit Decimal @default(0.00) @map("credit_limit") @db.Decimal(15, 2)
|
||||||
|
discountTier String @default("STANDARD") @map("discount_tier") @db.VarChar(30)
|
||||||
|
status String @default("ACTIVE") @db.VarChar(30)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("partner_accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Setting {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
category String @db.VarChar(50)
|
||||||
|
key String @unique @db.VarChar(100)
|
||||||
|
value Json
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
||||||
|
|
||||||
|
@@index([category])
|
||||||
|
@@map("settings")
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,12 @@ import { WholesaleModule } from './wholesale/wholesale.module';
|
|||||||
import { VideosModule } from './videos/videos.module';
|
import { VideosModule } from './videos/videos.module';
|
||||||
import { SmsModule } from './common/sms.module';
|
import { SmsModule } from './common/sms.module';
|
||||||
import { ContactModule } from './contact/contact.module';
|
import { ContactModule } from './contact/contact.module';
|
||||||
|
import { BannersModule } from './banners/banners.module';
|
||||||
|
import { SmartAdvisorModule } from './smart-advisor/smart-advisor.module';
|
||||||
|
import { TestimonialsModule } from './testimonials/testimonials.module';
|
||||||
|
import { IngredientsModule } from './ingredients/ingredients.module';
|
||||||
|
import { PrescriptionsModule } from './prescriptions/prescriptions.module';
|
||||||
|
import { B2BModule } from './b2b/b2b.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -48,6 +54,12 @@ import { ContactModule } from './contact/contact.module';
|
|||||||
CmsModule,
|
CmsModule,
|
||||||
WholesaleModule,
|
WholesaleModule,
|
||||||
VideosModule,
|
VideosModule,
|
||||||
|
BannersModule,
|
||||||
|
SmartAdvisorModule,
|
||||||
|
TestimonialsModule,
|
||||||
|
IngredientsModule,
|
||||||
|
PrescriptionsModule,
|
||||||
|
B2BModule,
|
||||||
],
|
],
|
||||||
controllers: [MetricsController],
|
controllers: [MetricsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
84
backend/src/b2b/b2b.controller.ts
Normal file
84
backend/src/b2b/b2b.controller.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { B2BService } from './b2b.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@ApiTags('B2B - خدمات عمدهفروشی و همکاران تجاری')
|
||||||
|
@Controller('b2b')
|
||||||
|
export class B2BController {
|
||||||
|
constructor(private readonly b2bService: B2BService) {}
|
||||||
|
|
||||||
|
@Post('inquiries')
|
||||||
|
@ApiOperation({ summary: 'ثبت درخواست همکاری عمده (B2B)' })
|
||||||
|
createInquiry(
|
||||||
|
@Body()
|
||||||
|
body: {
|
||||||
|
companyName: string;
|
||||||
|
contactName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
businessType: string;
|
||||||
|
estimatedVolume?: string;
|
||||||
|
message: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return this.b2bService.createInquiry(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Get('inquiries')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'دریافت لیست درخواستهای همکاری B2B (نیازمند ادمین)',
|
||||||
|
})
|
||||||
|
findAllInquiries() {
|
||||||
|
return this.b2bService.findAllInquiries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Patch('inquiries/:id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'بررسی و تغییر وضعیت درخواست همکاری (نیازمند ادمین)',
|
||||||
|
})
|
||||||
|
updateInquiryStatus(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { status: string; adminNotes?: string },
|
||||||
|
) {
|
||||||
|
return this.b2bService.updateInquiryStatus(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Get('partner')
|
||||||
|
@ApiOperation({ summary: 'دریافت اطلاعات حساب همکار تجاری' })
|
||||||
|
getPartnerProfile(@Req() req: any) {
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
return this.b2bService.getPartnerProfile(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Post('orders')
|
||||||
|
@ApiOperation({ summary: 'ثبت سفارش عمدهفروشی B2B' })
|
||||||
|
createWholesaleOrder(
|
||||||
|
@Req() req: any,
|
||||||
|
@Body() body: { items: any[]; totalAmount: number },
|
||||||
|
) {
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
return this.b2bService.createWholesaleOrder(userId, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/b2b/b2b.module.ts
Normal file
12
backend/src/b2b/b2b.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { B2BController } from './b2b.controller';
|
||||||
|
import { B2BService } from './b2b.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [B2BController],
|
||||||
|
providers: [B2BService],
|
||||||
|
exports: [B2BService],
|
||||||
|
})
|
||||||
|
export class B2BModule {}
|
||||||
89
backend/src/b2b/b2b.service.ts
Normal file
89
backend/src/b2b/b2b.service.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class B2BService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async createInquiry(data: {
|
||||||
|
companyName: string;
|
||||||
|
contactName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
businessType: string;
|
||||||
|
estimatedVolume?: string;
|
||||||
|
message: string;
|
||||||
|
}) {
|
||||||
|
return this.prisma.b2BInquiry.create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllInquiries() {
|
||||||
|
return this.prisma.b2BInquiry.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateInquiryStatus(
|
||||||
|
id: string,
|
||||||
|
data: { status: string; adminNotes?: string },
|
||||||
|
) {
|
||||||
|
const item = await this.prisma.b2BInquiry.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`B2BInquiry with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.b2BInquiry.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPartnerProfile(userId: string) {
|
||||||
|
const partner = await this.prisma.partnerAccount.findUnique({
|
||||||
|
where: { userId },
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
if (!partner) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Partner account for user ${userId} not found`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return partner;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWholesaleOrder(
|
||||||
|
userId: string,
|
||||||
|
data: { items: any[]; totalAmount: number },
|
||||||
|
) {
|
||||||
|
const partner = await this.prisma.partnerAccount.findUnique({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
if (!partner || partner.status !== 'ACTIVE') {
|
||||||
|
throw new ForbiddenException('User is not an active wholesale partner');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.order.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
totalAmount: data.totalAmount,
|
||||||
|
status: 'processing',
|
||||||
|
isRefill: false,
|
||||||
|
orderItems: {
|
||||||
|
create: data.items.map((item) => ({
|
||||||
|
productId: item.productId,
|
||||||
|
quantity: item.quantity,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { orderItems: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
64
backend/src/banners/banners.controller.ts
Normal file
64
backend/src/banners/banners.controller.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { BannersService } from './banners.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Banners - بنرهای تبلیغاتی و اسلایدر')
|
||||||
|
@Controller('banners')
|
||||||
|
export class BannersController {
|
||||||
|
constructor(private readonly bannersService: BannersService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'لیست تمامی بنرها' })
|
||||||
|
findAll() {
|
||||||
|
return this.bannersService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'ایجاد بنر جدید (نیازمند ادمین)' })
|
||||||
|
create(@Body() body: any) {
|
||||||
|
return this.bannersService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Put('reorder')
|
||||||
|
@ApiOperation({ summary: 'تغییر ترتیب نمایش بنرها (نیازمند ادمین)' })
|
||||||
|
reorder(@Body() body: { id: string; order: number }[]) {
|
||||||
|
return this.bannersService.reorder(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'ویرایش بنر (نیازمند ادمین)' })
|
||||||
|
update(@Param('id') id: string, @Body() body: any) {
|
||||||
|
return this.bannersService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'حذف بنر (نیازمند ادمین)' })
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.bannersService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/banners/banners.module.ts
Normal file
12
backend/src/banners/banners.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BannersController } from './banners.controller';
|
||||||
|
import { BannersService } from './banners.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [BannersController],
|
||||||
|
providers: [BannersService],
|
||||||
|
exports: [BannersService],
|
||||||
|
})
|
||||||
|
export class BannersModule {}
|
||||||
50
backend/src/banners/banners.service.ts
Normal file
50
backend/src/banners/banners.service.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BannersService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.prisma.banner.findMany({
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Prisma.BannerCreateInput) {
|
||||||
|
return this.prisma.banner.create({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Prisma.BannerUpdateInput) {
|
||||||
|
const banner = await this.prisma.banner.findUnique({ where: { id } });
|
||||||
|
if (!banner) {
|
||||||
|
throw new NotFoundException(`Banner with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.banner.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const banner = await this.prisma.banner.findUnique({ where: { id } });
|
||||||
|
if (!banner) {
|
||||||
|
throw new NotFoundException(`Banner with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.banner.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async reorder(items: { id: string; order: number }[]) {
|
||||||
|
const updates = items.map((item) =>
|
||||||
|
this.prisma.banner.update({
|
||||||
|
where: { id: item.id },
|
||||||
|
data: { order: item.order },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await this.prisma.$transaction(updates);
|
||||||
|
return { success: true, message: 'Banners reordered successfully' };
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/ingredients/ingredients.controller.ts
Normal file
60
backend/src/ingredients/ingredients.controller.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { IngredientsService } from './ingredients.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Ingredients - دانشنامه ترکیبات و مواد موثره')
|
||||||
|
@Controller('ingredients')
|
||||||
|
export class IngredientsController {
|
||||||
|
constructor(private readonly ingredientsService: IngredientsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'دریافت لیست ترکیبات فعال' })
|
||||||
|
findAll() {
|
||||||
|
return this.ingredientsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':idOrSlug')
|
||||||
|
@ApiOperation({ summary: 'دریافت اطلاعات یک ترکیب بر اساس آیدی یا اسلاگ' })
|
||||||
|
findOne(@Param('idOrSlug') idOrSlug: string) {
|
||||||
|
return this.ingredientsService.findOne(idOrSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'ایجاد ترکیب جدید (نیازمند ادمین)' })
|
||||||
|
create(@Body() body: any) {
|
||||||
|
return this.ingredientsService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'ویرایش اطلاعات ترکیب (نیازمند ادمین)' })
|
||||||
|
update(@Param('id') id: string, @Body() body: any) {
|
||||||
|
return this.ingredientsService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'حذف ترکیب (نیازمند ادمین)' })
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.ingredientsService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/ingredients/ingredients.module.ts
Normal file
12
backend/src/ingredients/ingredients.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { IngredientsController } from './ingredients.controller';
|
||||||
|
import { IngredientsService } from './ingredients.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [IngredientsController],
|
||||||
|
providers: [IngredientsService],
|
||||||
|
exports: [IngredientsService],
|
||||||
|
})
|
||||||
|
export class IngredientsModule {}
|
||||||
54
backend/src/ingredients/ingredients.service.ts
Normal file
54
backend/src/ingredients/ingredients.service.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IngredientsService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.prisma.ingredient.findMany({
|
||||||
|
orderBy: { nameFa: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(idOrSlug: string) {
|
||||||
|
const isUuid =
|
||||||
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
||||||
|
idOrSlug,
|
||||||
|
);
|
||||||
|
const ingredient = await this.prisma.ingredient.findFirst({
|
||||||
|
where: isUuid ? { id: idOrSlug } : { slug: idOrSlug },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ingredient) {
|
||||||
|
throw new NotFoundException(`Ingredient ${idOrSlug} not found`);
|
||||||
|
}
|
||||||
|
return ingredient;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Prisma.IngredientCreateInput) {
|
||||||
|
return this.prisma.ingredient.create({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Prisma.IngredientUpdateInput) {
|
||||||
|
const item = await this.prisma.ingredient.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`Ingredient with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.ingredient.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const item = await this.prisma.ingredient.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`Ingredient with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.ingredient.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
61
backend/src/prescriptions/prescriptions.controller.ts
Normal file
61
backend/src/prescriptions/prescriptions.controller.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PrescriptionsService } from './prescriptions.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Prescriptions - نسخه و تاییدیه دارویی')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('prescriptions')
|
||||||
|
export class PrescriptionsController {
|
||||||
|
constructor(private readonly prescriptionsService: PrescriptionsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'بارگذاری نسخه جدید توسط کاربر' })
|
||||||
|
create(
|
||||||
|
@Req() req: any,
|
||||||
|
@Body() body: { petId?: string; fileUrl: string; notes?: string },
|
||||||
|
) {
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
return this.prescriptionsService.create(userId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'دریافت لیست نسخهها (کاربر یا ادمین)' })
|
||||||
|
findAll(@Req() req: any) {
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
const isAdmin = req.user.role === 'Admin';
|
||||||
|
return this.prescriptionsService.findAll(userId, isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'دریافت جزئیات یک نسخه' })
|
||||||
|
findOne(@Req() req: any, @Param('id') id: string) {
|
||||||
|
const userId = req.user.id || req.user.userId;
|
||||||
|
const isAdmin = req.user.role === 'Admin';
|
||||||
|
return this.prescriptionsService.findOne(id, userId, isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@Patch(':id/review')
|
||||||
|
@ApiOperation({ summary: 'بررسی و تایید/رد نسخه دارویی (نیازمند ادمین)' })
|
||||||
|
review(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body()
|
||||||
|
body: { status: 'APPROVED' | 'REJECTED' | 'PENDING'; adminNotes?: string },
|
||||||
|
) {
|
||||||
|
return this.prescriptionsService.review(id, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/prescriptions/prescriptions.module.ts
Normal file
12
backend/src/prescriptions/prescriptions.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PrescriptionsController } from './prescriptions.controller';
|
||||||
|
import { PrescriptionsService } from './prescriptions.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [PrescriptionsController],
|
||||||
|
providers: [PrescriptionsService],
|
||||||
|
exports: [PrescriptionsService],
|
||||||
|
})
|
||||||
|
export class PrescriptionsModule {}
|
||||||
79
backend/src/prescriptions/prescriptions.service.ts
Normal file
79
backend/src/prescriptions/prescriptions.service.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrescriptionsService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
userId: string,
|
||||||
|
data: { petId?: string; fileUrl: string; notes?: string },
|
||||||
|
) {
|
||||||
|
return this.prisma.prescription.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
petId: data.petId,
|
||||||
|
fileUrl: data.fileUrl,
|
||||||
|
notes: data.notes,
|
||||||
|
status: 'PENDING',
|
||||||
|
},
|
||||||
|
include: { pet: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(userId: string, isAdmin: boolean) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return this.prisma.prescription.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
include: { user: true, pet: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.prisma.prescription.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
include: { pet: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string, userId: string, isAdmin: boolean) {
|
||||||
|
const item = await this.prisma.prescription.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { user: true, pet: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`Prescription with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin && item.userId !== userId) {
|
||||||
|
throw new ForbiddenException('Access denied to this prescription');
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async review(
|
||||||
|
id: string,
|
||||||
|
reviewData: {
|
||||||
|
status: 'APPROVED' | 'REJECTED' | 'PENDING';
|
||||||
|
adminNotes?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const item = await this.prisma.prescription.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`Prescription with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.prescription.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
status: reviewData.status,
|
||||||
|
adminNotes: reviewData.adminNotes,
|
||||||
|
},
|
||||||
|
include: { user: true, pet: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
Query,
|
Query,
|
||||||
Param,
|
Param,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ProductsService } from './products.service';
|
import { ProductsService } from './products.service';
|
||||||
import { GetProductsDto } from './dto/get-products.dto';
|
import { GetProductsDto } from './dto/get-products.dto';
|
||||||
@ -15,62 +18,21 @@ import {
|
|||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiNotFoundResponse,
|
ApiNotFoundResponse,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
@ApiTags('Products - مدیریت محصولات دارویی')
|
@ApiTags('Products - مدیریت محصولات دارویی')
|
||||||
@Controller('products')
|
@Controller('products')
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
description: 'خطای داخلی سرور',
|
description: 'خطای داخلی سرور',
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
success: false,
|
|
||||||
message: 'خطای داخلی سرور',
|
|
||||||
code: 'SERVER_ERROR',
|
|
||||||
details: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
export class ProductsController {
|
export class ProductsController {
|
||||||
constructor(private readonly productsService: ProductsService) {}
|
constructor(private readonly productsService: ProductsService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'لیست و فیلتر محصولات' })
|
@ApiOperation({ summary: 'لیست و فیلتر محصولات' })
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'لیست محصولات متناسب با فیلترها (دسته، پت و جستجو)',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: 'a1b2c3d4-1234-5678-abcd-ef1234567890',
|
|
||||||
artNo: 'canhydrox-gag',
|
|
||||||
name: 'Canhydrox GAG (کنهیدروکس)',
|
|
||||||
scientificTagline: 'برای تقویت مفاصل و استخوانها',
|
|
||||||
description:
|
|
||||||
'کنهیدروکس محصولی بینظیر برای مفاصل و سیستم حرکتی سگها...',
|
|
||||||
shortDescription: 'تقویت مفاصل و غضروفها',
|
|
||||||
category: 'سیستم حرکتی و مفاصل',
|
|
||||||
categorySlug: 'joints',
|
|
||||||
priceValue: '1750000.00',
|
|
||||||
priceDisplay: '۱,۷۵۰,۰۰۰ تومان',
|
|
||||||
unit: 'عدد قرص',
|
|
||||||
packageSize: '120.00',
|
|
||||||
dosageLogic: 'یک قرص به ازای هر ۱۰ کیلوگرم وزن بدن سگ',
|
|
||||||
suitableFor: 'سگ',
|
|
||||||
imageUrl: 'https://example.com/canhydrox.png',
|
|
||||||
createdAt: '2026-05-26T18:10:00.000Z',
|
|
||||||
ingredients: [],
|
|
||||||
symptoms: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
meta: {
|
|
||||||
total: 1,
|
|
||||||
page: 1,
|
|
||||||
lastPage: 1,
|
|
||||||
limit: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
findAll(@Query() query: GetProductsDto) {
|
findAll(@Query() query: GetProductsDto) {
|
||||||
return this.productsService.findAll(query);
|
return this.productsService.findAll(query);
|
||||||
}
|
}
|
||||||
@ -79,16 +41,6 @@ export class ProductsController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'دریافت فیلترهای فعال محصولات (دسته، علائم، نوع پت)',
|
summary: 'دریافت فیلترهای فعال محصولات (دسته، علائم، نوع پت)',
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'لیست فیلترهای پویا استخراج شده از دیتابیس',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
categories: [{ id: '1', name: 'مفاصل و استخوان', slug: 'joints' }],
|
|
||||||
symptoms: ['لنگش', 'ریزش مو'],
|
|
||||||
petTypes: ['سگ', 'گربه', 'هر دو'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
getActiveFilters() {
|
getActiveFilters() {
|
||||||
return this.productsService.getActiveFilters();
|
return this.productsService.getActiveFilters();
|
||||||
}
|
}
|
||||||
@ -97,72 +49,28 @@ export class ProductsController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'دریافت فیلترها و راهکارهای ناوبری (دسته با علائم مربوطه)',
|
summary: 'دریافت فیلترها و راهکارهای ناوبری (دسته با علائم مربوطه)',
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'ساختار درختی فیلترها و تگهای درمانی واقعی متصل به محصولات',
|
|
||||||
schema: {
|
|
||||||
example: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'مفاصل و استخوان',
|
|
||||||
slug: 'joints',
|
|
||||||
symptoms: ['درد مفاصل', 'لنگش'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
getNavigationFilters() {
|
getNavigationFilters() {
|
||||||
return this.productsService.getNavigationFilters();
|
return this.productsService.getNavigationFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':id/dosage-config')
|
||||||
|
@ApiOperation({ summary: 'دریافت پیکربندی دوز مصرف محصول' })
|
||||||
|
getDosageConfig(@Param('id') id: string) {
|
||||||
|
return this.productsService.getDosageConfig(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@Patch(':id/dosage-config')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'ویرایش پیکربندی دوز مصرف محصول (نیازمند دسترسی ادمین)',
|
||||||
|
})
|
||||||
|
updateDosageConfig(@Param('id') id: string, @Body() body: any) {
|
||||||
|
return this.productsService.updateDosageConfig(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'دریافت اطلاعات محصول با شناسه یکتا (ID)' })
|
@ApiOperation({ summary: 'دریافت اطلاعات محصول با شناسه یکتا (ID)' })
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'اطلاعات کامل محصول شامل مواد تشکیلدهنده و علائم مرتبط',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
id: 'a1b2c3d4-1234-5678-abcd-ef1234567890',
|
|
||||||
artNo: 'canhydrox-gag',
|
|
||||||
name: 'Canhydrox GAG (کنهیدروکس)',
|
|
||||||
scientificTagline: 'برای تقویت مفاصل و استخوانها',
|
|
||||||
description: 'کنهیدروکس محصولی بینظیر برای مفاصل...',
|
|
||||||
shortDescription: 'تقویت مفاصل و غضروفها',
|
|
||||||
category: 'سیستم حرکتی و مفاصل',
|
|
||||||
categorySlug: 'joints',
|
|
||||||
priceValue: '1750000.00',
|
|
||||||
priceDisplay: '۱,۷۵۰,۰۰۰ تومان',
|
|
||||||
unit: 'عدد قرص',
|
|
||||||
packageSize: '120.00',
|
|
||||||
dosageLogic: 'یک قرص به ازای هر ۱۰ کیلوگرم وزن بدن سگ',
|
|
||||||
suitableFor: 'سگ',
|
|
||||||
imageUrl: 'https://example.com/canhydrox.png',
|
|
||||||
createdAt: '2026-05-26T18:10:00.000Z',
|
|
||||||
ingredients: [
|
|
||||||
{
|
|
||||||
productId: 'a1b2c3d4-1234-5678-abcd-ef1234567890',
|
|
||||||
ingredient: 'صدف لبسبز',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
symptoms: [
|
|
||||||
{
|
|
||||||
productId: 'a1b2c3d4-1234-5678-abcd-ef1234567890',
|
|
||||||
symptom: 'لنگیدن',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiNotFoundResponse({
|
|
||||||
description: 'محصول با شناسه ارسال شده پیدا نشد',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
'Product with ID a1b2c3d4-1234-5678-abcd-ef1234567890 not found',
|
|
||||||
code: 'NOT_FOUND',
|
|
||||||
details: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async findOne(@Param('id') id: string) {
|
async findOne(@Param('id') id: string) {
|
||||||
const product = await this.productsService.findOne(id);
|
const product = await this.productsService.findOne(id);
|
||||||
if (!product) {
|
if (!product) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { GetProductsDto } from './dto/get-products.dto';
|
import { GetProductsDto } from './dto/get-products.dto';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProductsService {
|
export class ProductsService {
|
||||||
@ -21,7 +22,7 @@ export class ProductsService {
|
|||||||
sortOrder = 'desc',
|
sortOrder = 'desc',
|
||||||
} = filters;
|
} = filters;
|
||||||
|
|
||||||
const andConditions: any[] = [];
|
const andConditions: Prisma.ProductWhereInput[] = [];
|
||||||
|
|
||||||
if (requiresRx !== undefined && requiresRx !== null && requiresRx !== '') {
|
if (requiresRx !== undefined && requiresRx !== null && requiresRx !== '') {
|
||||||
andConditions.push({
|
andConditions.push({
|
||||||
@ -75,7 +76,7 @@ export class ProductsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause: any =
|
const whereClause: Prisma.ProductWhereInput =
|
||||||
andConditions.length > 0 ? { AND: andConditions } : {};
|
andConditions.length > 0 ? { AND: andConditions } : {};
|
||||||
|
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
@ -102,13 +103,11 @@ export class ProductsService {
|
|||||||
const isAdmin = userRole === 'ADMIN' || userRole === 'SuperAdmin';
|
const isAdmin = userRole === 'ADMIN' || userRole === 'SuperAdmin';
|
||||||
|
|
||||||
const data = rawProducts.map((p) => {
|
const data = rawProducts.map((p) => {
|
||||||
// Remove buyPrice for all non-admins
|
const { buyPrice: _b, wholesalePrice: _w, ...publicProduct } = p;
|
||||||
const { buyPrice, ...withoutBuyPrice } = p;
|
|
||||||
if (!isWholesaleOrAdmin) {
|
if (!isWholesaleOrAdmin) {
|
||||||
const { wholesalePrice, ...publicProduct } = withoutBuyPrice;
|
|
||||||
return publicProduct;
|
return publicProduct;
|
||||||
}
|
}
|
||||||
return isAdmin ? p : withoutBuyPrice;
|
return isAdmin ? p : { ...publicProduct, wholesalePrice: p.wholesalePrice };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -143,12 +142,11 @@ export class ProductsService {
|
|||||||
userRole === 'SuperAdmin';
|
userRole === 'SuperAdmin';
|
||||||
const isAdmin = userRole === 'ADMIN' || userRole === 'SuperAdmin';
|
const isAdmin = userRole === 'ADMIN' || userRole === 'SuperAdmin';
|
||||||
|
|
||||||
const { buyPrice, ...withoutBuyPrice } = product;
|
const { buyPrice: _b, wholesalePrice: _w, ...publicProduct } = product;
|
||||||
if (!isWholesaleOrAdmin) {
|
if (!isWholesaleOrAdmin) {
|
||||||
const { wholesalePrice, ...publicProduct } = withoutBuyPrice;
|
|
||||||
return publicProduct;
|
return publicProduct;
|
||||||
}
|
}
|
||||||
return isAdmin ? product : withoutBuyPrice;
|
return isAdmin ? product : { ...publicProduct, wholesalePrice: product.wholesalePrice };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveFilters() {
|
async getActiveFilters() {
|
||||||
@ -220,4 +218,26 @@ export class ProductsService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDosageConfig(id: string) {
|
||||||
|
const product = await this.prisma.product.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { dosageConfig: true, dosageLogic: true },
|
||||||
|
});
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException(`Product with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return product.dosageConfig || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDosageConfig(id: string, dosageConfig: Prisma.InputJsonValue) {
|
||||||
|
const product = await this.prisma.product.findUnique({ where: { id } });
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException(`Product with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.product.update({
|
||||||
|
where: { id },
|
||||||
|
data: { dosageConfig },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
HttpStatus,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from './settings.service';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
@ -17,12 +16,10 @@ import {
|
|||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiResponse,
|
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiUnauthorizedResponse,
|
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
@ApiTags('Settings - تنظیمات متون پویا و واژهنامه علمی')
|
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی و سیستم')
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@Roles('Admin')
|
@Roles('Admin')
|
||||||
@Controller('settings')
|
@Controller('settings')
|
||||||
@ -32,15 +29,7 @@ export class SettingsController {
|
|||||||
@Get('ui-texts')
|
@Get('ui-texts')
|
||||||
@ApiOperation({ summary: 'دریافت تمامی متون و پیکربندیهای رابط کاربری' })
|
@ApiOperation({ summary: 'دریافت تمامی متون و پیکربندیهای رابط کاربری' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description:
|
description: 'یک آبجکت کلید-مقدار حاوی تمامی متون، شعارها و برچسبهای دکمهها',
|
||||||
'یک آبجکت کلید-مقدار حاوی تمامی متون، شعارها و برچسبهای دکمهها',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
hero_badge: 'تخصص دارویی از آلمان',
|
|
||||||
hero_title: 'تخصص آلمانی در خدمت سلامت پتهای خانگی',
|
|
||||||
hero_desc: 'بیش از ۴۰ سال تجربه نوآورانه...',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
getUiTexts() {
|
getUiTexts() {
|
||||||
return this.settingsService.getUiTexts();
|
return this.settingsService.getUiTexts();
|
||||||
@ -49,45 +38,13 @@ export class SettingsController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Patch('ui-texts/:key')
|
@Patch('ui-texts/:key')
|
||||||
@ApiOperation({ summary: 'ویرایش متن یک کلید در رابط کاربری (نیازمند توکن)' })
|
@ApiOperation({ summary: 'ویرایش متن یک کلید در رابط کاربری' })
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'متن با موفقیت بهروزرسانی شد',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
key: 'hero_badge',
|
|
||||||
value: 'تخصص دارویی ممتاز از آلمان',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiUnauthorizedResponse({
|
|
||||||
description: 'عدم دسترسی به دلیل عدم احراز هویت',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
success: false,
|
|
||||||
message: 'Unauthorized',
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
updateUiText(@Param('key') key: string, @Body('value') value: string) {
|
updateUiText(@Param('key') key: string, @Body('value') value: string) {
|
||||||
return this.settingsService.updateUiText(key, value);
|
return this.settingsService.updateUiText(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('scientific-terms')
|
@Get('scientific-terms')
|
||||||
@ApiOperation({ summary: 'دریافت تمامی اصطلاحات واژهنامه علمی' })
|
@ApiOperation({ summary: 'دریافت تمامی اصطلاحات واژهنامه علمی' })
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'لیست کامل اصطلاحات علمی به همراه تعاریف و شناسهها',
|
|
||||||
schema: {
|
|
||||||
example: [
|
|
||||||
{
|
|
||||||
key: 'green-mussel',
|
|
||||||
term: 'صدف لبسبز (Perna Canaliculus)',
|
|
||||||
definition: 'این صدف بومی سواحل بکر نیوزیلند است...',
|
|
||||||
wikiId: 'general',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
getScientificTerms() {
|
getScientificTerms() {
|
||||||
return this.settingsService.getScientificTerms();
|
return this.settingsService.getScientificTerms();
|
||||||
}
|
}
|
||||||
@ -95,28 +52,7 @@ export class SettingsController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Put('scientific-terms/:key')
|
@Put('scientific-terms/:key')
|
||||||
@ApiOperation({ summary: 'ثبت یا ویرایش یک اصطلاح علمی (نیازمند توکن)' })
|
@ApiOperation({ summary: 'ثبت یا ویرایش یک اصطلاح علمی' })
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'اصطلاح علمی ثبت یا ویرایش شد',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
key: 'green-mussel',
|
|
||||||
term: 'صدف لبسبز اصل نیوزیلند',
|
|
||||||
definition: 'این صدف بومی سواحل بکر نیوزیلند است...',
|
|
||||||
wikiId: 'general',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiUnauthorizedResponse({
|
|
||||||
description: 'عدم دسترسی',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
success: false,
|
|
||||||
message: 'Unauthorized',
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
upsertScientificTerm(@Param('key') key: string, @Body() data: any) {
|
upsertScientificTerm(@Param('key') key: string, @Body() data: any) {
|
||||||
return this.settingsService.upsertScientificTerm(key, data);
|
return this.settingsService.upsertScientificTerm(key, data);
|
||||||
}
|
}
|
||||||
@ -124,27 +60,47 @@ export class SettingsController {
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Delete('scientific-terms/:key')
|
@Delete('scientific-terms/:key')
|
||||||
@ApiOperation({ summary: 'حذف یک اصطلاح علمی (نیازمند توکن)' })
|
@ApiOperation({ summary: 'حذف یک اصطلاح علمی' })
|
||||||
@ApiOkResponse({
|
|
||||||
description: 'اصطلاح علمی حذف شد',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
success: true,
|
|
||||||
message: 'Scientific term successfully deleted',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiUnauthorizedResponse({
|
|
||||||
description: 'عدم دسترسی',
|
|
||||||
schema: {
|
|
||||||
example: {
|
|
||||||
success: false,
|
|
||||||
message: 'Unauthorized',
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
deleteScientificTerm(@Param('key') key: string) {
|
deleteScientificTerm(@Param('key') key: string) {
|
||||||
return this.settingsService.deleteScientificTerm(key);
|
return this.settingsService.deleteScientificTerm(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SEO Settings
|
||||||
|
@Get('seo')
|
||||||
|
@ApiOperation({ summary: 'دریافت تنظیمات سئو' })
|
||||||
|
getSeoSettings() {
|
||||||
|
return this.settingsService.getCategorySetting('seo');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('seo')
|
||||||
|
@ApiOperation({ summary: 'بهروزرسانی تنظیمات سئو' })
|
||||||
|
updateSeoSettings(@Body() body: any) {
|
||||||
|
return this.settingsService.updateCategorySetting('seo', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Financial Settings
|
||||||
|
@Get('financial')
|
||||||
|
@ApiOperation({ summary: 'دریافت تنظیمات مالی' })
|
||||||
|
getFinancialSettings() {
|
||||||
|
return this.settingsService.getCategorySetting('financial');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('financial')
|
||||||
|
@ApiOperation({ summary: 'بهروزرسانی تنظیمات مالی' })
|
||||||
|
updateFinancialSettings(@Body() body: any) {
|
||||||
|
return this.settingsService.updateCategorySetting('financial', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Settings
|
||||||
|
@Get('system')
|
||||||
|
@ApiOperation({ summary: 'دریافت تنظیمات سیستم' })
|
||||||
|
getSystemSettings() {
|
||||||
|
return this.settingsService.getCategorySetting('system');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('system')
|
||||||
|
@ApiOperation({ summary: 'بهروزرسانی تنظیمات سیستم' })
|
||||||
|
updateSystemSettings(@Body() body: any) {
|
||||||
|
return this.settingsService.updateCategorySetting('system', body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,18 +22,22 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upsertScientificTerm(key: string, data: any) {
|
async upsertScientificTerm(key: string, data: any) {
|
||||||
|
const term = String(data?.term || '');
|
||||||
|
const definition = String(data?.definition || '');
|
||||||
|
const wikiId = String(data?.wikiId || 'general');
|
||||||
|
|
||||||
return this.prisma.scientificTerm.upsert({
|
return this.prisma.scientificTerm.upsert({
|
||||||
where: { key },
|
where: { key },
|
||||||
update: {
|
update: {
|
||||||
term: data.term,
|
term,
|
||||||
definition: data.definition,
|
definition,
|
||||||
wikiId: data.wikiId || 'general',
|
wikiId,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
key,
|
key,
|
||||||
term: data.term,
|
term,
|
||||||
definition: data.definition,
|
definition,
|
||||||
wikiId: data.wikiId || 'general',
|
wikiId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -43,4 +47,20 @@ export class SettingsService {
|
|||||||
where: { key },
|
where: { key },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCategorySetting(category: string) {
|
||||||
|
const setting = await this.prisma.setting.findFirst({
|
||||||
|
where: { category },
|
||||||
|
});
|
||||||
|
return setting ? setting.value : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCategorySetting(category: string, value: any) {
|
||||||
|
const key = `${category}_config`;
|
||||||
|
return this.prisma.setting.upsert({
|
||||||
|
where: { key },
|
||||||
|
update: { category, value },
|
||||||
|
create: { key, category, value },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
backend/src/smart-advisor/smart-advisor.controller.ts
Normal file
54
backend/src/smart-advisor/smart-advisor.controller.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SmartAdvisorService } from './smart-advisor.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Smart Advisor - دستیار هوشمند توصیه دارویی')
|
||||||
|
@Controller('smart-advisor/rules')
|
||||||
|
export class SmartAdvisorController {
|
||||||
|
constructor(private readonly smartAdvisorService: SmartAdvisorService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'لیست قانونهای دستیار هوشمند' })
|
||||||
|
findAll() {
|
||||||
|
return this.smartAdvisorService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'ایجاد قانون جدید دستیار هوشمند (نیازمند ادمین)' })
|
||||||
|
create(@Body() body: any) {
|
||||||
|
return this.smartAdvisorService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'ویرایش قانون دستیار هوشمند (نیازمند ادمین)' })
|
||||||
|
update(@Param('id') id: string, @Body() body: any) {
|
||||||
|
return this.smartAdvisorService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'حذف قانون دستیار هوشمند (نیازمند ادمین)' })
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.smartAdvisorService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/smart-advisor/smart-advisor.module.ts
Normal file
12
backend/src/smart-advisor/smart-advisor.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SmartAdvisorController } from './smart-advisor.controller';
|
||||||
|
import { SmartAdvisorService } from './smart-advisor.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [SmartAdvisorController],
|
||||||
|
providers: [SmartAdvisorService],
|
||||||
|
exports: [SmartAdvisorService],
|
||||||
|
})
|
||||||
|
export class SmartAdvisorModule {}
|
||||||
41
backend/src/smart-advisor/smart-advisor.service.ts
Normal file
41
backend/src/smart-advisor/smart-advisor.service.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmartAdvisorService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.prisma.smartAdvisorRule.findMany({
|
||||||
|
include: { product: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Prisma.SmartAdvisorRuleCreateInput) {
|
||||||
|
return this.prisma.smartAdvisorRule.create({
|
||||||
|
data,
|
||||||
|
include: { product: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Prisma.SmartAdvisorRuleUpdateInput) {
|
||||||
|
const rule = await this.prisma.smartAdvisorRule.findUnique({ where: { id } });
|
||||||
|
if (!rule) {
|
||||||
|
throw new NotFoundException(`SmartAdvisorRule with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.smartAdvisorRule.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
include: { product: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const rule = await this.prisma.smartAdvisorRule.findUnique({ where: { id } });
|
||||||
|
if (!rule) {
|
||||||
|
throw new NotFoundException(`SmartAdvisorRule with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.smartAdvisorRule.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
54
backend/src/testimonials/testimonials.controller.ts
Normal file
54
backend/src/testimonials/testimonials.controller.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TestimonialsService } from './testimonials.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RolesGuard } from '../common/guards/roles.guard';
|
||||||
|
import { Roles } from '../common/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Testimonials - نظرات و رضایتنامهها')
|
||||||
|
@Controller('testimonials')
|
||||||
|
export class TestimonialsController {
|
||||||
|
constructor(private readonly testimonialsService: TestimonialsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'دریافت لیست نظرات و رضایتنامهها' })
|
||||||
|
findAll() {
|
||||||
|
return this.testimonialsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'ایجاد نظر جدید (نیازمند ادمین)' })
|
||||||
|
create(@Body() body: any) {
|
||||||
|
return this.testimonialsService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'ویرایش نظر (نیازمند ادمین)' })
|
||||||
|
update(@Param('id') id: string, @Body() body: any) {
|
||||||
|
return this.testimonialsService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('Admin')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'حذف نظر (نیازمند ادمین)' })
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.testimonialsService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/testimonials/testimonials.module.ts
Normal file
12
backend/src/testimonials/testimonials.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TestimonialsController } from './testimonials.controller';
|
||||||
|
import { TestimonialsService } from './testimonials.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [TestimonialsController],
|
||||||
|
providers: [TestimonialsService],
|
||||||
|
exports: [TestimonialsService],
|
||||||
|
})
|
||||||
|
export class TestimonialsModule {}
|
||||||
39
backend/src/testimonials/testimonials.service.ts
Normal file
39
backend/src/testimonials/testimonials.service.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TestimonialsService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.prisma.testimonial.findMany({
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Prisma.TestimonialCreateInput) {
|
||||||
|
return this.prisma.testimonial.create({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Prisma.TestimonialUpdateInput) {
|
||||||
|
const item = await this.prisma.testimonial.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`Testimonial with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.testimonial.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const item = await this.prisma.testimonial.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
throw new NotFoundException(`Testimonial with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.prisma.testimonial.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,9 @@ describe('Phase 4 Master Backlog E2E Verification Matrix (e2e)', () => {
|
|||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
app = moduleFixture.createNestApplication();
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({ transform: true, whitelist: true }),
|
||||||
|
);
|
||||||
app.useGlobalFilters(
|
app.useGlobalFilters(
|
||||||
new CustomHttpExceptionFilter(),
|
new CustomHttpExceptionFilter(),
|
||||||
new PrismaExceptionFilter(),
|
new PrismaExceptionFilter(),
|
||||||
@ -76,12 +78,12 @@ describe('Phase 4 Master Backlog E2E Verification Matrix (e2e)', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(() => validateStartup('short', process.env.JWT_REFRESH_SECRET)).toThrow(
|
expect(() =>
|
||||||
'FATAL: JWT_ACCESS_SECRET missing or short',
|
validateStartup('short', process.env.JWT_REFRESH_SECRET),
|
||||||
);
|
).toThrow('FATAL: JWT_ACCESS_SECRET missing or short');
|
||||||
expect(() => validateStartup(process.env.JWT_ACCESS_SECRET, 'short')).toThrow(
|
expect(() =>
|
||||||
'FATAL: JWT_REFRESH_SECRET missing or short',
|
validateStartup(process.env.JWT_ACCESS_SECRET, 'short'),
|
||||||
);
|
).toThrow('FATAL: JWT_REFRESH_SECRET missing or short');
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateStartup(
|
validateStartup(
|
||||||
process.env.JWT_ACCESS_SECRET,
|
process.env.JWT_ACCESS_SECRET,
|
||||||
|
|||||||
29
docs/audit/CROSS_BOUNDARY_DEPENDENCIES.md
Normal file
29
docs/audit/CROSS_BOUNDARY_DEPENDENCIES.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Cross Boundary Dependencies & Backend Architecture Specification (Phase A)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This specification details the database models and backend controller endpoints required to support all 10 cross-boundary feature domains across the Canina Pharma platform.
|
||||||
|
|
||||||
|
## 10 Feature Domains
|
||||||
|
|
||||||
|
1. **SEO Settings**: Global & category-level search engine metadata management (`/api/settings/seo`).
|
||||||
|
2. **Banners**: Dynamic promotion banners and hero sliders (`/api/banners`).
|
||||||
|
3. **Smart Advisor**: Rule-based veterinary product recommendation engine (`/api/smart-advisor/rules`).
|
||||||
|
4. **Testimonials**: Verified customer and veterinary doctor testimonials (`/api/testimonials`).
|
||||||
|
5. **Ingredients**: Scientific active ingredient glossary and properties (`/api/ingredients`).
|
||||||
|
6. **Product Dosage**: Dynamic weight-based dosage configuration for medical products (`/api/products/:id/dosage-config`).
|
||||||
|
7. **Financial Settings**: Platform tax rates, shipping rates, and charity contribution percentages (`/api/settings/financial`).
|
||||||
|
8. **System Settings**: System operational flags, maintenance modes, and feature toggles (`/api/settings/system`).
|
||||||
|
9. **Prescriptions**: Veterinary prescription upload, verification, and approval workflow (`/api/prescriptions`).
|
||||||
|
10. **B2B Logic**: Wholesale partner registration, credit limits, and bulk inquiries (`/api/b2b`).
|
||||||
|
|
||||||
|
## Schema Additions
|
||||||
|
|
||||||
|
- **`Setting`**: Stores category-based (`seo`, `financial`, `system`) key-value dynamic configurations.
|
||||||
|
- **`Banner`**: Hero and section promotion banners with custom display ordering.
|
||||||
|
- **`SmartAdvisorRule`**: Condition and pet type matching rules linking to recommended products.
|
||||||
|
- **`Testimonial`**: Customer & vet testimonials with rating and ordering.
|
||||||
|
- **`Ingredient`**: Farsi/English ingredient details, scientific names, and benefits list.
|
||||||
|
- **`Prescription`**: User & pet prescription attachments with verification status.
|
||||||
|
- **`B2BInquiry`**: Commercial partner inquiries with business metadata.
|
||||||
|
- **`PartnerAccount`**: Credit limits and wholesale tiers for approved B2B clients.
|
||||||
|
- **`Product.dosageConfig`**: JSON field holding dynamic calculation parameters per product.
|
||||||
Loading…
Reference in New Issue
Block a user