343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
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<Restaurant> {
|
|
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<PaginatedResult<Restaurant>> {
|
|
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<Restaurant> {
|
|
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<Restaurant> {
|
|
const restaurant = await this.restRepository.findOne({ id });
|
|
|
|
if (!restaurant) {
|
|
throw new NotFoundException(RestMessage.NOT_FOUND);
|
|
}
|
|
|
|
return restaurant;
|
|
}
|
|
|
|
async findOneBySubscriptionId(subscriptionId: string): Promise<Restaurant> {
|
|
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<ActiveRestaurantListItemDto[]> {
|
|
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<Restaurant> {
|
|
|
|
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<void> {
|
|
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<void> {
|
|
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<Restaurant> {
|
|
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<void> {
|
|
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<Restaurant> {
|
|
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;
|
|
}
|
|
}
|