749 lines
22 KiB
TypeScript
749 lines
22 KiB
TypeScript
import {
|
|
Injectable,
|
|
HttpException,
|
|
NotFoundException,
|
|
BadRequestException,
|
|
} from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { RedisService } from '../redis/redis.service';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
|
|
import { normalizeMobile } from '../common/utils/phone.utils';
|
|
|
|
export class PaginationQuery {
|
|
page?: number | string;
|
|
limit?: number | string;
|
|
search?: string;
|
|
role?: string;
|
|
categoryId?: string;
|
|
status?: string;
|
|
}
|
|
|
|
export class ProductInput {
|
|
artNo?: string;
|
|
nameFa?: string;
|
|
nameEn?: string;
|
|
scientificTagline?: string;
|
|
description?: string;
|
|
shortDescription?: string;
|
|
categoryId?: string;
|
|
categorySlug?: string;
|
|
priceValue?: number;
|
|
priceDisplay?: string;
|
|
unit?: string;
|
|
packageSize?: number;
|
|
dosageLogic?: string;
|
|
suitableFor?: string;
|
|
imageUrl?: string;
|
|
images?: string | string[];
|
|
podcastUrl?: string | null;
|
|
videoUrl?: string | null;
|
|
pdfUrl?: string | null;
|
|
metaTitle?: string;
|
|
metaDescription?: string;
|
|
keywords?: string;
|
|
canonicalUrl?: string;
|
|
slug?: string;
|
|
symptoms?: string[];
|
|
}
|
|
|
|
export class CouponTargetInput {
|
|
targetType!: string;
|
|
targetId!: string;
|
|
modifierType?: string;
|
|
modifierValue?: number;
|
|
}
|
|
|
|
export class CouponInput {
|
|
code!: string;
|
|
type?: string;
|
|
value!: number;
|
|
minCartValue?: number;
|
|
maxCartValue?: number;
|
|
maxUses?: number;
|
|
expiresAt?: string | Date;
|
|
isActive?: boolean;
|
|
targets?: CouponTargetInput[];
|
|
}
|
|
|
|
@Injectable()
|
|
export class AdminService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private redisService: RedisService,
|
|
) {}
|
|
|
|
async getDashboardStats() {
|
|
const orders = await this.prisma.order.findMany({
|
|
where: { status: { not: 'failed' } },
|
|
select: { totalAmount: true },
|
|
});
|
|
|
|
const revenue = orders.reduce((sum, o) => sum + Number(o.totalAmount), 0);
|
|
|
|
const newOrders = await this.prisma.order.count({
|
|
where: { status: 'processing' },
|
|
});
|
|
|
|
const users = await this.prisma.user.count();
|
|
|
|
const todayKey = `visits:${new Date().toISOString().split('T')[0]}`;
|
|
let todayVisits = 0;
|
|
try {
|
|
const count = await this.redisService.get(todayKey);
|
|
todayVisits = count ? parseInt(count, 10) : 0;
|
|
} catch {
|
|
todayVisits = 0;
|
|
}
|
|
|
|
const [dogCount, catCount, bothCount] = await Promise.all([
|
|
this.prisma.product.count({ where: { suitableFor: 'سگ' } }),
|
|
this.prisma.product.count({ where: { suitableFor: 'گربه' } }),
|
|
this.prisma.product.count({
|
|
where: { suitableFor: { contains: 'هر دو' } },
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
revenue,
|
|
newOrders,
|
|
users,
|
|
todayVisits,
|
|
categoriesDistribution: [
|
|
{ name: 'مکمل سگ', value: dogCount || 8 },
|
|
{ name: 'مکمل گربه', value: catCount || 6 },
|
|
{ name: 'هر دو (سگ و گربه)', value: bothCount || 12 },
|
|
],
|
|
};
|
|
}
|
|
|
|
async getUsers(query: PaginationQuery = {}) {
|
|
const page = Number(query.page) || 1;
|
|
const limit = Number(query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.UserWhereInput = {};
|
|
if (query.search) {
|
|
where.OR = [
|
|
{ firstName: { contains: query.search, mode: 'insensitive' } },
|
|
{ lastName: { contains: query.search, mode: 'insensitive' } },
|
|
{ email: { contains: query.search, mode: 'insensitive' } },
|
|
{ mobile: { contains: query.search } },
|
|
];
|
|
}
|
|
if (query.role) {
|
|
if (query.role === 'B2B') {
|
|
where.role = { contains: 'B2B' };
|
|
} else {
|
|
where.role = query.role;
|
|
}
|
|
}
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.user.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
addresses: true,
|
|
walletTransactions: {
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 20,
|
|
},
|
|
},
|
|
}),
|
|
this.prisma.user.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: { total, page, limit, lastPage: Math.ceil(total / limit) },
|
|
};
|
|
}
|
|
|
|
async getUserDetails(id: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id },
|
|
include: {
|
|
addresses: true,
|
|
pets: true,
|
|
orders: { orderBy: { createdAt: 'desc' }, take: 10 },
|
|
walletTransactions: { orderBy: { createdAt: 'desc' }, take: 20 },
|
|
},
|
|
});
|
|
if (!user) throw new NotFoundException(`کاربری با شناسه ${id} یافت نشد.`);
|
|
return user;
|
|
}
|
|
|
|
async createUser(dto: CreateUserDto) {
|
|
const mobile = normalizeMobile(dto.mobile);
|
|
const existingMobile = await this.prisma.user.findUnique({
|
|
where: { mobile },
|
|
});
|
|
if (existingMobile) {
|
|
throw new BadRequestException('کاربری با این شماره موبایل قبلاً در سیستم ثبت شده است');
|
|
}
|
|
|
|
if (dto.email) {
|
|
const existingEmail = await this.prisma.user.findUnique({
|
|
where: { email: dto.email.trim().toLowerCase() },
|
|
});
|
|
if (existingEmail) {
|
|
throw new BadRequestException('کاربری با این آدرس ایمیل قبلاً در سیستم ثبت شده است');
|
|
}
|
|
}
|
|
|
|
const passwordToHash = dto.password?.trim();
|
|
const hashedPassword = passwordToHash ? await bcrypt.hash(passwordToHash, 10) : '';
|
|
|
|
return this.prisma.user.create({
|
|
data: {
|
|
firstName: dto.firstName.trim(),
|
|
lastName: dto.lastName.trim(),
|
|
mobile,
|
|
email: dto.email ? dto.email.trim().toLowerCase() : null,
|
|
password: hashedPassword,
|
|
role: dto.role || 'User_PetOwner',
|
|
walletBalance:
|
|
dto.walletBalance !== undefined
|
|
? new Prisma.Decimal(dto.walletBalance)
|
|
: new Prisma.Decimal(0),
|
|
},
|
|
});
|
|
}
|
|
|
|
async updateUser(id: string, dto: UpdateUserDto) {
|
|
if (id === '12345678-1234-1234-1234-123456789012') {
|
|
throw new BadRequestException('امکان ویرایش حساب ادمین پیشفرض وجود ندارد');
|
|
}
|
|
|
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException(`کاربری با شناسه ${id} یافت نشد`);
|
|
}
|
|
|
|
const normalizedMobile = dto.mobile ? normalizeMobile(dto.mobile) : undefined;
|
|
if (normalizedMobile && normalizedMobile !== user.mobile) {
|
|
const existingMobile = await this.prisma.user.findUnique({
|
|
where: { mobile: normalizedMobile },
|
|
});
|
|
if (existingMobile && existingMobile.id !== id) {
|
|
throw new BadRequestException('این شماره موبایل متعلق به کاربر دیگری است');
|
|
}
|
|
}
|
|
|
|
const normalizedEmail = dto.email ? dto.email.trim().toLowerCase() : undefined;
|
|
if (normalizedEmail && normalizedEmail !== user.email) {
|
|
const existingEmail = await this.prisma.user.findUnique({
|
|
where: { email: normalizedEmail },
|
|
});
|
|
if (existingEmail && existingEmail.id !== id) {
|
|
throw new BadRequestException('این آدرس ایمیل متعلق به کاربر دیگری است');
|
|
}
|
|
}
|
|
|
|
const updateData: Prisma.UserUpdateInput = {};
|
|
if (dto.firstName !== undefined) updateData.firstName = dto.firstName.trim();
|
|
if (dto.lastName !== undefined) updateData.lastName = dto.lastName.trim();
|
|
if (normalizedMobile !== undefined) updateData.mobile = normalizedMobile;
|
|
if (normalizedEmail !== undefined) updateData.email = normalizedEmail || null;
|
|
if (dto.role !== undefined) updateData.role = dto.role;
|
|
if (dto.walletBalance !== undefined) {
|
|
updateData.walletBalance = new Prisma.Decimal(dto.walletBalance);
|
|
}
|
|
if (dto.password && dto.password.trim().length >= 6) {
|
|
updateData.password = await bcrypt.hash(dto.password.trim(), 10);
|
|
}
|
|
|
|
return this.prisma.user.update({
|
|
where: { id },
|
|
data: updateData,
|
|
});
|
|
}
|
|
|
|
async updateUserRole(id: string, role: string) {
|
|
return this.prisma.user.update({
|
|
where: { id },
|
|
data: { role },
|
|
});
|
|
}
|
|
|
|
async deleteUser(id: string) {
|
|
if (id === '12345678-1234-1234-1234-123456789012') {
|
|
throw new BadRequestException('امکان حذف حساب ادمین پیشفرض وجود ندارد');
|
|
}
|
|
|
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException(`کاربری با شناسه ${id} یافت نشد`);
|
|
}
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
await tx.userAddress.deleteMany({ where: { userId: id } });
|
|
await tx.walletTransaction.deleteMany({ where: { userId: id } });
|
|
await tx.pet.deleteMany({ where: { userId: id } });
|
|
await tx.prescription.deleteMany({ where: { userId: id } });
|
|
await tx.paymentTransaction.deleteMany({ where: { userId: id } });
|
|
await tx.partnerAccount.deleteMany({ where: { userId: id } });
|
|
await tx.blog.deleteMany({ where: { authorId: id } });
|
|
await tx.order.deleteMany({ where: { userId: id } });
|
|
return tx.user.delete({ where: { id } });
|
|
});
|
|
}
|
|
|
|
async getProducts(query: PaginationQuery = {}) {
|
|
try {
|
|
const page = Number(query.page) || 1;
|
|
const limit = Number(query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.ProductWhereInput = {};
|
|
if (query.search) {
|
|
where.OR = [
|
|
{ nameFa: { contains: query.search, mode: 'insensitive' } },
|
|
{ artNo: { contains: query.search } },
|
|
];
|
|
}
|
|
if (query.categoryId) {
|
|
where.categoryId = query.categoryId;
|
|
}
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.product.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { category: true, symptoms: true },
|
|
}),
|
|
this.prisma.product.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: { total, page, limit, lastPage: Math.ceil(total / limit) },
|
|
};
|
|
} catch (error) {
|
|
const err = error as { message?: string };
|
|
console.error('[AdminService] getProducts error:', err);
|
|
throw new HttpException(err.message || 'Error fetching products', 500);
|
|
}
|
|
}
|
|
|
|
async createProduct(data: ProductInput) {
|
|
const product = await this.prisma.product.create({
|
|
data: {
|
|
artNo: data.artNo || `ART-${Date.now()}`,
|
|
nameFa: data.nameFa || '',
|
|
nameEn: data.nameEn || '',
|
|
scientificTagline: data.scientificTagline || '',
|
|
description: data.description || '',
|
|
shortDescription: data.shortDescription || '',
|
|
categoryId: data.categoryId || '',
|
|
categorySlug: data.categorySlug || 'general',
|
|
priceValue: data.priceValue || 0,
|
|
priceDisplay: data.priceDisplay || '',
|
|
unit: data.unit || 'عدد',
|
|
packageSize: data.packageSize || 100,
|
|
dosageLogic: data.dosageLogic || '',
|
|
suitableFor: data.suitableFor || 'سگ',
|
|
imageUrl: data.imageUrl || '',
|
|
images: Array.isArray(data.images)
|
|
? data.images
|
|
: data.images
|
|
? [data.images]
|
|
: [],
|
|
podcastUrl: data.podcastUrl || null,
|
|
videoUrl: data.videoUrl || null,
|
|
pdfUrl: data.pdfUrl || null,
|
|
metaTitle: data.metaTitle || '',
|
|
metaDescription: data.metaDescription || '',
|
|
keywords: data.keywords || '',
|
|
canonicalUrl: data.canonicalUrl || '',
|
|
slug: data.slug || data.artNo || `slug-${Date.now()}`,
|
|
},
|
|
});
|
|
|
|
if (data.symptoms && Array.isArray(data.symptoms)) {
|
|
await this.prisma.productSymptom.createMany({
|
|
data: data.symptoms
|
|
.map((s: string) => ({
|
|
productId: product.id,
|
|
symptom: s.trim(),
|
|
}))
|
|
.filter((s: { symptom: string }) => s.symptom.length > 0),
|
|
});
|
|
}
|
|
|
|
return product;
|
|
}
|
|
|
|
async updateProduct(id: string, data: ProductInput) {
|
|
const existing = await this.prisma.product.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new NotFoundException(`محصولی با شناسه ${id} یافت نشد.`);
|
|
}
|
|
|
|
const product = await this.prisma.product.update({
|
|
where: { id },
|
|
data: {
|
|
artNo: data.artNo,
|
|
nameFa: data.nameFa,
|
|
nameEn: data.nameEn,
|
|
scientificTagline: data.scientificTagline,
|
|
description: data.description,
|
|
shortDescription: data.shortDescription,
|
|
categoryId: data.categoryId,
|
|
categorySlug: data.categorySlug,
|
|
priceValue: data.priceValue,
|
|
priceDisplay: data.priceDisplay,
|
|
unit: data.unit,
|
|
packageSize: data.packageSize,
|
|
dosageLogic: data.dosageLogic,
|
|
suitableFor: data.suitableFor,
|
|
imageUrl: data.imageUrl,
|
|
images: Array.isArray(data.images)
|
|
? data.images
|
|
: data.images
|
|
? [data.images]
|
|
: undefined,
|
|
podcastUrl: data.podcastUrl !== undefined ? data.podcastUrl : undefined,
|
|
videoUrl: data.videoUrl !== undefined ? data.videoUrl : undefined,
|
|
pdfUrl: data.pdfUrl !== undefined ? data.pdfUrl : undefined,
|
|
metaTitle: data.metaTitle,
|
|
metaDescription: data.metaDescription,
|
|
keywords: data.keywords,
|
|
canonicalUrl: data.canonicalUrl,
|
|
slug: data.slug || data.artNo,
|
|
},
|
|
});
|
|
|
|
if (data.symptoms !== undefined && Array.isArray(data.symptoms)) {
|
|
await this.prisma.productSymptom.deleteMany({ where: { productId: id } });
|
|
if (data.symptoms.length > 0) {
|
|
await this.prisma.productSymptom.createMany({
|
|
data: data.symptoms
|
|
.map((s: string) => ({
|
|
productId: id,
|
|
symptom: s.trim(),
|
|
}))
|
|
.filter((s: { symptom: string }) => s.symptom.length > 0),
|
|
});
|
|
}
|
|
}
|
|
|
|
return product;
|
|
}
|
|
|
|
async deleteProduct(id: string) {
|
|
return this.prisma.product.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
async getOrders(query: PaginationQuery = {}) {
|
|
const page = Number(query.page) || 1;
|
|
const limit = Number(query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.OrderWhereInput = {};
|
|
if (query.search) {
|
|
where.OR = [
|
|
{ trackingNumber: { contains: query.search, mode: 'insensitive' } },
|
|
{
|
|
user: { firstName: { contains: query.search, mode: 'insensitive' } },
|
|
},
|
|
{ user: { lastName: { contains: query.search, mode: 'insensitive' } } },
|
|
{ user: { mobile: { contains: query.search } } },
|
|
];
|
|
}
|
|
if (query.status) {
|
|
where.status = query.status;
|
|
}
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.order.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
user: true,
|
|
orderItems: {
|
|
include: {
|
|
product: true,
|
|
},
|
|
},
|
|
coupon: true,
|
|
},
|
|
}),
|
|
this.prisma.order.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: { total, page, limit, lastPage: Math.ceil(total / limit) },
|
|
};
|
|
}
|
|
|
|
async updateOrderStatus(id: string, status: string, trackingNumber?: string) {
|
|
const dataToUpdate: Prisma.OrderUpdateInput = { status };
|
|
if (trackingNumber !== undefined) {
|
|
dataToUpdate.trackingNumber = trackingNumber;
|
|
}
|
|
return this.prisma.order.update({
|
|
where: { id },
|
|
data: dataToUpdate,
|
|
include: {
|
|
user: true,
|
|
orderItems: {
|
|
include: {
|
|
product: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async getCoupons(query: PaginationQuery = {}) {
|
|
const page = Number(query.page) || 1;
|
|
const limit = Number(query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.CouponWhereInput = query.search
|
|
? { code: { contains: query.search, mode: 'insensitive' } }
|
|
: {};
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.coupon.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { targets: true },
|
|
}),
|
|
this.prisma.coupon.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
lastPage: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
}
|
|
|
|
async createCoupon(data: CouponInput) {
|
|
return this.prisma.coupon.create({
|
|
data: {
|
|
code: data.code,
|
|
type: data.type || 'percent',
|
|
value: data.value,
|
|
minCartValue: data.minCartValue || null,
|
|
maxCartValue: data.maxCartValue || null,
|
|
maxUses: data.maxUses || null,
|
|
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
|
|
isActive: data.isActive !== undefined ? data.isActive : true,
|
|
targets:
|
|
data.targets && data.targets.length > 0
|
|
? {
|
|
create: data.targets.map((t) => ({
|
|
targetType: t.targetType,
|
|
targetId: t.targetId,
|
|
modifierType: t.modifierType || 'override',
|
|
modifierValue: t.modifierValue || null,
|
|
})),
|
|
}
|
|
: undefined,
|
|
},
|
|
include: { targets: true },
|
|
});
|
|
}
|
|
|
|
async updateCoupon(id: string, data: CouponInput) {
|
|
await this.prisma.couponTarget.deleteMany({ where: { couponId: id } });
|
|
|
|
return this.prisma.coupon.update({
|
|
where: { id },
|
|
data: {
|
|
code: data.code,
|
|
type: data.type,
|
|
value: data.value,
|
|
minCartValue: data.minCartValue,
|
|
maxCartValue: data.maxCartValue,
|
|
maxUses: data.maxUses,
|
|
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
|
|
isActive: data.isActive,
|
|
targets:
|
|
data.targets && data.targets.length > 0
|
|
? {
|
|
create: data.targets.map((t) => ({
|
|
targetType: t.targetType,
|
|
targetId: t.targetId,
|
|
modifierType: t.modifierType || 'override',
|
|
modifierValue: t.modifierValue || null,
|
|
})),
|
|
}
|
|
: undefined,
|
|
},
|
|
include: { targets: true },
|
|
});
|
|
}
|
|
|
|
async toggleCoupon(id: string, isActive: boolean) {
|
|
return this.prisma.coupon.update({
|
|
where: { id },
|
|
data: { isActive },
|
|
});
|
|
}
|
|
|
|
async deleteCoupon(id: string) {
|
|
return this.prisma.coupon.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
async getSettings() {
|
|
const settings = await this.prisma.uiText.findMany();
|
|
return settings.reduce(
|
|
(acc, curr) => ({ ...acc, [curr.key]: curr.value }),
|
|
{} as Record<string, string>,
|
|
);
|
|
}
|
|
|
|
async updateSettings(data: Record<string, string>) {
|
|
const operations = Object.entries(data).map(([key, value]) => {
|
|
return this.prisma.uiText.upsert({
|
|
where: { key },
|
|
update: { value: String(value) },
|
|
create: { key, value: String(value) },
|
|
});
|
|
});
|
|
|
|
await this.prisma.$transaction(operations);
|
|
return this.getSettings();
|
|
}
|
|
|
|
async getPets(query: PaginationQuery = {}) {
|
|
const page = Number(query.page) || 1;
|
|
const limit = Number(query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.PetWhereInput = {};
|
|
if (query.search) {
|
|
where.OR = [
|
|
{ name: { contains: query.search, mode: 'insensitive' } },
|
|
{ breed: { contains: query.search, mode: 'insensitive' } },
|
|
{
|
|
user: { firstName: { contains: query.search, mode: 'insensitive' } },
|
|
},
|
|
{ user: { lastName: { contains: query.search, mode: 'insensitive' } } },
|
|
];
|
|
}
|
|
|
|
const [data, total] = await Promise.all([
|
|
this.prisma.pet.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
mobile: true,
|
|
email: true,
|
|
},
|
|
},
|
|
medicalConditions: true,
|
|
reminders: true,
|
|
healthLogs: {
|
|
orderBy: { loggedDate: 'desc' },
|
|
take: 10,
|
|
},
|
|
},
|
|
}),
|
|
this.prisma.pet.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
data,
|
|
meta: { total, page, limit, lastPage: Math.ceil(total / limit) },
|
|
};
|
|
}
|
|
|
|
async getDoctors() {
|
|
return this.prisma.doctor.findMany({
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createDoctor(data: {
|
|
name: string;
|
|
title: string;
|
|
avatarUrl?: string;
|
|
bio?: string;
|
|
clinic?: string;
|
|
}) {
|
|
return this.prisma.doctor.create({ data });
|
|
}
|
|
|
|
async updateDoctor(
|
|
id: string,
|
|
data: {
|
|
name?: string;
|
|
title?: string;
|
|
avatarUrl?: string;
|
|
bio?: string;
|
|
clinic?: string;
|
|
},
|
|
) {
|
|
return this.prisma.doctor.update({ where: { id }, data });
|
|
}
|
|
|
|
async deleteDoctor(id: string) {
|
|
return this.prisma.doctor.delete({ where: { id } });
|
|
}
|
|
|
|
async adjustUserWallet(
|
|
userId: string,
|
|
amount: number,
|
|
type: 'deposit' | 'withdrawal' | 'refund',
|
|
description?: string,
|
|
) {
|
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
if (!user) {
|
|
throw new NotFoundException(`کاربری با شناسه ${userId} یافت نشد.`);
|
|
}
|
|
|
|
const transaction = await this.prisma.walletTransaction.create({
|
|
data: {
|
|
userId,
|
|
amount,
|
|
type,
|
|
status: 'completed',
|
|
description: description || 'تغییر دستی توسط مدیر سیستم',
|
|
},
|
|
});
|
|
|
|
const isIncrement = type === 'deposit' || type === 'refund';
|
|
await this.prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
walletBalance: isIncrement
|
|
? { increment: amount }
|
|
: { decrement: amount },
|
|
},
|
|
});
|
|
|
|
return transaction;
|
|
}
|
|
}
|