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 {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
firstName String @map("first_name") @db.VarChar(100)
|
||||
lastName String @map("last_name") @db.VarChar(100)
|
||||
email String? @unique @db.VarChar(150)
|
||||
mobile String @unique @db.VarChar(15)
|
||||
password String? @db.VarChar(255)
|
||||
role String @default("User_PetOwner") @db.VarChar(30)
|
||||
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)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz()
|
||||
|
||||
addresses UserAddress[]
|
||||
walletTransactions WalletTransaction[]
|
||||
pets Pet[]
|
||||
orders Order[]
|
||||
blogs Blog[]
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
firstName String @map("first_name") @db.VarChar(100)
|
||||
lastName String @map("last_name") @db.VarChar(100)
|
||||
email String? @unique @db.VarChar(150)
|
||||
mobile String @unique @db.VarChar(15)
|
||||
password String? @db.VarChar(255)
|
||||
role String @default("User_PetOwner") @db.VarChar(30)
|
||||
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)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz()
|
||||
|
||||
addresses UserAddress[]
|
||||
walletTransactions WalletTransaction[]
|
||||
pets Pet[]
|
||||
orders Order[]
|
||||
blogs Blog[]
|
||||
prescriptions Prescription[]
|
||||
partnerAccount PartnerAccount?
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@ -43,7 +45,7 @@ model UserAddress {
|
||||
isDefault Boolean @default(false) @map("is_default")
|
||||
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])
|
||||
@@map("user_addresses")
|
||||
@ -59,7 +61,7 @@ model WalletTransaction {
|
||||
description String? @db.Text
|
||||
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")
|
||||
}
|
||||
@ -91,9 +93,9 @@ model Category {
|
||||
imageUrl String? @map("image_url") @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
parent Category? @relation("SubCategories", fields: [parentId], references: [id])
|
||||
children Category[] @relation("SubCategories")
|
||||
products Product[]
|
||||
parent Category? @relation("SubCategories", fields: [parentId], references: [id])
|
||||
children Category[] @relation("SubCategories")
|
||||
products Product[]
|
||||
|
||||
@@map("categories")
|
||||
}
|
||||
@ -112,7 +114,7 @@ model Blog {
|
||||
createdAt DateTime @default(now()) @map("created_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")
|
||||
}
|
||||
@ -139,6 +141,7 @@ model Product {
|
||||
unit String @db.VarChar(50)
|
||||
packageSize Decimal @map("package_size") @db.Decimal(10, 2)
|
||||
dosageLogic String? @map("dosage_logic") @db.Text
|
||||
dosageConfig Json? @map("dosage_config")
|
||||
suitableFor String @map("suitable_for") @db.VarChar(15) // سگ, گربه, هر دو
|
||||
imageUrl String @map("image_url") @db.Text
|
||||
images String[] @default([])
|
||||
@ -151,12 +154,12 @@ model Product {
|
||||
keywords String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
category Category @relation(fields: [categoryId], references: [id])
|
||||
ingredientList ProductIngredient[]
|
||||
symptoms ProductSymptom[]
|
||||
reminders Reminder[]
|
||||
orderItems OrderItem[]
|
||||
advisorRules SmartAdvisorRule[]
|
||||
category Category @relation(fields: [categoryId], references: [id])
|
||||
ingredientList ProductIngredient[]
|
||||
symptoms ProductSymptom[]
|
||||
reminders Reminder[]
|
||||
orderItems OrderItem[]
|
||||
advisorRules SmartAdvisorRule[]
|
||||
|
||||
@@index([categorySlug])
|
||||
@@index([suitableFor])
|
||||
@ -197,21 +200,22 @@ model ProductSymptom {
|
||||
}
|
||||
|
||||
model Pet {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
type String @db.VarChar(10) // سگ, گربه
|
||||
breed String @db.VarChar(100)
|
||||
age Int
|
||||
weight Decimal @db.Decimal(5, 2)
|
||||
activityLevel String @map("activity_level") @db.VarChar(15) // کم, متوسط, زیاد
|
||||
imageUrl String? @map("image_url") @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
type String @db.VarChar(10) // سگ, گربه
|
||||
breed String @db.VarChar(100)
|
||||
age Int
|
||||
weight Decimal @db.Decimal(5, 2)
|
||||
activityLevel String @map("activity_level") @db.VarChar(15) // کم, متوسط, زیاد
|
||||
imageUrl String? @map("image_url") @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
medicalConditions PetMedicalCondition[]
|
||||
reminders Reminder[]
|
||||
healthLogs HealthLog[]
|
||||
prescriptions Prescription[]
|
||||
|
||||
@@map("pets")
|
||||
}
|
||||
@ -226,13 +230,13 @@ model PetMedicalCondition {
|
||||
}
|
||||
|
||||
model Reminder {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
petId String @map("pet_id") @db.Uuid
|
||||
productId String? @map("product_id") @db.Uuid
|
||||
title String @db.VarChar(150)
|
||||
time String @db.VarChar(5) // 08:30
|
||||
frequency String @db.VarChar(20) // روزانه, هفتگی
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
petId String @map("pet_id") @db.Uuid
|
||||
productId String? @map("product_id") @db.Uuid
|
||||
title String @db.VarChar(150)
|
||||
time String @db.VarChar(5) // 08:30
|
||||
frequency String @db.VarChar(20) // روزانه, هفتگی
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
pet Pet @relation(fields: [petId], references: [id], onDelete: Cascade)
|
||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||
@ -248,7 +252,7 @@ model ReminderCompletion {
|
||||
completedDate DateTime @map("completed_date") @db.Date
|
||||
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])
|
||||
@@index([reminderId, completedDate])
|
||||
@ -256,68 +260,68 @@ model ReminderCompletion {
|
||||
}
|
||||
|
||||
model HealthLog {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
petId String @map("pet_id") @db.Uuid
|
||||
appetite String @db.VarChar(15)
|
||||
energy String @db.VarChar(15)
|
||||
digestion String @db.VarChar(15)
|
||||
note String? @db.Text
|
||||
loggedDate DateTime @default(now()) @map("logged_date") @db.Date
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
petId String @map("pet_id") @db.Uuid
|
||||
appetite String @db.VarChar(15)
|
||||
energy String @db.VarChar(15)
|
||||
digestion String @db.VarChar(15)
|
||||
note String? @db.Text
|
||||
loggedDate DateTime @default(now()) @map("logged_date") @db.Date
|
||||
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")
|
||||
}
|
||||
|
||||
model Coupon {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code String @unique @db.VarChar(50)
|
||||
type String @default("percent") @db.VarChar(20) // fixed, percent
|
||||
value Decimal @db.Decimal(15, 2)
|
||||
minCartValue Decimal? @map("min_cart_value") @db.Decimal(15, 2)
|
||||
maxCartValue Decimal? @map("max_cart_value") @db.Decimal(15, 2)
|
||||
maxUses Int? @map("max_uses")
|
||||
usedCount Int @default(0) @map("used_count")
|
||||
expiresAt DateTime? @map("expires_at") @db.Timestamptz()
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code String @unique @db.VarChar(50)
|
||||
type String @default("percent") @db.VarChar(20) // fixed, percent
|
||||
value Decimal @db.Decimal(15, 2)
|
||||
minCartValue Decimal? @map("min_cart_value") @db.Decimal(15, 2)
|
||||
maxCartValue Decimal? @map("max_cart_value") @db.Decimal(15, 2)
|
||||
maxUses Int? @map("max_uses")
|
||||
usedCount Int @default(0) @map("used_count")
|
||||
expiresAt DateTime? @map("expires_at") @db.Timestamptz()
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
orders Order[]
|
||||
targets CouponTarget[]
|
||||
orders Order[]
|
||||
targets CouponTarget[]
|
||||
|
||||
@@map("coupons")
|
||||
}
|
||||
|
||||
model CouponTarget {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
couponId String @map("coupon_id") @db.Uuid
|
||||
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
|
||||
modifierType String @default("override") @map("modifier_type") @db.VarChar(20) // override, add, subtract
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
couponId String @map("coupon_id") @db.Uuid
|
||||
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
|
||||
modifierType String @default("override") @map("modifier_type") @db.VarChar(20) // override, add, subtract
|
||||
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])
|
||||
@@map("coupon_targets")
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
couponId String? @map("coupon_id") @db.Uuid
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(15, 2)
|
||||
charityDonation Decimal @default(0.00) @map("charity_donation") @db.Decimal(15, 2)
|
||||
isRefill Boolean @default(false) @map("is_refill")
|
||||
refillIntervalDays Int? @map("refill_interval_days")
|
||||
status String @default("processing") @db.VarChar(30) // processing, shipped, delivered
|
||||
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
couponId String? @map("coupon_id") @db.Uuid
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(15, 2)
|
||||
charityDonation Decimal @default(0.00) @map("charity_donation") @db.Decimal(15, 2)
|
||||
isRefill Boolean @default(false) @map("is_refill")
|
||||
refillIntervalDays Int? @map("refill_interval_days")
|
||||
status String @default("processing") @db.VarChar(30) // processing, shipped, delivered
|
||||
trackingNumber String? @unique @map("tracking_number") @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
coupon Coupon? @relation(fields: [couponId], references: [id])
|
||||
orderItems OrderItem[]
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
coupon Coupon? @relation(fields: [couponId], references: [id])
|
||||
orderItems OrderItem[]
|
||||
|
||||
@@map("orders")
|
||||
}
|
||||
@ -330,8 +334,8 @@ model OrderItem {
|
||||
doseQty Decimal? @map("dose_qty") @db.Decimal(10, 2)
|
||||
doseUnit String? @map("dose_unit") @db.VarChar(50)
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("order_items")
|
||||
}
|
||||
@ -344,8 +348,8 @@ model UiText {
|
||||
}
|
||||
|
||||
model ScientificTerm {
|
||||
key String @id @db.VarChar(100)
|
||||
term String @db.VarChar(150)
|
||||
key String @id @db.VarChar(100)
|
||||
term String @db.VarChar(150)
|
||||
definition String @db.Text
|
||||
wikiId String @map("wiki_id") @db.VarChar(50)
|
||||
metaTitle String? @map("meta_title") @db.VarChar(200)
|
||||
@ -356,42 +360,42 @@ model ScientificTerm {
|
||||
}
|
||||
|
||||
model HeroBanner {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
title String @db.VarChar(200)
|
||||
subtitle String? @db.Text
|
||||
imageUrl String @map("image_url") @db.Text
|
||||
buttonText String? @map("button_text") @db.VarChar(100)
|
||||
buttonLink String? @map("button_link") @db.Text
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
title String @db.VarChar(200)
|
||||
subtitle String? @db.Text
|
||||
imageUrl String @map("image_url") @db.Text
|
||||
buttonText String? @map("button_text") @db.VarChar(100)
|
||||
buttonLink String? @map("button_link") @db.Text
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
@@map("hero_banners")
|
||||
}
|
||||
|
||||
model VetTestimonial {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
vetName String @map("vet_name") @db.VarChar(150)
|
||||
clinicName String? @map("clinic_name") @db.VarChar(150)
|
||||
imageUrl String? @map("image_url") @db.Text
|
||||
quote String @db.Text
|
||||
rating Int @default(5)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
vetName String @map("vet_name") @db.VarChar(150)
|
||||
clinicName String? @map("clinic_name") @db.VarChar(150)
|
||||
imageUrl String? @map("image_url") @db.Text
|
||||
quote String @db.Text
|
||||
rating Int @default(5)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
@@map("vet_testimonials")
|
||||
}
|
||||
|
||||
model SmartAdvisorRule {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
condition String @db.VarChar(200)
|
||||
targetPetType String? @map("target_pet_type") @db.VarChar(50)
|
||||
condition String @db.VarChar(200)
|
||||
targetPetType String? @map("target_pet_type") @db.VarChar(50)
|
||||
recommendedProduct String @map("recommended_product_id") @db.Uuid
|
||||
reason String @db.Text
|
||||
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")
|
||||
}
|
||||
@ -427,14 +431,122 @@ model ContactSubmission {
|
||||
}
|
||||
|
||||
model ContactInfo {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
key String @unique @db.VarChar(100)
|
||||
title String @db.VarChar(200)
|
||||
value String @db.Text
|
||||
icon String? @db.VarChar(100)
|
||||
order Int @default(0)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
key String @unique @db.VarChar(100)
|
||||
title String @db.VarChar(200)
|
||||
value String @db.Text
|
||||
icon String? @db.VarChar(100)
|
||||
order Int @default(0)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
|
||||
|
||||
@@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 { SmsModule } from './common/sms.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({
|
||||
imports: [
|
||||
@ -48,6 +54,12 @@ import { ContactModule } from './contact/contact.module';
|
||||
CmsModule,
|
||||
WholesaleModule,
|
||||
VideosModule,
|
||||
BannersModule,
|
||||
SmartAdvisorModule,
|
||||
TestimonialsModule,
|
||||
IngredientsModule,
|
||||
PrescriptionsModule,
|
||||
B2BModule,
|
||||
],
|
||||
controllers: [MetricsController],
|
||||
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 {
|
||||
Controller,
|
||||
Get,
|
||||
Patch,
|
||||
Body,
|
||||
Query,
|
||||
Param,
|
||||
NotFoundException,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ProductsService } from './products.service';
|
||||
import { GetProductsDto } from './dto/get-products.dto';
|
||||
@ -15,62 +18,21 @@ import {
|
||||
ApiOkResponse,
|
||||
ApiNotFoundResponse,
|
||||
} 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 - مدیریت محصولات دارویی')
|
||||
@Controller('products')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: 'خطای داخلی سرور',
|
||||
schema: {
|
||||
example: {
|
||||
success: false,
|
||||
message: 'خطای داخلی سرور',
|
||||
code: 'SERVER_ERROR',
|
||||
details: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
export class ProductsController {
|
||||
constructor(private readonly productsService: ProductsService) {}
|
||||
|
||||
@Get()
|
||||
@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) {
|
||||
return this.productsService.findAll(query);
|
||||
}
|
||||
@ -79,16 +41,6 @@ export class ProductsController {
|
||||
@ApiOperation({
|
||||
summary: 'دریافت فیلترهای فعال محصولات (دسته، علائم، نوع پت)',
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: 'لیست فیلترهای پویا استخراج شده از دیتابیس',
|
||||
schema: {
|
||||
example: {
|
||||
categories: [{ id: '1', name: 'مفاصل و استخوان', slug: 'joints' }],
|
||||
symptoms: ['لنگش', 'ریزش مو'],
|
||||
petTypes: ['سگ', 'گربه', 'هر دو'],
|
||||
},
|
||||
},
|
||||
})
|
||||
getActiveFilters() {
|
||||
return this.productsService.getActiveFilters();
|
||||
}
|
||||
@ -97,72 +49,28 @@ export class ProductsController {
|
||||
@ApiOperation({
|
||||
summary: 'دریافت فیلترها و راهکارهای ناوبری (دسته با علائم مربوطه)',
|
||||
})
|
||||
@ApiOkResponse({
|
||||
description: 'ساختار درختی فیلترها و تگهای درمانی واقعی متصل به محصولات',
|
||||
schema: {
|
||||
example: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'مفاصل و استخوان',
|
||||
slug: 'joints',
|
||||
symptoms: ['درد مفاصل', 'لنگش'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
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')
|
||||
@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) {
|
||||
const product = await this.productsService.findOne(id);
|
||||
if (!product) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { GetProductsDto } from './dto/get-products.dto';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class ProductsService {
|
||||
@ -21,7 +22,7 @@ export class ProductsService {
|
||||
sortOrder = 'desc',
|
||||
} = filters;
|
||||
|
||||
const andConditions: any[] = [];
|
||||
const andConditions: Prisma.ProductWhereInput[] = [];
|
||||
|
||||
if (requiresRx !== undefined && requiresRx !== null && requiresRx !== '') {
|
||||
andConditions.push({
|
||||
@ -75,7 +76,7 @@ export class ProductsService {
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause: any =
|
||||
const whereClause: Prisma.ProductWhereInput =
|
||||
andConditions.length > 0 ? { AND: andConditions } : {};
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
@ -102,13 +103,11 @@ export class ProductsService {
|
||||
const isAdmin = userRole === 'ADMIN' || userRole === 'SuperAdmin';
|
||||
|
||||
const data = rawProducts.map((p) => {
|
||||
// Remove buyPrice for all non-admins
|
||||
const { buyPrice, ...withoutBuyPrice } = p;
|
||||
const { buyPrice: _b, wholesalePrice: _w, ...publicProduct } = p;
|
||||
if (!isWholesaleOrAdmin) {
|
||||
const { wholesalePrice, ...publicProduct } = withoutBuyPrice;
|
||||
return publicProduct;
|
||||
}
|
||||
return isAdmin ? p : withoutBuyPrice;
|
||||
return isAdmin ? p : { ...publicProduct, wholesalePrice: p.wholesalePrice };
|
||||
});
|
||||
|
||||
return {
|
||||
@ -143,12 +142,11 @@ export class ProductsService {
|
||||
userRole === 'SuperAdmin';
|
||||
const isAdmin = userRole === 'ADMIN' || userRole === 'SuperAdmin';
|
||||
|
||||
const { buyPrice, ...withoutBuyPrice } = product;
|
||||
const { buyPrice: _b, wholesalePrice: _w, ...publicProduct } = product;
|
||||
if (!isWholesaleOrAdmin) {
|
||||
const { wholesalePrice, ...publicProduct } = withoutBuyPrice;
|
||||
return publicProduct;
|
||||
}
|
||||
return isAdmin ? product : withoutBuyPrice;
|
||||
return isAdmin ? product : { ...publicProduct, wholesalePrice: product.wholesalePrice };
|
||||
}
|
||||
|
||||
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,
|
||||
Param,
|
||||
UseGuards,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
@ -17,12 +16,10 @@ import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiResponse,
|
||||
ApiOkResponse,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Settings - تنظیمات متون پویا و واژهنامه علمی')
|
||||
@ApiTags('Settings - تنظیمات متون پویا، تنظیمات سئو، مالی و سیستم')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('Admin')
|
||||
@Controller('settings')
|
||||
@ -32,15 +29,7 @@ export class SettingsController {
|
||||
@Get('ui-texts')
|
||||
@ApiOperation({ summary: 'دریافت تمامی متون و پیکربندیهای رابط کاربری' })
|
||||
@ApiOkResponse({
|
||||
description:
|
||||
'یک آبجکت کلید-مقدار حاوی تمامی متون، شعارها و برچسبهای دکمهها',
|
||||
schema: {
|
||||
example: {
|
||||
hero_badge: 'تخصص دارویی از آلمان',
|
||||
hero_title: 'تخصص آلمانی در خدمت سلامت پتهای خانگی',
|
||||
hero_desc: 'بیش از ۴۰ سال تجربه نوآورانه...',
|
||||
},
|
||||
},
|
||||
description: 'یک آبجکت کلید-مقدار حاوی تمامی متون، شعارها و برچسبهای دکمهها',
|
||||
})
|
||||
getUiTexts() {
|
||||
return this.settingsService.getUiTexts();
|
||||
@ -49,45 +38,13 @@ export class SettingsController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Patch('ui-texts/:key')
|
||||
@ApiOperation({ summary: 'ویرایش متن یک کلید در رابط کاربری (نیازمند توکن)' })
|
||||
@ApiOkResponse({
|
||||
description: 'متن با موفقیت بهروزرسانی شد',
|
||||
schema: {
|
||||
example: {
|
||||
key: 'hero_badge',
|
||||
value: 'تخصص دارویی ممتاز از آلمان',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'عدم دسترسی به دلیل عدم احراز هویت',
|
||||
schema: {
|
||||
example: {
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
code: 'UNAUTHORIZED',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: 'ویرایش متن یک کلید در رابط کاربری' })
|
||||
updateUiText(@Param('key') key: string, @Body('value') value: string) {
|
||||
return this.settingsService.updateUiText(key, value);
|
||||
}
|
||||
|
||||
@Get('scientific-terms')
|
||||
@ApiOperation({ summary: 'دریافت تمامی اصطلاحات واژهنامه علمی' })
|
||||
@ApiOkResponse({
|
||||
description: 'لیست کامل اصطلاحات علمی به همراه تعاریف و شناسهها',
|
||||
schema: {
|
||||
example: [
|
||||
{
|
||||
key: 'green-mussel',
|
||||
term: 'صدف لبسبز (Perna Canaliculus)',
|
||||
definition: 'این صدف بومی سواحل بکر نیوزیلند است...',
|
||||
wikiId: 'general',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
getScientificTerms() {
|
||||
return this.settingsService.getScientificTerms();
|
||||
}
|
||||
@ -95,28 +52,7 @@ export class SettingsController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Put('scientific-terms/:key')
|
||||
@ApiOperation({ summary: 'ثبت یا ویرایش یک اصطلاح علمی (نیازمند توکن)' })
|
||||
@ApiOkResponse({
|
||||
description: 'اصطلاح علمی ثبت یا ویرایش شد',
|
||||
schema: {
|
||||
example: {
|
||||
key: 'green-mussel',
|
||||
term: 'صدف لبسبز اصل نیوزیلند',
|
||||
definition: 'این صدف بومی سواحل بکر نیوزیلند است...',
|
||||
wikiId: 'general',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'عدم دسترسی',
|
||||
schema: {
|
||||
example: {
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
code: 'UNAUTHORIZED',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: 'ثبت یا ویرایش یک اصطلاح علمی' })
|
||||
upsertScientificTerm(@Param('key') key: string, @Body() data: any) {
|
||||
return this.settingsService.upsertScientificTerm(key, data);
|
||||
}
|
||||
@ -124,27 +60,47 @@ export class SettingsController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Delete('scientific-terms/:key')
|
||||
@ApiOperation({ summary: 'حذف یک اصطلاح علمی (نیازمند توکن)' })
|
||||
@ApiOkResponse({
|
||||
description: 'اصطلاح علمی حذف شد',
|
||||
schema: {
|
||||
example: {
|
||||
success: true,
|
||||
message: 'Scientific term successfully deleted',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'عدم دسترسی',
|
||||
schema: {
|
||||
example: {
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
code: 'UNAUTHORIZED',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiOperation({ summary: 'حذف یک اصطلاح علمی' })
|
||||
deleteScientificTerm(@Param('key') key: string) {
|
||||
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) {
|
||||
const term = String(data?.term || '');
|
||||
const definition = String(data?.definition || '');
|
||||
const wikiId = String(data?.wikiId || 'general');
|
||||
|
||||
return this.prisma.scientificTerm.upsert({
|
||||
where: { key },
|
||||
update: {
|
||||
term: data.term,
|
||||
definition: data.definition,
|
||||
wikiId: data.wikiId || 'general',
|
||||
term,
|
||||
definition,
|
||||
wikiId,
|
||||
},
|
||||
create: {
|
||||
key,
|
||||
term: data.term,
|
||||
definition: data.definition,
|
||||
wikiId: data.wikiId || 'general',
|
||||
term,
|
||||
definition,
|
||||
wikiId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -43,4 +47,20 @@ export class SettingsService {
|
||||
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.setGlobalPrefix('api');
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ transform: true, whitelist: true }),
|
||||
);
|
||||
app.useGlobalFilters(
|
||||
new CustomHttpExceptionFilter(),
|
||||
new PrismaExceptionFilter(),
|
||||
@ -76,12 +78,12 @@ describe('Phase 4 Master Backlog E2E Verification Matrix (e2e)', () => {
|
||||
}
|
||||
};
|
||||
|
||||
expect(() => validateStartup('short', process.env.JWT_REFRESH_SECRET)).toThrow(
|
||||
'FATAL: JWT_ACCESS_SECRET missing or short',
|
||||
);
|
||||
expect(() => validateStartup(process.env.JWT_ACCESS_SECRET, 'short')).toThrow(
|
||||
'FATAL: JWT_REFRESH_SECRET missing or short',
|
||||
);
|
||||
expect(() =>
|
||||
validateStartup('short', process.env.JWT_REFRESH_SECRET),
|
||||
).toThrow('FATAL: JWT_ACCESS_SECRET missing or short');
|
||||
expect(() =>
|
||||
validateStartup(process.env.JWT_ACCESS_SECRET, 'short'),
|
||||
).toThrow('FATAL: JWT_REFRESH_SECRET missing or short');
|
||||
expect(() =>
|
||||
validateStartup(
|
||||
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