Deploy to Staging #9
			
				
			
		
		
		
	| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "id": -1002593407119,
 | 
				
			||||||
 | 
					  "title": "Bid histories dev",
 | 
				
			||||||
 | 
					  "type": "supergroup",
 | 
				
			||||||
 | 
					  "invite_link": "https://t.me/+CSBIA7mbyBhkM2Jl",
 | 
				
			||||||
 | 
					  "permissions": {
 | 
				
			||||||
 | 
					    "can_send_messages": true,
 | 
				
			||||||
 | 
					    "can_send_media_messages": true,
 | 
				
			||||||
 | 
					    "can_send_audios": true,
 | 
				
			||||||
 | 
					    "can_send_documents": true,
 | 
				
			||||||
 | 
					    "can_send_photos": true,
 | 
				
			||||||
 | 
					    "can_send_videos": true,
 | 
				
			||||||
 | 
					    "can_send_video_notes": true,
 | 
				
			||||||
 | 
					    "can_send_voice_notes": true,
 | 
				
			||||||
 | 
					    "can_send_polls": true,
 | 
				
			||||||
 | 
					    "can_send_other_messages": true,
 | 
				
			||||||
 | 
					    "can_add_web_page_previews": true,
 | 
				
			||||||
 | 
					    "can_change_info": true,
 | 
				
			||||||
 | 
					    "can_invite_users": true,
 | 
				
			||||||
 | 
					    "can_pin_messages": true,
 | 
				
			||||||
 | 
					    "can_manage_topics": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "join_to_send_messages": true,
 | 
				
			||||||
 | 
					  "max_reaction_count": 11,
 | 
				
			||||||
 | 
					  "accent_color_id": 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{"createdAt":1743133400720}
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import {
 | 
				
			||||||
} from './system/routes/exclude-route';
 | 
					} from './system/routes/exclude-route';
 | 
				
			||||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
					import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
				
			||||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
					import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
				
			||||||
 | 
					import { NotificationModule } from './modules/notification/notification.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -22,6 +23,7 @@ import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/clien
 | 
				
			||||||
    AppValidatorsModule,
 | 
					    AppValidatorsModule,
 | 
				
			||||||
    AuthModule,
 | 
					    AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
 | 
					    NotificationModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [],
 | 
					  controllers: [],
 | 
				
			||||||
  providers: [],
 | 
					  providers: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,16 @@
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { ConfigModule } from '@nestjs/config';
 | 
					import { ConfigModule } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    ConfigModule.forRoot({
 | 
					    ConfigModule.forRoot({
 | 
				
			||||||
      isGlobal: true,
 | 
					      isGlobal: true,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					    EventEmitterModule.forRoot({
 | 
				
			||||||
 | 
					      wildcard: true,
 | 
				
			||||||
 | 
					      global: true,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AppConfigsModule {}
 | 
					export class AppConfigsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,18 @@ import { escapeMarkdownV2 } from 'src/ultils';
 | 
				
			||||||
import { Bid } from '../entities/bid.entity';
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import * as dayjs from 'dayjs';
 | 
					import * as dayjs from 'dayjs';
 | 
				
			||||||
import { SendMessageHistoriesService } from '../services/send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from '../services/send-message-histories.service';
 | 
				
			||||||
 | 
					import { join } from 'path';
 | 
				
			||||||
 | 
					import { Constant } from '../utils/constant';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  existsSync,
 | 
				
			||||||
 | 
					  mkdir,
 | 
				
			||||||
 | 
					  mkdirSync,
 | 
				
			||||||
 | 
					  readdirSync,
 | 
				
			||||||
 | 
					  readFileSync,
 | 
				
			||||||
 | 
					  statSync,
 | 
				
			||||||
 | 
					  writeFile,
 | 
				
			||||||
 | 
					  writeFileSync,
 | 
				
			||||||
 | 
					} from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BotTelegramApi {
 | 
					export class BotTelegramApi {
 | 
				
			||||||
| 
						 | 
					@ -64,6 +76,73 @@ export class BotTelegramApi {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async createFolderPath(): Promise<string> {
 | 
				
			||||||
 | 
					    const rootDir = process.cwd();
 | 
				
			||||||
 | 
					    const folderPath = join(rootDir, `${Constant.BOT_TELEGRAM_PATH}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!existsSync(folderPath)) {
 | 
				
			||||||
 | 
					      mkdirSync(folderPath, { recursive: true, mode: 0o777 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ✅ Lưu metadata lần đầu
 | 
				
			||||||
 | 
					      const metadataPath = join(folderPath, 'metadata.json');
 | 
				
			||||||
 | 
					      writeFileSync(
 | 
				
			||||||
 | 
					        metadataPath,
 | 
				
			||||||
 | 
					        JSON.stringify({ createdAt: Date.now() }),
 | 
				
			||||||
 | 
					        'utf-8',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return folderPath;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getGroupInfo(
 | 
				
			||||||
 | 
					    chatId: string = this.configService.get<string>('CHAT_ID'),
 | 
				
			||||||
 | 
					  ): Promise<any> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const folderPath = await this.createFolderPath();
 | 
				
			||||||
 | 
					      const metadataPath = join(folderPath, 'metadata.json');
 | 
				
			||||||
 | 
					      const dataFilePath = join(folderPath, `group_${chatId}.json`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 10 minute
 | 
				
			||||||
 | 
					      const TIME_TO_REFRESH_DATA = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (existsSync(metadataPath)) {
 | 
				
			||||||
 | 
					        const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
 | 
				
			||||||
 | 
					        const createdAt = metadata?.createdAt || 0;
 | 
				
			||||||
 | 
					        const now = Date.now();
 | 
				
			||||||
 | 
					        const diffMinutes = (now - createdAt) / 60000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (diffMinutes < TIME_TO_REFRESH_DATA && existsSync(dataFilePath)) {
 | 
				
			||||||
 | 
					          return JSON.parse(readFileSync(dataFilePath, 'utf-8'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const url = `${this.apiUrl}/getChat`;
 | 
				
			||||||
 | 
					      const { data } = await axios({
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        params: { chat_id: chatId },
 | 
				
			||||||
 | 
					        family: 4,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (data?.ok) {
 | 
				
			||||||
 | 
					        writeFileSync(
 | 
				
			||||||
 | 
					          dataFilePath,
 | 
				
			||||||
 | 
					          JSON.stringify(data.result, null, 2),
 | 
				
			||||||
 | 
					          'utf-8',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        writeFileSync(
 | 
				
			||||||
 | 
					          metadataPath,
 | 
				
			||||||
 | 
					          JSON.stringify({ createdAt: Date.now() }),
 | 
				
			||||||
 | 
					          'utf-8',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data.result;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(error || error.message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async sendBidInfo(bid: Bid): Promise<boolean> {
 | 
					  async sendBidInfo(bid: Bid): Promise<boolean> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const text = this.formatBidMessage(bid);
 | 
					      const text = this.formatBidMessage(bid);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@ import { AdminSendMessageHistoriesController } from './controllers/admin/admin-s
 | 
				
			||||||
import { AuthModule } from '../auth/auth.module';
 | 
					import { AuthModule } from '../auth/auth.module';
 | 
				
			||||||
import { AdminsModule } from '../admins/admins.module';
 | 
					import { AdminsModule } from '../admins/admins.module';
 | 
				
			||||||
import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
					import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
				
			||||||
 | 
					import { NotificationModule } from '../notification/notification.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -35,11 +36,9 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
				
			||||||
      WebBid,
 | 
					      WebBid,
 | 
				
			||||||
      SendMessageHistory,
 | 
					      SendMessageHistory,
 | 
				
			||||||
    ]),
 | 
					    ]),
 | 
				
			||||||
    EventEmitterModule.forRoot({
 | 
					 | 
				
			||||||
      wildcard: true,
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    // AuthModule,
 | 
					    // AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
 | 
					    NotificationModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [
 | 
					  controllers: [
 | 
				
			||||||
    BidsController,
 | 
					    BidsController,
 | 
				
			||||||
| 
						 | 
					@ -62,5 +61,6 @@ import { AdminBidGateway } from './getways/admin-bid-getway';
 | 
				
			||||||
    GraysApi,
 | 
					    GraysApi,
 | 
				
			||||||
    SendMessageHistoriesService,
 | 
					    SendMessageHistoriesService,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					  exports: [BotTelegramApi],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class BidsModule {}
 | 
					export class BidsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,16 @@
 | 
				
			||||||
 | 
					import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
				
			||||||
 | 
					import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { JwtService } from '@nestjs/jwt';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  OnGatewayConnection,
 | 
				
			||||||
  WebSocketGateway,
 | 
					  WebSocketGateway,
 | 
				
			||||||
  WebSocketServer,
 | 
					  WebSocketServer,
 | 
				
			||||||
  SubscribeMessage,
 | 
					 | 
				
			||||||
  OnGatewayConnection,
 | 
					 | 
				
			||||||
} from '@nestjs/websockets';
 | 
					} from '@nestjs/websockets';
 | 
				
			||||||
import { Server, Socket } from 'socket.io';
 | 
					 | 
				
			||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
					 | 
				
			||||||
import { BidsService } from '../services/bids.service';
 | 
					 | 
				
			||||||
import { WebBidsService } from '../services/web-bids.service';
 | 
					 | 
				
			||||||
import { plainToClass } from 'class-transformer';
 | 
					import { plainToClass } from 'class-transformer';
 | 
				
			||||||
 | 
					import { Server, Socket } from 'socket.io';
 | 
				
			||||||
import { WebBid } from '../entities/wed-bid.entity';
 | 
					import { WebBid } from '../entities/wed-bid.entity';
 | 
				
			||||||
import * as cookie from 'cookie';
 | 
					import { WebBidsService } from '../services/web-bids.service';
 | 
				
			||||||
import { Constant } from '@/modules/auth/ultils/constant';
 | 
					 | 
				
			||||||
import { getWayMiddleware } from '@/modules/auth/middlewares/get-way.middleware';
 | 
					 | 
				
			||||||
import { AdminsService } from '@/modules/admins/services/admins.service';
 | 
					 | 
				
			||||||
import { JwtService } from '@nestjs/jwt';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@WebSocketGateway({
 | 
					@WebSocketGateway({
 | 
				
			||||||
  namespace: 'admin-bid-ws',
 | 
					  namespace: 'admin-bid-ws',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
 | 
					import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
 | 
				
			||||||
import { BotTelegramApi } from '../apis/bot-telegram.api';
 | 
					import { BotTelegramApi } from '../apis/bot-telegram.api';
 | 
				
			||||||
import { SendMessageHistoriesService } from './send-message-histories.service';
 | 
					import { SendMessageHistoriesService } from './send-message-histories.service';
 | 
				
			||||||
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidHistoriesService {
 | 
					export class BidHistoriesService {
 | 
				
			||||||
| 
						 | 
					@ -23,6 +24,7 @@ export class BidHistoriesService {
 | 
				
			||||||
    readonly bidsRepo: Repository<Bid>,
 | 
					    readonly bidsRepo: Repository<Bid>,
 | 
				
			||||||
    private readonly botTelegramApi: BotTelegramApi,
 | 
					    private readonly botTelegramApi: BotTelegramApi,
 | 
				
			||||||
    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
					    readonly sendMessageHistoriesService: SendMessageHistoriesService,
 | 
				
			||||||
 | 
					    private readonly notificationService: NotificationService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async index() {
 | 
					  async index() {
 | 
				
			||||||
| 
						 | 
					@ -54,6 +56,9 @@ export class BidHistoriesService {
 | 
				
			||||||
    if (price + bid.plus_price > bid.max_price) {
 | 
					    if (price + bid.plus_price > bid.max_price) {
 | 
				
			||||||
      this.bidsRepo.update(bid_id, { status: 'out-bid' });
 | 
					      this.bidsRepo.update(bid_id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // send message event
 | 
				
			||||||
 | 
					      this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      throw new BadRequestException(
 | 
					      throw new BadRequestException(
 | 
				
			||||||
        AppResponse.toResponse(null, {
 | 
					        AppResponse.toResponse(null, {
 | 
				
			||||||
          message: 'Price is more than Max price ' + bid.max_price,
 | 
					          message: 'Price is more than Max price ' + bid.max_price,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
					import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
				
			||||||
import { Constant } from '../utils/constant';
 | 
					import { Constant } from '../utils/constant';
 | 
				
			||||||
import { WebBidsService } from './web-bids.service';
 | 
					import { WebBidsService } from './web-bids.service';
 | 
				
			||||||
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidsService {
 | 
					export class BidsService {
 | 
				
			||||||
| 
						 | 
					@ -39,6 +40,7 @@ export class BidsService {
 | 
				
			||||||
    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
					    readonly bidHistoriesRepo: Repository<BidHistory>,
 | 
				
			||||||
    private readonly webBidsService: WebBidsService,
 | 
					    private readonly webBidsService: WebBidsService,
 | 
				
			||||||
    private eventEmitter: EventEmitter2,
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					    private notificationService: NotificationService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async index(query: PaginateQuery) {
 | 
					  async index(query: PaginateQuery) {
 | 
				
			||||||
| 
						 | 
					@ -159,6 +161,9 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.emitAllBidEvent();
 | 
					      this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // send message event
 | 
				
			||||||
 | 
					      this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return AppResponse.toResponse(true);
 | 
					      return AppResponse.toResponse(true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -183,6 +188,9 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.bidsRepo.update(id, { status: 'biding' });
 | 
					    await this.bidsRepo.update(id, { status: 'biding' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // send message event
 | 
				
			||||||
 | 
					    this.notificationService.emitBidStatus({ ...bid, status: 'biding' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -210,12 +218,6 @@ export class BidsService {
 | 
				
			||||||
      data.current_price >= bid.max_price + bid.plus_price ||
 | 
					      data.current_price >= bid.max_price + bid.plus_price ||
 | 
				
			||||||
      (bid.close_time && isTimeReached(bid.close_time))
 | 
					      (bid.close_time && isTimeReached(bid.close_time))
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      console.log({
 | 
					 | 
				
			||||||
        a: data.current_price >= bid.max_price + bid.plus_price,
 | 
					 | 
				
			||||||
        b: bid.close_time && !close_time,
 | 
					 | 
				
			||||||
        c: bid.close_time && isTimeReached(bid.close_time),
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      bid.status = 'out-bid';
 | 
					      bid.status = 'out-bid';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -234,16 +236,26 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // send event message
 | 
				
			||||||
 | 
					    if (result.status === 'out-bid') {
 | 
				
			||||||
 | 
					      this.notificationService.emitBidStatus(result);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
					    return AppResponse.toResponse(plainToClass(Bid, result));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async outBid(id: Bid['id']) {
 | 
					  async outBid(id: Bid['id']) {
 | 
				
			||||||
    const result = await this.bidsRepo.update(id, { status: 'out-bid' });
 | 
					    const result = await this.bidsRepo.update(id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const bid = await this.bidsRepo.findOne({ where: { id } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
					    if (!result) throw new BadRequestException(AppResponse.toResponse(false));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.emitAllBidEvent();
 | 
					    await this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // send message event
 | 
				
			||||||
 | 
					    this.notificationService.emitBidStatus(bid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -309,9 +321,25 @@ export class BidsService {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lastHistory && lastHistory.price === data.current_price) {
 | 
					    if (lastHistory && lastHistory.price === data.current_price) {
 | 
				
			||||||
      await this.bidsRepo.update(bid.id, { status: 'win-bid' });
 | 
					      if (bid.status !== 'win-bid') {
 | 
				
			||||||
 | 
					        await this.bidsRepo.update(bid.id, { status: 'win-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // send event message
 | 
				
			||||||
 | 
					        this.notificationService.emitBidStatus({
 | 
				
			||||||
 | 
					          ...bid,
 | 
				
			||||||
 | 
					          status: 'win-bid',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      await this.bidsRepo.update(bid.id, { status: 'out-bid' });
 | 
					      if (bid.status !== 'out-bid') {
 | 
				
			||||||
 | 
					        await this.bidsRepo.update(bid.id, { status: 'out-bid' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // send event message
 | 
				
			||||||
 | 
					        this.notificationService.emitBidStatus({
 | 
				
			||||||
 | 
					          ...bid,
 | 
				
			||||||
 | 
					          status: 'out-bid',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.emitAllBidEvent();
 | 
					    this.emitAllBidEvent();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
export class Constant {
 | 
					export class Constant {
 | 
				
			||||||
  public static MEDIA_PATH = 'public';
 | 
					  public static MEDIA_PATH = 'public';
 | 
				
			||||||
 | 
					  public static BOT_TELEGRAM_PATH = 'bot-data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
					  public static WORK_IMAGES_FOLDER = 'work-images';
 | 
				
			||||||
  public static TMP_FOLDER = 'tmp';
 | 
					  public static TMP_FOLDER = 'tmp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					export const NAME_EVENTS = {
 | 
				
			||||||
 | 
					  BID_STATUS: 'notify.bid-status',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export class CreateNotificationDto {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					import { PartialType } from '@nestjs/mapped-types';
 | 
				
			||||||
 | 
					import { CreateNotificationDto } from './create-notification.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpdateNotificationDto extends PartialType(CreateNotificationDto) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('notifications')
 | 
				
			||||||
 | 
					@Index(['message', 'raw_data'])
 | 
				
			||||||
 | 
					export class Notification extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'varchar' })
 | 
				
			||||||
 | 
					  raw_data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
 | 
					  read_at: Date | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ type: 'text' })
 | 
				
			||||||
 | 
					  send_to: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
 | 
				
			||||||
 | 
					export abstract class Timestamp {
 | 
				
			||||||
 | 
					  @CreateDateColumn({ type: 'timestamp', name: 'created_at' })
 | 
				
			||||||
 | 
					  created_at: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
 | 
				
			||||||
 | 
					  updated_at: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { OnEvent } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { NAME_EVENTS } from '../constants';
 | 
				
			||||||
 | 
					import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
				
			||||||
 | 
					import { Notification } from '../entities/notification.entity';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AdminNotificationListener {
 | 
				
			||||||
 | 
					  constructor(private readonly botTelegramApi: BotTelegramApi) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OnEvent(NAME_EVENTS.BID_STATUS)
 | 
				
			||||||
 | 
					  handleBidStatus({
 | 
				
			||||||
 | 
					    bid,
 | 
				
			||||||
 | 
					    notification,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    bid: Bid;
 | 
				
			||||||
 | 
					    notification: Notification;
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    if (JSON.parse(notification.send_to).length <= 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.botTelegramApi.sendMessage(notification.message);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { NotificationController } from './notification.controller';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('NotificationController', () => {
 | 
				
			||||||
 | 
					  let controller: NotificationController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      controllers: [NotificationController],
 | 
				
			||||||
 | 
					      providers: [NotificationService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller = module.get<NotificationController>(NotificationController);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(controller).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					import { Controller, Get } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/notifications')
 | 
				
			||||||
 | 
					export class NotificationController {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly notificationService: NotificationService,
 | 
				
			||||||
 | 
					    private botTelegramApi: BotTelegramApi,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('')
 | 
				
			||||||
 | 
					  async test() {
 | 
				
			||||||
 | 
					    return await this.botTelegramApi.getGroupInfo();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					import { forwardRef, Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					import { NotificationController } from './notification.controller';
 | 
				
			||||||
 | 
					import { BidsModule } from '../bids/bids.module';
 | 
				
			||||||
 | 
					import { Notification } from './entities/notification.entity';
 | 
				
			||||||
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { AdminNotificationListener } from './listeners/admin-notification.listener';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    forwardRef(() => BidsModule),
 | 
				
			||||||
 | 
					    TypeOrmModule.forFeature([Notification]),
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  controllers: [NotificationController],
 | 
				
			||||||
 | 
					  providers: [NotificationService, AdminNotificationListener],
 | 
				
			||||||
 | 
					  exports: [NotificationService],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class NotificationModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { NotificationService } from './notification.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('NotificationService', () => {
 | 
				
			||||||
 | 
					  let service: NotificationService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      providers: [NotificationService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    service = module.get<NotificationService>(NotificationService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,108 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { Bid } from '../bids/entities/bid.entity';
 | 
				
			||||||
 | 
					import { NAME_EVENTS } from './constants';
 | 
				
			||||||
 | 
					import { BotTelegramApi } from '../bids/apis/bot-telegram.api';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Notification } from './entities/notification.entity';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { isTimeReached } from '@/ultils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FilterOperator,
 | 
				
			||||||
 | 
					  FilterSuffix,
 | 
				
			||||||
 | 
					  paginate,
 | 
				
			||||||
 | 
					  PaginateQuery,
 | 
				
			||||||
 | 
					} from 'nestjs-paginate';
 | 
				
			||||||
 | 
					import { Column } from 'nestjs-paginate/lib/helper';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class NotificationService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					    private readonly botTelegramApi: BotTelegramApi,
 | 
				
			||||||
 | 
					    @InjectRepository(Notification)
 | 
				
			||||||
 | 
					    readonly notificationRepo: Repository<Notification>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async index(query: PaginateQuery) {
 | 
				
			||||||
 | 
					    const filterableColumns: {
 | 
				
			||||||
 | 
					      [key in Column<Bid> | (string & {})]?:
 | 
				
			||||||
 | 
					        | (FilterOperator | FilterSuffix)[]
 | 
				
			||||||
 | 
					        | true;
 | 
				
			||||||
 | 
					    } = {
 | 
				
			||||||
 | 
					      id: true,
 | 
				
			||||||
 | 
					      message: true,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await paginate(query, this.notificationRepo, {
 | 
				
			||||||
 | 
					      sortableColumns: ['id'],
 | 
				
			||||||
 | 
					      searchableColumns: ['id'],
 | 
				
			||||||
 | 
					      defaultLimit: 15,
 | 
				
			||||||
 | 
					      filterableColumns,
 | 
				
			||||||
 | 
					      defaultSortBy: [['id', 'DESC']],
 | 
				
			||||||
 | 
					      maxLimit: 100,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toPagination<Notification>(data, true, Notification);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBidStatusMessage(bid: Bid): string | null {
 | 
				
			||||||
 | 
					    switch (bid.status) {
 | 
				
			||||||
 | 
					      case 'biding':
 | 
				
			||||||
 | 
					        return !bid.name ? null : `✅ The item has been activated. ${bid.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'out-bid':
 | 
				
			||||||
 | 
					        if (isTimeReached(bid.close_time)) {
 | 
				
			||||||
 | 
					          return `⏳ The auction for *${bid.name || 'this item'}* has ended.`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const itemName = `*${bid.name || 'the item'}*`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					          bid.max_price + bid.plus_price <= bid.current_price ||
 | 
				
			||||||
 | 
					          bid.reserve_price > bid.max_price + bid.plus_price
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          return `💰 The current bid for ${itemName} has exceeded your maximum bid.`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `🛑 The auction for ${itemName} has been canceled.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case 'win-bid':
 | 
				
			||||||
 | 
					        return `🎉 Congratulations! You won the auction for ${itemName} at *${bid.current_price}*.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return '❓ Unknown auction status.';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async emitBidStatus(bid: Bid, sendTo: boolean = true) {
 | 
				
			||||||
 | 
					    const groupData = await this.botTelegramApi.getGroupInfo();
 | 
				
			||||||
 | 
					    const sendToData = groupData && sendTo ? [groupData?.title || 'None'] : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const message = this.getBidStatusMessage(bid);
 | 
				
			||||||
 | 
					    if (!message) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const notification = await this.notificationRepo.save({
 | 
				
			||||||
 | 
					      message,
 | 
				
			||||||
 | 
					      raw_data: JSON.stringify({
 | 
				
			||||||
 | 
					        id: bid.id,
 | 
				
			||||||
 | 
					        status: bid.status,
 | 
				
			||||||
 | 
					        name: bid.name,
 | 
				
			||||||
 | 
					        close_time: bid.close_time,
 | 
				
			||||||
 | 
					        current_price: bid.current_price,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      send_to: JSON.stringify(sendToData),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, {
 | 
				
			||||||
 | 
					      bid: {
 | 
				
			||||||
 | 
					        ...bid,
 | 
				
			||||||
 | 
					        status: 'out-bid',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      notification,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue