diff --git a/src/app.module.ts b/src/app.module.ts index 5c81400..a2314bc 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -42,6 +42,7 @@ import { TicketsModule } from "./modules/tickets/tickets.module"; import { UploaderModule } from "./modules/uploader/uploader.module"; import { UsersModule } from "./modules/users/users.module"; import { WalletsModule } from "./modules/wallets/wallets.module"; +import { DkalaModule } from "./modules/dkala/dmenu.module"; @Module({ imports: [ // TelegrafModule.forRootAsync(telegrafConfig()), @@ -87,6 +88,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; SupportPlansModule, AccessLogsModule, DmenuModule, + DkalaModule ], controllers: [], }) diff --git a/src/configs/dkala.config.ts b/src/configs/dkala.config.ts new file mode 100644 index 0000000..15a5340 --- /dev/null +++ b/src/configs/dkala.config.ts @@ -0,0 +1,20 @@ +import { ConfigService } from "@nestjs/config"; + +export function dkalaConfig() { + return { + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + return { + baseUrl: configService.get("DKALA_BACKEND_URL")??'https://dkala-api.danakcorp.com', + username: configService.get("DKALA_USERNAME")??'danak@dsc.com', + password: configService.get("DKALA_PASSWORD")??'DsCdAnAk?@ABC', + }; + }, + }; +} + +export interface IDkalaConfig { + baseUrl: string; + username: string; + password: string; +} diff --git a/src/modules/dkala/DTO/buy-premium-plan.dto.ts b/src/modules/dkala/DTO/buy-premium-plan.dto.ts new file mode 100644 index 0000000..f64a3e7 --- /dev/null +++ b/src/modules/dkala/DTO/buy-premium-plan.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsUUID } from "class-validator"; + +import { SubscriptionMessage } from "../../../common/enums/message.enum"; + +export class BuyPremiumPlanDto { + @IsNotEmpty({ message: SubscriptionMessage.PLAN_ID_REQUIRED }) + @IsUUID(4, { message: SubscriptionMessage.PLAN_ID_SHOULD_BE_UUID }) + @ApiProperty({ description: "The restaurant subscription ID to upgrade", example: "f7b3b3b3-7b3b-4b3b-8b3b-3b3b3b3b3b3b" }) + userSubscriptionId: string; +} diff --git a/src/modules/dkala/DTO/create-group.dto.ts b/src/modules/dkala/DTO/create-group.dto.ts new file mode 100644 index 0000000..ba8b912 --- /dev/null +++ b/src/modules/dkala/DTO/create-group.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class CreateGroupDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ description: "Group name", example: "Navigation Icons" }) + name: string; +} diff --git a/src/modules/dkala/DTO/create-icon.dto.ts b/src/modules/dkala/DTO/create-icon.dto.ts new file mode 100644 index 0000000..b5b1854 --- /dev/null +++ b/src/modules/dkala/DTO/create-icon.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator"; + +export class CreateIconDto { + @IsNotEmpty() + @IsUrl({ protocols: ["http", "https"], require_protocol: true }) + @ApiProperty({ description: "Icon URL", example: "https://example.com/icons/home.svg" }) + url: string; + + @IsOptional() + @IsString() + @ApiProperty({ description: "Icon group ID", example: "group-123", required: false }) + groupId?: string; +} diff --git a/src/modules/dkala/DTO/create-shop-admin.dto.ts b/src/modules/dkala/DTO/create-shop-admin.dto.ts new file mode 100644 index 0000000..f5b4b67 --- /dev/null +++ b/src/modules/dkala/DTO/create-shop-admin.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateMyShopAdminDto { + @ApiProperty({ description: 'Mobile phone number' }) + @IsNotEmpty() + @IsString() + phone!: string; + + @ApiProperty({ description: 'First name', required: false }) + @IsOptional() + @IsString() + firstName?: string; + + @ApiProperty({ description: 'Last name', required: false }) + @IsOptional() + @IsString() + lastName?: string; + + @ApiProperty({ description: 'Role id for the admin' }) + @IsNotEmpty() + @IsString() + roleId!: string; +} diff --git a/src/modules/dkala/DTO/create-shop.dto.ts b/src/modules/dkala/DTO/create-shop.dto.ts new file mode 100644 index 0000000..f46390b --- /dev/null +++ b/src/modules/dkala/DTO/create-shop.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class SetupShopDto { + + @ApiProperty({ example: 'zhivan', description: 'شناسه (slug) رستوران، لاتین و بدون فاصله' }) + @IsString() + slug!: string; + + + @ApiProperty({ example: 'sub_1234567890', description: 'شناسه اشتراک' }) + @IsString() + userSubscriptionId!: string; + +} diff --git a/src/modules/dkala/DTO/find-shops.dto.ts b/src/modules/dkala/DTO/find-shops.dto.ts new file mode 100644 index 0000000..d2f9295 --- /dev/null +++ b/src/modules/dkala/DTO/find-shops.dto.ts @@ -0,0 +1,64 @@ +import { + IsOptional, + IsString, + IsNumber, + Min, + IsIn, + IsBoolean, +} from 'class-validator'; +import { Type, Transform } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +const sortOrderOptions = ['asc', 'desc'] as const; +type SortOrder = (typeof sortOrderOptions)[number]; + +export class FindShopsDto { + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + page: number = 1; + + @ApiPropertyOptional({ default: 10, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + limit: number = 10; + + @ApiPropertyOptional({ + description: 'Search by name, slug, domain, or address', + }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ + description: 'Filter by active status', + type: Boolean, + }) + @IsOptional() + @Transform(({ value }) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; + }) + @IsBoolean() + isActive?: boolean; + + + @ApiPropertyOptional({ default: 'createdAt' }) + @IsOptional() + @IsString() + orderBy: string = 'createdAt'; + + @ApiPropertyOptional({ + enum: sortOrderOptions, + default: 'desc', + }) + @IsOptional() + @IsIn(sortOrderOptions) + order: SortOrder = 'desc'; +} + diff --git a/src/modules/dkala/DTO/update-group.dto.ts b/src/modules/dkala/DTO/update-group.dto.ts new file mode 100644 index 0000000..1c3fdd6 --- /dev/null +++ b/src/modules/dkala/DTO/update-group.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateGroupDto } from "./create-group.dto"; + +export class UpdateGroupDto extends PartialType(CreateGroupDto) {} + diff --git a/src/modules/dkala/DTO/update-icon.dto.ts b/src/modules/dkala/DTO/update-icon.dto.ts new file mode 100644 index 0000000..940a9eb --- /dev/null +++ b/src/modules/dkala/DTO/update-icon.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateIconDto } from "./create-icon.dto"; + +export class UpdateIconDto extends PartialType(CreateIconDto) {} + diff --git a/src/modules/dkala/DTO/update-shop.dto.ts b/src/modules/dkala/DTO/update-shop.dto.ts new file mode 100644 index 0000000..ddc033f --- /dev/null +++ b/src/modules/dkala/DTO/update-shop.dto.ts @@ -0,0 +1,48 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsNumber, IsDate, IsBoolean } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class UpdateShopDto { + @ApiPropertyOptional({ example: 'کافه ژیوان', description: 'نام رستوران' }) + @IsOptional() + @IsString() + name?: string; + + @ApiPropertyOptional({ example: 'zhivan', description: 'شناسه (slug) رستوران، لاتین و بدون فاصله' }) + @IsOptional() + @IsString() + slug?: string; + + @ApiPropertyOptional({ example: 2020, description: 'سال تأسیس' }) + @IsOptional() + @IsNumber() + establishedYear?: number; + + @ApiPropertyOptional({ example: '09123456789', description: 'شماره تلفن' }) + @IsOptional() + @IsString() + phone?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + isActive?: boolean; + + + @ApiPropertyOptional({ example: 'sub_1234567890', description: 'شناسه اشتراک' }) + @IsOptional() + @IsString() + subscriptionId?: string; + + @ApiPropertyOptional({ example: '2025-12-26', description: 'تاریخ انقضای اشتراک' }) + @IsOptional() + @Type(() => Date) + @IsDate() + subscriptionEndDate?: Date; + + @ApiPropertyOptional({ example: '2025-12-26', description: 'تاریخ شروع اشتراک' }) + @IsOptional() + @Type(() => Date) + @IsDate() + subscriptionStartDate?: Date; +} diff --git a/src/modules/dkala/constants/index.ts b/src/modules/dkala/constants/index.ts new file mode 100644 index 0000000..3d6d810 --- /dev/null +++ b/src/modules/dkala/constants/index.ts @@ -0,0 +1 @@ +export const DKALA_CONFIG = "DKALA_CONFIG"; diff --git a/src/modules/dkala/dmenu.controller.ts b/src/modules/dkala/dmenu.controller.ts new file mode 100644 index 0000000..6a4d5dd --- /dev/null +++ b/src/modules/dkala/dmenu.controller.ts @@ -0,0 +1,219 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery, ApiTags } from "@nestjs/swagger"; + +import { DContactService } from "./providers/contact.service"; +import { DkalaIconsService } from "./providers/icons.service"; +import { ShopService } from "./providers/shop.service"; + import { CreateIconDto } from "./DTO/create-icon.dto"; +import { UpdateIconDto } from "./DTO/update-icon.dto"; +import { CreateGroupDto } from "./DTO/create-group.dto"; +import { UpdateGroupDto } from "./DTO/update-group.dto"; +import { FindShopsDto } from "./DTO/find-shops.dto"; +import { SetupShopDto } from "./DTO/create-shop.dto"; +import { UpdateShopDto } from "./DTO/update-shop.dto"; +import { CreateMyShopAdminDto } from "./DTO/create-shop-admin.dto"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { PermissionsDec } from "../../common/decorators/permission.decorator"; +import { PermissionEnum } from "../users/enums/permission.enum"; +import { UserDec } from "../../common/decorators/user.decorator"; + +@ApiTags("d-menu") +@Controller() +@AuthGuards() + +@ApiBearerAuth() +export class DkalaController { + constructor( + private readonly iconsService: DkalaIconsService, + private readonly contactService: DContactService, + private readonly shopService: ShopService, + ) { } + + // Icon endpoints + @PermissionsDec(PermissionEnum.DKALA) + @Post('admin/dkala/icons') + @ApiOperation({ summary: "Create icon" }) + createIcon(@Body() dto: CreateIconDto) { + return this.iconsService.createIcon(dto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get('admin/dkala/icons') + @ApiOperation({ summary: "Get all icons" }) + findAllIcons() { + return this.iconsService.findAllIcons(); + } + // Group endpoints: place BEFORE `icons/:id` routes to avoid route conflicts + @PermissionsDec(PermissionEnum.DKALA) + @Post("admin/dkala/icons/groups") + @ApiOperation({ summary: "Create icon group" }) + createGroup(@Body() dto: CreateGroupDto) { + return this.iconsService.createGroup(dto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get("admin/dkala/icons/groups") + @ApiOperation({ summary: "Get all icon groups" }) + findAllGroups() { + return this.iconsService.findAllGroups(); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get("admin/dkala/icons/groups/:id") + @ApiOperation({ summary: "Get icon group by id" }) + @ApiParam({ name: "id", required: true, type: String }) + findOneGroup(@Param("id") id: string) { + return this.iconsService.findOneGroup(id); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Patch("admin/dkala/icons/groups/:id") + @ApiOperation({ summary: "Update icon group" }) + @ApiParam({ name: "id" }) + updateGroup(@Param("id") id: string, @Body() dto: UpdateGroupDto) { + return this.iconsService.updateGroup(id, dto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Delete("admin/dkala/icons/groups/:id") + @ApiOperation({ summary: "Delete icon group" }) + @ApiParam({ name: "id" }) + removeGroup(@Param("id") id: string) { + return this.iconsService.removeGroup(id); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get("admin/dkala/icons/:id") + @ApiOperation({ summary: "Get icon by id" }) + @ApiParam({ name: "id", required: true, type: String }) + findOneIcon(@Param("id") id: string) { + return this.iconsService.findOneIcon(id); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Patch("admin/dkala/icons/:id") + @ApiOperation({ summary: "Update icon" }) + @ApiParam({ name: "id" }) + updateIcon(@Param("id") id: string, @Body() dto: UpdateIconDto) { + return this.iconsService.updateIcon(id, dto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Delete("admin/dkala/icons/:id") + @ApiOperation({ summary: "Delete icon" }) + @ApiParam({ name: "id" }) + removeIcon(@Param("id") id: string) { + return this.iconsService.removeIcon(id); + } + // contacts + @PermissionsDec(PermissionEnum.DKALA) + @Get('admin/dkala/contacts') + findAll() { + return this.contactService.findAll(); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get("admin/dkala/contacts/:id") + findOne(@Param("id") id: string) { + return this.contactService.findOne(+id); + } + + // restaurant + @PermissionsDec(PermissionEnum.DKALA) + @Get('admin/dkala/shops') + @ApiOperation({ summary: "Get all restaurants" }) + findAllRestaurant(@Query() queryDto: FindShopsDto) { + return this.shopService.findAll(queryDto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get('admin/dkala/shops/:id') + @ApiOperation({ summary: "Get restaurant by ID" }) + @ApiParam({ name: "id", required: true, type: String }) + getRestaurantById(@Param("id") id: string) { + return this.shopService.getShopById(id); + } + + @ApiBearerAuth() + @Get('admin/dkala/shops/sms-count') + @ApiOperation({ summary: 'Get SMS count for each restaurant with pagination' }) + @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)', minimum: 1 }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 10)', minimum: 1 }) + async getSmsCount( + @Query('page') page?: number, + @Query('limit') limit?: number, + ) { + const parsedPage = page ? parseInt(page.toString(), 10) : 1; + const parsedLimit = limit ? parseInt(limit.toString(), 10) : 10; + return await this.shopService.getSmsCountByRestaurant(parsedPage, parsedLimit); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Patch('admin/dkala/shops/:id') + @ApiOperation({ summary: "Update restaurant" }) + @ApiParam({ name: "id", required: true, type: String }) + updateRestaurant(@Param("id") id: string, @Body() dto: UpdateShopDto) { + return this.shopService.updateRestaurant(id, dto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Delete('admin/dkala/shops/:id/hard-delete') + @ApiOperation({ summary: "Hard delete restaurant" }) + @ApiParam({ name: "id", required: true, type: String }) + hardDeleteRestaurant(@Param("id") id: string) { + return this.shopService.hardDeleteShop(id); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get('admin/dkala/system-roles') + @ApiOperation({ summary: "Get system roles" }) + getSystemRoles() { + return this.shopService.getSystemRoles(); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Get('admin/dkala/shops/:restaurantId/admins') + @ApiOperation({ summary: "Get restaurant admins" }) + @ApiParam({ name: "restaurantId", required: true, type: String }) + getRestaurantAdmins(@Param("restaurantId") restaurantId: string) { + return this.shopService.getRestaurantAdmins(restaurantId); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Post('admin/dkala/shops/:restaurantId/admins') + @ApiOperation({ summary: "Assign admin to restaurant" }) + @ApiParam({ name: "restaurantId", required: true, type: String }) + assignRestaurantAdmin(@Param("restaurantId") restaurantId: string, @Body() dto: CreateMyShopAdminDto) { + return this.shopService.assignRestaurantAdmin(restaurantId, dto); + } + + @PermissionsDec(PermissionEnum.DKALA) + @Delete('admin/dkala/shops/:restaurantId/admins/:adminId') + @ApiOperation({ summary: "Revoke admin from restaurant" }) + @ApiParam({ name: "restaurantId", required: true, type: String }) + @ApiParam({ name: "adminId", required: true, type: String }) + revokeRestaurantAdmin(@Param("restaurantId") restaurantId: string, @Param("adminId") adminId: string) { + return this.shopService.revokeRestaurantAdmin(restaurantId, adminId); + } + + //***User Endpoints*** + @Post('dkala/shops') + @ApiOperation({ summary: "Create restaurant" }) + createRestaurant(@Body() dto: SetupShopDto, @UserDec("id") userId: string) { + return this.shopService.createRestaurant(dto, userId); + } + + @Get('dkala/shops/subscription/:subscriptionId') + @ApiOperation({ summary: "Get restaurant subscription by subscription ID" }) + @ApiParam({ name: "subscriptionId", required: true, type: String }) + getRestaurantSubscription(@Param("subscriptionId") subscriptionId: string) { + return this.shopService.getRestaurantSubscription(subscriptionId); + } + + @Post('dkala/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.shopService.directLogin(userSubscriptionId, userId); + } +} diff --git a/src/modules/dkala/dmenu.module.ts b/src/modules/dkala/dmenu.module.ts new file mode 100644 index 0000000..ae74bf1 --- /dev/null +++ b/src/modules/dkala/dmenu.module.ts @@ -0,0 +1,31 @@ +import { forwardRef, Module } from "@nestjs/common"; + +import { DKALA_CONFIG } from "./constants"; +import { DkalaController } from "./dmenu.controller"; +import { DContactService } from "./providers/contact.service"; +import { DkalaIconsService } from "./providers/icons.service"; +import { ShopService } from "./providers/shop.service"; +import { SubscriptionsModule } from "../subscriptions/subscriptions.module"; +import { UsersModule } from "../users/users.module"; +import { dkalaConfig } from "../../configs/dkala.config"; + + +@Module({ + imports: [ + forwardRef(() => SubscriptionsModule), + UsersModule + ], + controllers: [DkalaController], + providers: [ + DkalaIconsService, + DContactService, + ShopService, + { + provide: DKALA_CONFIG, + useFactory: dkalaConfig().useFactory, + inject: dkalaConfig().inject, + }, + ], + exports: [ShopService], +}) +export class DkalaModule {} diff --git a/src/modules/dkala/interface/plan.interface.ts b/src/modules/dkala/interface/plan.interface.ts new file mode 100644 index 0000000..ea1a3f2 --- /dev/null +++ b/src/modules/dkala/interface/plan.interface.ts @@ -0,0 +1,23 @@ + +export interface ISetupShop { + name: string; + slug: string; + establishedYear: number; + phone: string; + subscriptionId: string; + subscriptionEndDate: string | Date; + subscriptionStartDate: string | Date; + isActive: boolean +} + +export interface IExternalApiPaginationMeta { + total: number; + page: number; + limit: number; + totalPages: number; +} + +export interface IExternalApiResponse { + data: T; + meta: IExternalApiPaginationMeta; +} \ No newline at end of file diff --git a/src/modules/dkala/providers/contact.service.ts b/src/modules/dkala/providers/contact.service.ts new file mode 100644 index 0000000..1b5711d --- /dev/null +++ b/src/modules/dkala/providers/contact.service.ts @@ -0,0 +1,60 @@ +import { HttpService } from "@nestjs/axios"; +import { Injectable, Logger } from "@nestjs/common"; +import { Inject } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { catchError, firstValueFrom, throwError } from "rxjs"; + +import { DKALA_CONFIG } from "../constants"; +import { IDkalaConfig } from "../../../configs/dkala.config"; + +@Injectable() +export class DContactService { + private readonly logger = new Logger(DContactService.name); + + constructor( + @Inject(DKALA_CONFIG) private readonly config: IDkalaConfig, + private readonly httpService: HttpService, + ) {} + + private getHeaders(): Record { + const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64"); + return { + "Content-Type": "application/json", + Authorization: `Basic ${credentials}`, + }; + } + + async findAll() { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/super-admin/contact`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch contacts: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching contacts: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findOne(id: number) { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/super-admin/contact/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch contact: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching contact: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } +} diff --git a/src/modules/dkala/providers/icons.service.ts b/src/modules/dkala/providers/icons.service.ts new file mode 100644 index 0000000..c781c3f --- /dev/null +++ b/src/modules/dkala/providers/icons.service.ts @@ -0,0 +1,201 @@ +import { HttpService } from "@nestjs/axios"; +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { catchError, firstValueFrom, throwError } from "rxjs"; + +import { IDmenuConfig } from "../../../configs/icons.config"; +import { DKALA_CONFIG } from "../constants"; +import { CreateGroupDto } from "../DTO/create-group.dto"; +import { CreateIconDto } from "../DTO/create-icon.dto"; +import { UpdateGroupDto } from "../DTO/update-group.dto"; +import { UpdateIconDto } from "../DTO/update-icon.dto"; + +@Injectable() +export class DkalaIconsService { + private readonly logger = new Logger(DkalaIconsService.name); + + constructor( + @Inject(DKALA_CONFIG) private readonly config: IDmenuConfig, + private readonly httpService: HttpService, + ) {} + + private getHeaders(): Record { + const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64"); + return { + "Content-Type": "application/json", + Authorization: `Basic ${credentials}`, + }; + } + + // Icon methods + async createIcon(dto: CreateIconDto) { + try { + const { data } = await firstValueFrom( + this.httpService.post(`${this.config.baseUrl}/super-admin/icons`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to create icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error creating icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findAllIcons() { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/super-admin/icons`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch icons: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching icons: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findOneIcon(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/super-admin/icons/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async updateIcon(id: string, dto: UpdateIconDto) { + try { + const { data } = await firstValueFrom( + this.httpService.patch(`${this.config.baseUrl}/super-admin/icons/${id}`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to update icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error updating icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async removeIcon(id: string) { + try { + const response = await firstValueFrom( + this.httpService.delete(`${this.config.baseUrl}/super-admin/icons/${id}`, { headers: this.getHeaders(), data: {} }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to delete icon: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return response.data ?? { message: "Icon deleted successfully" }; + } catch (error: unknown) { + this.logger.error(`Error deleting icon: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + // Group methods + async createGroup(dto: CreateGroupDto) { + try { + const { data } = await firstValueFrom( + this.httpService.post(`${this.config.baseUrl}/super-admin/icons/groups`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to create group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error creating group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findAllGroups() { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/super-admin/icons/groups`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch groups: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching groups: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async findOneGroup(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService.get(`${this.config.baseUrl}/super-admin/icons/groups/${id}`, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error fetching group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async updateGroup(id: string, dto: UpdateGroupDto) { + try { + const { data } = await firstValueFrom( + this.httpService.patch(`${this.config.baseUrl}/super-admin/icons/groups/${id}`, dto, { headers: this.getHeaders() }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to update group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return data; + } catch (error: unknown) { + this.logger.error(`Error updating group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async removeGroup(id: string) { + try { + const response = await firstValueFrom( + this.httpService.delete(`${this.config.baseUrl}/super-admin/icons/groups/${id}`, { headers: this.getHeaders(), data: {} }).pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to delete group: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return response.data ?? { message: "Group deleted successfully" }; + } catch (error: unknown) { + this.logger.error(`Error deleting group: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } +} \ No newline at end of file diff --git a/src/modules/dkala/providers/shop.service.ts b/src/modules/dkala/providers/shop.service.ts new file mode 100644 index 0000000..f7942e1 --- /dev/null +++ b/src/modules/dkala/providers/shop.service.ts @@ -0,0 +1,494 @@ +import { HttpService } from "@nestjs/axios"; +import { BadRequestException, HttpException, Injectable, Logger } from "@nestjs/common"; +import { Inject } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { catchError, firstValueFrom, throwError } from "rxjs"; + +import { DKALA_CONFIG } from "../constants"; +import { FindShopsDto } from "../DTO/find-shops.dto"; +import { SetupShopDto } from "../DTO/create-shop.dto"; +import { UpdateShopDto } from "../DTO/update-shop.dto"; +import { CreateMyShopAdminDto } from "../DTO/create-shop-admin.dto"; +import { ISetupShop, IExternalApiResponse } from "../interface/plan.interface"; +import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service"; +import { UsersService } from "../../users/providers/users.service"; +import { SubscriptionMessage } from "../../../common/enums/message.enum"; +import { IDkalaConfig } from "../../../configs/dkala.config"; + + +@Injectable() +export class ShopService { + private readonly logger = new Logger(ShopService.name); + + constructor( + @Inject(DKALA_CONFIG) private readonly config: IDkalaConfig, + private readonly httpService: HttpService, + private readonly subscriptionService: SubscriptionsService, + private readonly usersService: UsersService, + ) { } + + private getHeaders(): Record { + const credentials = Buffer.from(`${this.config.username}:${this.config.password}`).toString("base64"); + return { + "Content-Type": "application/json", + Authorization: `Basic ${credentials}`, + }; + } + + async findAll(queryDto: FindShopsDto) { + try { + const params: Record = { + page: queryDto.page || 1, + limit: queryDto.limit || 10, + orderBy: queryDto.orderBy || "createdAt", + order: queryDto.order || "desc", + }; + + if (queryDto.search) { + params.search = queryDto.search; + } + + if (queryDto.isActive !== undefined) { + params.isActive = queryDto.isActive; + } + + + const { data } = await firstValueFrom( + this.httpService + .get>(`${this.config.baseUrl}/super-admin/restaurants`, { + params, + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to fetch restaurants: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + return { + restaurants: data.data, + count: data.meta.total, + paginate: true, + }; + } 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 fetching restaurants: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async getShopById(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: SetupShopDto, userId: string) { + const { userSubscription } = await this.subscriptionService.getUserSubscriptionById(dto.userSubscriptionId, userId); + if (!userSubscription) { + throw new BadRequestException(SubscriptionMessage.USER_SUBS_NOT_FOUND); + } + + const user = await this.usersService.findOneById(userId) + + if (!user) { + throw new BadRequestException("User Not found"); + } + + const setupRestaurantDto: ISetupShop = { + name: userSubscription.businessName, + slug: dto.slug, + establishedYear: 1400, + phone: user.user.phone, + subscriptionId: dto.userSubscriptionId, + subscriptionEndDate: userSubscription.endDate.toISOString(), + subscriptionStartDate: userSubscription.startDate.toISOString(), + isActive: true + }; + + try { + const { data } = await firstValueFrom( + this.httpService + .post(`${this.config.baseUrl}/super-admin/restaurants`, setupRestaurantDto, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to create restaurant: ${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 creating restaurant: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async updateRestaurant(id: string, dto: UpdateShopDto) { + try { + const { data } = await firstValueFrom( + this.httpService + .patch(`${this.config.baseUrl}/super-admin/restaurants/${id}`, dto, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to update restaurant: ${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: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async getRestaurantSubscription(subscriptionId: string) { + try { + console.log('subscriptionId', subscriptionId) + const { data } = await firstValueFrom( + this.httpService + .get(`${this.config.baseUrl}/super-admin/restaurants/subscription/${subscriptionId}`, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to get restaurant subscription: ${err.message}`, err.stack); + return throwError(() => err); + }), + ), + ); + console.log('data', data) + 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 subscription: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async getSmsCountByRestaurant(page: number, limit: number) { + try { + const { data } = await firstValueFrom( + this.httpService + .get(this.config.baseUrl + "/super-admin/notifications/sms-count", { + params: { page, limit }, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error("error in getting SMS count by restaurant", err); + 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 in getSmsCountByRestaurant", error); + throw error; + } + } + + async getSystemRoles() { + try { + const { data } = await firstValueFrom( + this.httpService + .get(`${this.config.baseUrl}/super-admin/system-roles`, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to get system roles: ${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 system roles: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async getRestaurantAdmins(restaurantId: string) { + try { + const { data } = await firstValueFrom( + this.httpService + .get(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/admins`, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to get restaurant admins: ${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 admins: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async revokeRestaurantAdmin(restaurantId: string, adminId: string) { + try { + const { data } = await firstValueFrom( + this.httpService + .delete(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/admins/${adminId}`, { + headers: this.getHeaders(), + data: {}, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to revoke restaurant admin: ${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 revoking restaurant admin: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async assignRestaurantAdmin(restaurantId: string, dto: CreateMyShopAdminDto) { + try { + + const { data } = await firstValueFrom( + this.httpService + .post(`${this.config.baseUrl}/super-admin/restaurants/${restaurantId}/admins`, dto, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to assign restaurant admin: ${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 assigning restaurant admin: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async upgradeSubscription(subscriptionId: string, newPlan: 'base' | 'premium', subscriptionEndDate: Date) { + try { + const { data } = await firstValueFrom( + this.httpService + .patch(`${this.config.baseUrl}/super-admin/restaurants/subscription/${subscriptionId}/upgrade`, { + newPlan, + subscriptionEndDate + }, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to upgrade 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 upgrading restaurant subscription: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async hardDeleteShop(id: string) { + try { + const { data } = await firstValueFrom( + this.httpService + .delete(`${this.config.baseUrl}/super-admin/restaurants/${id}/hard-delete`, { + headers: this.getHeaders(), + data: {}, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to hard delete restaurant: ${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 hard deleting restaurant: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + async directLogin(userSubscriptionId: string, userId: string) { + try { + // 1. Get userSubscription by id + const user = await this.usersService.findOneById(userId) + + if (!user) { + throw new BadRequestException("User Not found"); + } + // 2. Get phone from userSubscription + const phone = user.user.phone + if (!phone) { + throw new BadRequestException("Phone number not found in user subscription"); + } + + // 3. Get restaurant by subscription id + const restaurant = await this.getRestaurantSubscription(userSubscriptionId); + + // 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, + slug, + }, { + headers: this.getHeaders(), + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error(`Failed to perform direct login: ${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 performing direct login: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } +} diff --git a/src/modules/users/enums/permission.enum.ts b/src/modules/users/enums/permission.enum.ts index 588dc65..2040139 100755 --- a/src/modules/users/enums/permission.enum.ts +++ b/src/modules/users/enums/permission.enum.ts @@ -21,4 +21,5 @@ export enum PermissionEnum { MANAGE_SSO_CLIENTS = "manage_sso_clients", SUPPORT_PLAN = "support_plan", DMENU = "dmenu", + DKALA = "dkala", }