import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { SetupRestaurantDto } from '../dto/setup-restaurant.dto'; import { UpdateRestaurantDto } from '../dto/update-restaurant.dto'; import { UpdateRestaurantBgDto } from '../dto/update-restaurant-bg.dto'; import { UpgradeSubscriptionDto } from '../dto/upgrade-subscription.dto'; import { Restaurant } from '../entities/restaurant.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { RestRepository } from '../repositories/rest.repository'; import { RestMessage } from 'src/common/enums/message.enum'; import { FindRestaurantsDto } from '../dto/find-restaurants.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 '../../foods/entities/category.entity'; import { Food } from '../../foods/entities/food.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'; import { CacheService } from 'src/modules/utils/cache.service'; import { CacheKeys } from 'src/common/constants/cache-keys.constant'; import { BackgroundRepository } from '../repositories/background.repository'; import { ActiveRestaurantListItemDto } from '../dto/active-restaurant-list-item.dto'; @Injectable() export class RestaurantsService { constructor( private readonly em: EntityManager, private readonly restRepository: RestRepository, private readonly backgroundRepository: BackgroundRepository, private readonly cacheService: CacheService, ) { } async setupRestuarant(dto: SetupRestaurantDto): Promise { 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(Restaurant, { subscriptionId: dto.subscriptionId }) if (validateSubscriptionId) { throw new BadRequestException("Duplicate subscriptionId: " + dto.subscriptionId) } // Create restaurant const restaurant = em.create(Restaurant, { name: dto.name, slug, establishedYear: dto.establishedYear, phones: dto.phone ? [dto.phone] : undefined, 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, restaurant, }); // Create notification preferences for the restaurant const notificationPreferences = notificationPreferencesData.map(preferenceData => em.create(NotificationPreference, { restaurant, title: preferenceData.title, channels: preferenceData.channels, }) ); // Persist all entities em.persist([restaurant, admin, adminRole, ...notificationPreferences]); await em.flush(); await this.invalidateActiveRestaurantsListCache(); return restaurant; }); } async findAll(dto: FindRestaurantsDto): Promise> { 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 { const restaurant = await this.em.findOne(Restaurant, { slug }); if (!restaurant) { throw new NotFoundException(`Restaurant with slug "${slug}" not found`); } return restaurant; } async findOneOrFail(id: string): Promise { const restaurant = await this.restRepository.findOne({ id }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } return restaurant; } async findOneBySubscriptionId(subscriptionId: string): Promise { const restaurant = await this.restRepository.findOne({ subscriptionId }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } return restaurant; } async getRestaurantSpecification(slug: string) { const restaurant = await this.findBySlug(slug); return restaurant; } async findAllActiveRestaurants(): Promise { const restaurants = await this.restRepository.findAllActive(); return restaurants.map(({ slug, name, address, logo, establishedYear }) => ({ slug, name, address, logo, establishedYear, })); } // TODO : it must be done inside transaction async update(id: string, dto: UpdateRestaurantDto): Promise { const restaurant = await this.restRepository.findOne({ id: id }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } if (dto.plan && dto.plan !== restaurant.plan) { const isPremium = dto.plan === PlanEnum.Premium // find admins and make them premium or base const adminRoles = await this.em.find(AdminRole, { restaurant: { 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) } const previousSlug = restaurant.slug; this.restRepository.assign(restaurant, dto); await this.em.persistAndFlush(restaurant); await this.invalidateRestaurantCache(previousSlug); if (restaurant.slug !== previousSlug) { await this.invalidateRestaurantCache(restaurant.slug); } return restaurant; } private async invalidateRestaurantCache(slug?: string): Promise { const tasks = [this.invalidateActiveRestaurantsListCache()]; if (slug) { tasks.push( this.cacheService.del(CacheKeys.restaurantSpec(slug)), this.cacheService.del(CacheKeys.foodsByRestaurant(slug)), this.cacheService.del(CacheKeys.categoriesByRestaurant(slug)), ); } await Promise.all(tasks); } private async invalidateActiveRestaurantsListCache(): Promise { await this.cacheService.del(CacheKeys.activeRestaurantsList()); } async remove(id: string) { // Soft delete the restaurant by setting isActive to false const restaurant = await this.restRepository.findOne({ id }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } restaurant.isActive = false; await this.em.persistAndFlush(restaurant); await this.invalidateRestaurantCache(restaurant.slug); return restaurant; } async upgradeSubscription(subscriptionId: string, dto: UpgradeSubscriptionDto): Promise { const restaurant = await this.restRepository.findOne({ subscriptionId }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } const isActive = new Date(dto.subscriptionEndDate) > new Date() await this.update(restaurant.id, { plan: dto.newPlan, subscriptionEndDate: dto.subscriptionEndDate, isActive }) return restaurant; } async hardDeleteRestaurant(id: string): Promise { return await this.em.transactional(async (em) => { // Find the restaurant first const restaurant = await em.findOne(Restaurant, { id }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } // Delete orders first (will cascade to order items and payments due to cascade settings) await em.nativeDelete(Order, { restaurant: id }); // Delete coupons await em.nativeDelete(Coupon, { restaurant: id }); // Delete foods (will cascade to reviews, favorites, and inventory due to cascade settings) await em.nativeDelete(Food, { restaurant: id }); // Delete categories (foods should be deleted by cascade, but delete explicitly to be safe) await em.nativeDelete(Category, { restaurant: id }); // Delete deliveries await em.nativeDelete(Delivery, { restaurant: id }); // Delete schedules await em.nativeDelete(Schedule, { restId: id }); // Delete notification preferences await em.nativeDelete(NotificationPreference, { restaurant: id }); // Delete notifications await em.nativeDelete(Notification, { restaurant: id }); // Delete SMS logs await em.nativeDelete(SmsLog, { restaurant: id }); // Delete admin roles for this restaurant await em.nativeDelete(AdminRole, { restaurant: id }); // Delete payment methods await em.nativeDelete(PaymentMethod, { restaurant: id }); // Delete pagers await em.nativeDelete(Pager, { restaurant: id }); // Delete wallet transactions await em.nativeDelete(WalletTransaction, { restaurant: id }); // Finally, delete the restaurant itself await em.nativeDelete(Restaurant, { id }); }); } findAllBackgrounds() { return this.backgroundRepository.findAll(); } async updateBackground(id: string, dto: UpdateRestaurantBgDto): Promise { const restaurant = await this.restRepository.findOne({ id }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } this.restRepository.assign(restaurant, dto); await this.em.persistAndFlush(restaurant); await this.invalidateRestaurantCache(restaurant.slug); return restaurant; } }