feat(backend): implement Phase A database models and controllers for cross-boundary dependencies
Some checks failed
Deploy Canina / deploy (push) Failing after 28s

This commit is contained in:
پارسا آقایی 2026-08-06 23:45:51 +03:30
parent 74fcaa6a4e
commit 1ae4c99ffc
26 changed files with 1205 additions and 345 deletions

View File

@ -26,6 +26,8 @@ model User {
pets Pet[] pets Pet[]
orders Order[] orders Order[]
blogs Blog[] blogs Blog[]
prescriptions Prescription[]
partnerAccount PartnerAccount?
@@map("users") @@map("users")
} }
@ -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([])
@ -212,6 +215,7 @@ model Pet {
medicalConditions PetMedicalCondition[] medicalConditions PetMedicalCondition[]
reminders Reminder[] reminders Reminder[]
healthLogs HealthLog[] healthLogs HealthLog[]
prescriptions Prescription[]
@@map("pets") @@map("pets")
} }
@ -438,3 +442,111 @@ model ContactInfo {
@@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")
}

View File

@ -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: [

View 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);
}
}

View 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 {}

View 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 },
});
}
}

View 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);
}
}

View 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 {}

View 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' };
}
}

View 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);
}
}

View 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 {}

View 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 } });
}
}

View 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);
}
}

View 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 {}

View 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 },
});
}
}

View File

@ -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) {

View File

@ -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 },
});
}
} }

View File

@ -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);
}
} }

View File

@ -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 },
});
}
} }

View 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);
}
}

View 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 {}

View 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 } });
}
}

View 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);
}
}

View 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 {}

View 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 } });
}
}

View File

@ -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,

View 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.