diff --git a/src/modules/dmenu/DTO/direct-login.dto.ts b/src/modules/dmenu/DTO/direct-login.dto.ts deleted file mode 100644 index f583d1f..0000000 --- a/src/modules/dmenu/DTO/direct-login.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; - -export class DirectLoginDto { - @ApiProperty({ example: '09123456789', description: 'شماره تلفن کاربر' }) - @IsString() - phone!: string; - - @ApiProperty({ example: 'zhivan', description: 'شناسه (slug) رستوران' }) - @IsString() - slug!: string; -} diff --git a/src/modules/dmenu/dmenu.controller.ts b/src/modules/dmenu/dmenu.controller.ts index 1c091b0..b2241f2 100644 --- a/src/modules/dmenu/dmenu.controller.ts +++ b/src/modules/dmenu/dmenu.controller.ts @@ -12,7 +12,6 @@ import { FindRestaurantsDto } from "./DTO/find-restaurants.dto"; import { SetupRestaurantDto } from "./DTO/create-restaurant.dto"; import { UpdateRestaurantDto } from "./DTO/update-restaurant.dto"; import { CreateMyRestaurantAdminDto } from "./DTO/create-restaurant-admin.dto"; -import { DirectLoginDto } from "./DTO/direct-login.dto"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { PermissionEnum } from "../users/enums/permission.enum"; @@ -127,6 +126,13 @@ export class DmenuController { return this.restaurantService.findAll(queryDto); } + @PermissionsDec(PermissionEnum.DMENU) + @Get('admin/dmenu/restaurants/:id') + @ApiOperation({ summary: "Get restaurant by ID" }) + @ApiParam({ name: "id", required: true, type: String }) + getRestaurantById(@Param("id") id: string) { + return this.restaurantService.getRestaurantById(id); + } @ApiBearerAuth() @Get('admin/dmenu/restaurants/sms-count') @@ -196,9 +202,10 @@ export class DmenuController { return this.restaurantService.getRestaurantSubscription(subscriptionId); } - @Post('dmenu/auth/direct-login') - @ApiOperation({ summary: "Direct login with phone number and restaurant slug" }) - directLogin(@Body() dto: DirectLoginDto) { - return this.restaurantService.directLogin(dto); + @Post('dmenu/auth/direct-login/:userSubscriptionId') + @ApiOperation({ summary: "Direct login with user subscription ID" }) + @ApiParam({ name: "userSubscriptionId", required: true, type: String }) + directLogin(@Param("userSubscriptionId") userSubscriptionId: string, @UserDec("id") userId: string) { + return this.restaurantService.directLogin(userSubscriptionId, userId); } } diff --git a/src/modules/dmenu/dmenu.module.ts b/src/modules/dmenu/dmenu.module.ts index 88a46a5..b5439fc 100644 --- a/src/modules/dmenu/dmenu.module.ts +++ b/src/modules/dmenu/dmenu.module.ts @@ -7,9 +7,15 @@ import { IconsService } from "./providers/icons.service"; import { RestaurantService } from "./providers/restaurant.service"; import { dmenuConfig } from "../../configs/icons.config"; import { SubscriptionsModule } from "../subscriptions/subscriptions.module"; +import { UsersModule } from "../users/users.module"; +import { DanakServicesModule } from "../danak-services/danak-services.module"; @Module({ - imports: [forwardRef(() => SubscriptionsModule)], + imports: [ + forwardRef(() => SubscriptionsModule), + forwardRef(() => UsersModule), + forwardRef(() => DanakServicesModule), + ], controllers: [DmenuController], providers: [ IconsService, diff --git a/src/modules/dmenu/providers/restaurant.service.ts b/src/modules/dmenu/providers/restaurant.service.ts index 6ca5543..ef04155 100644 --- a/src/modules/dmenu/providers/restaurant.service.ts +++ b/src/modules/dmenu/providers/restaurant.service.ts @@ -3,6 +3,8 @@ import { BadRequestException, HttpException, Injectable, Logger } from "@nestjs/ import { Inject } from "@nestjs/common"; import { AxiosError } from "axios"; import { catchError, firstValueFrom, throwError } from "rxjs"; +import { DataSource } from "typeorm"; +import slugify from "slugify"; import { IDmenuConfig } from "../../../configs/icons.config"; import { DMENU_CONFIG } from "../constants"; @@ -13,7 +15,12 @@ import { CreateMyRestaurantAdminDto } from "../DTO/create-restaurant-admin.dto"; import { PlanEnum, ISetupRestaurant, IExternalApiResponse } from "../interface/plan.interface"; import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service"; import { SubscriptionMessage } from "../../../common/enums/message.enum"; -import { DirectLoginDto } from "../DTO/direct-login.dto"; +import { UsersService } from "../../users/providers/users.service"; +import { DanakServicesService } from "../../danak-services/providers/danak-services.service"; +import { User } from "../../users/entities/user.entity"; +import { Role } from "../../users/entities/role.entity"; +import { RoleEnum } from "../../users/enums/role.enum"; +import { SubscribeServiceDto } from "../../subscriptions/DTO/subscribe-service.dto"; @Injectable() export class RestaurantService { @@ -23,7 +30,10 @@ export class RestaurantService { @Inject(DMENU_CONFIG) private readonly config: IDmenuConfig, private readonly httpService: HttpService, private readonly subscriptionService: SubscriptionsService, - ) {} + private readonly usersService: UsersService, + private readonly danakServicesService: DanakServicesService, + private readonly dataSource: DataSource, + ) { } private getHeaders(): Record { const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64"); @@ -86,6 +96,35 @@ export class RestaurantService { } } + async getRestaurantById(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService + .get(`${this.config.baseUrl}/super-admin/restaurants/${id}`, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to get restaurant by ID: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + if (error instanceof AxiosError && error.response) { + this.logger.error(`External API error response:`, error.response.data); + const errorResponse = { + ...error.response.data, + message: error.response.data.error?.message || error.message, + }; + throw new HttpException(errorResponse, error.response.status); + } + this.logger.error(`Error getting restaurant by ID: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + async createRestaurant(dto: SetupRestaurantDto, userId: string) { const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(dto.userSubscriptionId, userId); if (!userSubscription) { @@ -308,8 +347,116 @@ export class RestaurantService { } } + private async updateRestaurantSubscription(restaurantId: string, subscriptionId: string, subscriptionExpiryDate: Date) { + try { + const { data } = await firstValueFrom( + this.httpService + .patch(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/subscription`, { + subscriptionId, + subscriptionExpiryDate, + }, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to update restaurant subscription: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + if (error instanceof AxiosError && error.response) { + this.logger.error(`External API error response:`, error.response.data); + const errorResponse = { + ...error.response.data, + message: error.response.data.error?.message || error.message, + }; + throw new HttpException(errorResponse, error.response.status); + } + this.logger.error(`Error updating restaurant subscription: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + private async findOrCreateUserByPhone(phone: string, dto: CreateMyRestaurantAdminDto) { + let user = await this.usersService.findOneWithPhone(phone); + if (!user) { + // Create a basic user for restaurant admin assignment + const queryRunner = this.dataSource.createQueryRunner(); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const role = await queryRunner.manager.findOneBy(Role, { name: RoleEnum.USER }); + if (!role) throw new BadRequestException("User role not found"); + + const userName = slugify(`u-${phone}`, { lower: true, trim: true }); + const defaultFirstName = "کاربر"; + const defaultLastName = userName; + + user = queryRunner.manager.create(User, { + phone, + userName, + password: "temp_password", // This should be changed later + firstName: dto.firstName || defaultFirstName, + lastName: dto.lastName || defaultLastName, + roles: [role], + }); + + await queryRunner.manager.save(user); + + // Note: Skipping user settings and wallet creation for now as they might be optional for this use case + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + return user; + } + async assignRestaurantAdmin(restaurantId: string, dto: CreateMyRestaurantAdminDto) { - try { + try { + // 1. Create user with phone if not exists + const user = await this.findOrCreateUserByPhone(dto.phone, dto); + + // 2. Get restaurant by id + const restaurant = await this.getRestaurantById(restaurantId); + + // 3. If restaurant doesn't have subscription id, create user subscription with free plan of dmenu service + let subscriptionId = restaurant.subscriptionId; + let subscriptionExpiryDate = restaurant.subscriptionExpiryDate; + + if (!subscriptionId) { + const dmenuService = await this.danakServicesService.getServiceByName("dmenu"); + const freePlan = await this.subscriptionService['subscriptionsPlanRepository'].findOne({ + where: { service: { id: dmenuService.id }, isFree: true }, + }); + + if (!freePlan) { + throw new BadRequestException("Free plan not found for dmenu service"); + } + + const subscriptionDto: SubscribeServiceDto = { + planId: freePlan.id, + businessName: restaurant.name || `Restaurant ${restaurantId}`, + businessPhone: dto.phone, + description: restaurant.description || `Restaurant admin for ${restaurant.name || restaurantId}`, + slug: slugify(restaurant.name || `restaurant-${restaurantId}`, { lower: true, strict: true }), + }; + + const subscriptionResult = await this.subscriptionService.subscribeToPlan(dmenuService.id, subscriptionDto, user.id); + subscriptionId = subscriptionResult.userSubscription.id; + subscriptionExpiryDate = subscriptionResult.userSubscription.endDate; + } + + // 4. Update restaurant with subscription id and expiry date + await this.updateRestaurantSubscription(restaurantId, subscriptionId, subscriptionExpiryDate); + + // 5. Assign restaurant admin const { data } = await firstValueFrom( this.httpService .post(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/admins`, dto, { @@ -322,7 +469,13 @@ export class RestaurantService { }), ), ); - return data; + + return { + ...data, + user, + subscriptionId, + subscriptionExpiryDate, + }; } catch (error: unknown) { if (error instanceof AxiosError && error.response) { this.logger.error(`External API error response:`, error.response.data); @@ -369,13 +522,33 @@ export class RestaurantService { } } - async directLogin(dto: DirectLoginDto) { + async directLogin(userSubscriptionId: string, userId: string) { try { + // 1. Get userSubscription by id + const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(userSubscriptionId, userId); + + // 2. Get phone from userSubscription + const phone = userSubscription.businessPhone; + if (!phone) { + throw new BadRequestException("Phone number not found in user subscription"); + } + + // 3. Get restaurant by subscription id + const restaurant = await this.getRestaurantSubscription(userSubscriptionId); + console.log('restaurant', restaurant); + + // 4. Get slug from restaurant + const slug = restaurant.data.slug; + if (!slug) { + throw new BadRequestException("Restaurant slug not found"); + } + + // 5. Login with phone and slug const { data } = await firstValueFrom( this.httpService .post(`${this.config.baseUrl}/super-admin/auth/direct-login`, { - phone: dto.phone, - slug: dto.slug, + phone, + slug, }, { headers: this.getHeaders(), })