282 lines
8.9 KiB
TypeScript
282 lines
8.9 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||
import { CreateRestaurantDto } from '../dto/create-shop.dto';
|
||
import { UpdateRestaurantDto } from '../dto/update-shop.dto';
|
||
import { UpgradeSubscriptionDto } from '../dto/upgrade-subscription.dto';
|
||
import { Shop } from '../entities/shop.entity';
|
||
import { EntityManager } from '@mikro-orm/postgresql';
|
||
import { RestRepository } from '../repositories/rest.repository';
|
||
import { RestMessage } from 'src/common/enums/message.enum';
|
||
import { FindRestaurantsDto } from ../../..shops.dto';
|
||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||
import { PlanEnum } from '../interface/plan.interface';
|
||
import { Admin } from '../../../admin/entities/admin.entity';
|
||
import { AdminRole } from '../../../admin/entities/adminRole.entity';
|
||
import { Role } from '../../../roles/entities/role.entity';
|
||
import { normalizePhone } from 'src/modules/utils/phone.util';
|
||
import slugify from 'slugify';
|
||
import { NotificationPreference } from '../../../notifications/entities/notification-preference.entity';
|
||
import { notificationPreferencesData } from '../../../../seeders/data/notification-preferences.data';
|
||
import { Order } from '../../../orders/entities/order.entity';
|
||
import { Coupon } from '../../../coupons/entities/coupon.entity';
|
||
import { Category } from ../../..products/entities/category.entity';
|
||
import { Product } from ../../..products/entities/product.entity';
|
||
import { Delivery } from '../../../delivery/entities/delivery.entity';
|
||
import { PaymentMethod } from '../../../payments/entities/payment-method.entity';
|
||
import { Pager } from '../../../pager/entities/pager.entity';
|
||
import { Schedule } from '../entities/schedule.entity';
|
||
import { WalletTransaction } from '../../../users/entities/wallet-transaction.entity';
|
||
import { SmsLog } from '../../../notifications/entities/smsLogs.entity';
|
||
import { Notification } from '../../../notifications/entities/notification.entity';
|
||
|
||
|
||
@Injectable()
|
||
export class RestaurantsService {
|
||
constructor(
|
||
private readonly em: EntityManager,
|
||
private readonly restRepository: RestRepository,
|
||
) { }
|
||
|
||
async setupRestuarant(dto: CreateRestaurantDto): Promise<Shop> {
|
||
return await this.em.transactional(async (em) => {
|
||
// Generate or validate slug
|
||
const slug = slugify(dto.slug ?? dto.name, {
|
||
lower: true,
|
||
strict: true,
|
||
locale: 'fa'
|
||
});
|
||
|
||
|
||
const validateSubscriptionId = await em.findOne(Shop, { subscriptionId: dto.subscriptionId })
|
||
if (validateSubscriptionId) {
|
||
throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId)
|
||
}
|
||
|
||
// Create shop
|
||
const shop = em.create(Shop, {
|
||
name: dto.name,
|
||
slug,
|
||
establishedYear: dto.establishedYear,
|
||
phone: dto.phone,
|
||
isActive: true,
|
||
plan: dto.plan,
|
||
domain: `https://dmenu.danakcorp.com/${slug}`,
|
||
subscriptionId: dto.subscriptionId,
|
||
subscriptionEndDate: dto.subscriptionEndDate,
|
||
subscriptionStartDate: dto.subscriptionStartDate,
|
||
});
|
||
|
||
// Find the appropriate role based on plan
|
||
const roleName = dto.plan === PlanEnum.Base ? 'مدیر (پلن پایه)' : 'مدیر ( پلن ویژه)';
|
||
const role = await em.findOne(Role, { name: roleName });
|
||
if (!role) {
|
||
throw new BadRequestException(`Role not found for plan: ${dto.plan}`);
|
||
}
|
||
|
||
const normalizedPhone = normalizePhone(dto.phone);
|
||
let admin = await em.findOne(Admin, { phone: normalizedPhone });
|
||
if (!admin) {
|
||
admin = em.create(Admin, {
|
||
phone: normalizedPhone,
|
||
firstName: '[نام]',
|
||
lastName: '[نام خانوادگی]',
|
||
});
|
||
}
|
||
|
||
// Create admin role relationship
|
||
const adminRole = em.create(AdminRole, {
|
||
admin,
|
||
role,
|
||
shop,
|
||
});
|
||
|
||
// Create notification preferences for the shop
|
||
const notificationPreferences = notificationPreferencesData.map(preferenceData =>
|
||
em.create(NotificationPreference, {
|
||
shop,
|
||
title: preferenceData.title,
|
||
channels: preferenceData.channels,
|
||
})
|
||
);
|
||
|
||
// Persist all entities
|
||
em.persist([shop, admin, adminRole, ...notificationPreferences]);
|
||
await em.flush();
|
||
|
||
return shop;
|
||
});
|
||
}
|
||
|
||
async findAll(dto: FindRestaurantsDto): Promise<PaginatedResult<Shop>> {
|
||
return this.restRepository.findAllPaginated({
|
||
page: dto.page,
|
||
limit: dto.limit,
|
||
search: dto.search,
|
||
isActive: dto.isActive,
|
||
plan: dto.plan,
|
||
orderBy: dto.orderBy,
|
||
order: dto.order,
|
||
});
|
||
}
|
||
|
||
async findBySlug(slug: string): Promise<Shop> {
|
||
const shop = await this.em.findOne(Shop, { slug });
|
||
|
||
if (!shop) {
|
||
throw new NotFoundException(`Shop with slug "${slug}" not found`);
|
||
}
|
||
|
||
return shop;
|
||
}
|
||
|
||
async findOne(id: string): Promise<Shop> {
|
||
const shop = await this.restRepository.findOne({ id });
|
||
|
||
if (!shop) {
|
||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||
}
|
||
|
||
return shop;
|
||
}
|
||
|
||
async findOneBySubscriptionId(subscriptionId: string): Promise<Shop> {
|
||
console.log('subscriptionId', subscriptionId)
|
||
const shop = await this.restRepository.findOne({ subscriptionId });
|
||
console.log('shop', shop)
|
||
if (!shop) {
|
||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||
}
|
||
return shop;
|
||
}
|
||
|
||
async getRestaurantSpecification(slug: string) {
|
||
const shop = await this.findBySlug(slug);
|
||
return shop;
|
||
}
|
||
// TODO : it must be done inside transaction
|
||
async update(id: string, dto: UpdateRestaurantDto): Promise<Shop> {
|
||
|
||
const shop = await this.restRepository.findOne({ id: id });
|
||
|
||
if (!shop) {
|
||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||
}
|
||
|
||
if (dto.plan && dto.plan !== shop.plan) {
|
||
const isPremium = dto.plan === PlanEnum.Premium
|
||
// find admins and make them premium or base
|
||
const adminRoles = await this.em.find(AdminRole, {
|
||
shop: {
|
||
id
|
||
},
|
||
role: {
|
||
name: isPremium ? 'مدیر (پلن پایه)' : 'مدیر ( پلن ویژه)'
|
||
}
|
||
})
|
||
const newRole = await this.em.findOne(Role, {
|
||
name: isPremium ? 'مدیر ( پلن ویژه)' : 'مدیر (پلن پایه)'
|
||
})
|
||
if (!newRole) {
|
||
throw new BadRequestException("Role Not Found")
|
||
}
|
||
adminRoles.forEach(a => {
|
||
a.role = newRole
|
||
})
|
||
|
||
this.em.persistAndFlush(adminRoles)
|
||
|
||
}
|
||
|
||
this.restRepository.assign(shop, dto);
|
||
|
||
await this.em.persistAndFlush(shop);
|
||
|
||
return shop;
|
||
}
|
||
|
||
async remove(id: string) {
|
||
// Soft delete the shop by setting isActive to false
|
||
const shop = await this.restRepository.findOne({ id });
|
||
|
||
if (!shop) {
|
||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||
}
|
||
|
||
shop.isActive = false;
|
||
await this.em.persistAndFlush(shop);
|
||
|
||
return shop;
|
||
}
|
||
|
||
async upgradeSubscription(subscriptionId: string, dto: UpgradeSubscriptionDto): Promise<Shop> {
|
||
const shop = await this.restRepository.findOne({ subscriptionId });
|
||
|
||
if (!shop) {
|
||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||
}
|
||
|
||
const isActive = new Date(dto.subscriptionEndDate) > new Date()
|
||
|
||
await this.update(shop.id, {
|
||
plan: dto.newPlan,
|
||
subscriptionEndDate: dto.subscriptionEndDate,
|
||
isActive
|
||
})
|
||
|
||
return shop;
|
||
}
|
||
|
||
async hardDeleteRestaurant(id: string): Promise<void> {
|
||
return await this.em.transactional(async (em) => {
|
||
// Find the shop first
|
||
const shop = await em.findOne(Shop, { id });
|
||
|
||
if (!shop) {
|
||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||
}
|
||
|
||
// Delete orders first (will cascade to order items and payments due to cascade settings)
|
||
await em.nativeDelete(Order, { shop: id });
|
||
|
||
// Delete coupons
|
||
await em.nativeDelete(Coupon, { shop: id });
|
||
|
||
// Delete products (will cascade to reviews and favorites due to cascade settings)
|
||
await em.nativeDelete(Product, { shop: id });
|
||
|
||
// Delete categories (products should be deleted by cascade, but delete explicitly to be safe)
|
||
await em.nativeDelete(Category, { shop: id });
|
||
|
||
// Delete deliveries
|
||
await em.nativeDelete(Delivery, { shop: id });
|
||
|
||
// Delete schedules
|
||
await em.nativeDelete(Schedule, { restId: id });
|
||
|
||
// Delete notification preferences
|
||
await em.nativeDelete(NotificationPreference, { shop: id });
|
||
|
||
// Delete notifications
|
||
await em.nativeDelete(Notification, { shop: id });
|
||
|
||
// Delete SMS logs
|
||
await em.nativeDelete(SmsLog, { shop: id });
|
||
|
||
// Delete admin roles for this shop
|
||
await em.nativeDelete(AdminRole, { shop: id });
|
||
|
||
// Delete payment methods
|
||
await em.nativeDelete(PaymentMethod, { shop: id });
|
||
|
||
// Delete pagers
|
||
await em.nativeDelete(Pager, { shop: id });
|
||
|
||
// Delete wallet transactions
|
||
await em.nativeDelete(WalletTransaction, { shop: id });
|
||
|
||
// Finally, delete the shop itself
|
||
await em.nativeDelete(Shop, { id });
|
||
});
|
||
}
|
||
|
||
}
|