dkala module

This commit is contained in:
2026-02-15 11:29:47 +03:30
parent 5e8cfed98e
commit efd060c46a
19 changed files with 1249 additions and 0 deletions
+2
View File
@@ -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: [],
})
+20
View File
@@ -0,0 +1,20 @@
import { ConfigService } from "@nestjs/config";
export function dkalaConfig() {
return {
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
baseUrl: configService.get<string>("DKALA_BACKEND_URL")??'https://dkala-api.danakcorp.com',
username: configService.get<string>("DKALA_USERNAME")??'danak@dsc.com',
password: configService.get<string>("DKALA_PASSWORD")??'DsCdAnAk?@ABC',
};
},
};
}
export interface IDkalaConfig {
baseUrl: string;
username: string;
password: string;
}
@@ -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;
}
@@ -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;
}
+14
View File
@@ -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;
}
@@ -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;
}
+15
View File
@@ -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;
}
+64
View File
@@ -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';
}
@@ -0,0 +1,6 @@
import { PartialType } from "@nestjs/swagger";
import { CreateGroupDto } from "./create-group.dto";
export class UpdateGroupDto extends PartialType(CreateGroupDto) {}
+6
View File
@@ -0,0 +1,6 @@
import { PartialType } from "@nestjs/swagger";
import { CreateIconDto } from "./create-icon.dto";
export class UpdateIconDto extends PartialType(CreateIconDto) {}
+48
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export const DKALA_CONFIG = "DKALA_CONFIG";
+219
View File
@@ -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);
}
}
+31
View File
@@ -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 {}
@@ -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<T> {
data: T;
meta: IExternalApiPaginationMeta;
}
@@ -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<string, string> {
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;
}
}
}
@@ -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<string, string> {
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;
}
}
}
+494
View File
@@ -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<string, string> {
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<string, any> = {
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<IExternalApiResponse<any[]>>(`${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;
}
}
}
@@ -21,4 +21,5 @@ export enum PermissionEnum {
MANAGE_SSO_CLIENTS = "manage_sso_clients",
SUPPORT_PLAN = "support_plan",
DMENU = "dmenu",
DKALA = "dkala",
}