Deploy to staging #41
			
				
			
		
		
		
	| 
						 | 
					@ -118,10 +118,6 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!isIBid(data)) {
 | 
					 | 
				
			||||||
    console.log(data);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Box
 | 
					      <Box
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,12 +26,12 @@ const schema = {
 | 
				
			||||||
    .number({ message: "Arrival offset seconds is required" })
 | 
					    .number({ message: "Arrival offset seconds is required" })
 | 
				
			||||||
    .refine((val) => val >= 60, {
 | 
					    .refine((val) => val >= 60, {
 | 
				
			||||||
      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
					      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
				
			||||||
    }),
 | 
					    }).optional(),
 | 
				
			||||||
    early_tracking_seconds: z
 | 
					    early_tracking_seconds: z
 | 
				
			||||||
    .number({ message: "Early login seconds is required" })
 | 
					    .number({ message: "Early login seconds is required" })
 | 
				
			||||||
    .refine((val) => val >= 600, {
 | 
					    .refine((val) => val >= 600, {
 | 
				
			||||||
      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
					      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
				
			||||||
    }),
 | 
					    }).optional(),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebBidModal({
 | 
					export default function WebBidModal({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
{"createdAt":1747011314493}
 | 
					{"createdAt":1747191706164}
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@
 | 
				
			||||||
        "@nestjs/mapped-types": "*",
 | 
					        "@nestjs/mapped-types": "*",
 | 
				
			||||||
        "@nestjs/platform-express": "^10.4.15",
 | 
					        "@nestjs/platform-express": "^10.4.15",
 | 
				
			||||||
        "@nestjs/platform-socket.io": "^11.0.11",
 | 
					        "@nestjs/platform-socket.io": "^11.0.11",
 | 
				
			||||||
 | 
					        "@nestjs/schedule": "^6.0.0",
 | 
				
			||||||
        "@nestjs/throttler": "^6.4.0",
 | 
					        "@nestjs/throttler": "^6.4.0",
 | 
				
			||||||
        "@nestjs/typeorm": "^11.0.0",
 | 
					        "@nestjs/typeorm": "^11.0.0",
 | 
				
			||||||
        "@nestjs/websockets": "^11.0.11",
 | 
					        "@nestjs/websockets": "^11.0.11",
 | 
				
			||||||
| 
						 | 
					@ -51,7 +52,7 @@
 | 
				
			||||||
        "@types/jest": "^29.5.2",
 | 
					        "@types/jest": "^29.5.2",
 | 
				
			||||||
        "@types/lodash": "^4.17.16",
 | 
					        "@types/lodash": "^4.17.16",
 | 
				
			||||||
        "@types/multer": "^1.4.12",
 | 
					        "@types/multer": "^1.4.12",
 | 
				
			||||||
        "@types/node": "^20.3.1",
 | 
					        "@types/node": "^20.17.46",
 | 
				
			||||||
        "@types/supertest": "^6.0.0",
 | 
					        "@types/supertest": "^6.0.0",
 | 
				
			||||||
        "@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
					        "@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
				
			||||||
        "@typescript-eslint/parser": "^6.0.0",
 | 
					        "@typescript-eslint/parser": "^6.0.0",
 | 
				
			||||||
| 
						 | 
					@ -2458,6 +2459,19 @@
 | 
				
			||||||
        "rxjs": "^7.1.0"
 | 
					        "rxjs": "^7.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@nestjs/schedule": {
 | 
				
			||||||
 | 
					      "version": "6.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "cron": "4.3.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "@nestjs/common": "^10.0.0 || ^11.0.0",
 | 
				
			||||||
 | 
					        "@nestjs/core": "^10.0.0 || ^11.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@nestjs/schematics": {
 | 
					    "node_modules/@nestjs/schematics": {
 | 
				
			||||||
      "version": "10.2.3",
 | 
					      "version": "10.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -3007,6 +3021,12 @@
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/luxon": {
 | 
				
			||||||
 | 
					      "version": "3.6.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/methods": {
 | 
					    "node_modules/@types/methods": {
 | 
				
			||||||
      "version": "1.1.4",
 | 
					      "version": "1.1.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
 | 
				
			||||||
| 
						 | 
					@ -3032,9 +3052,9 @@
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/node": {
 | 
					    "node_modules/@types/node": {
 | 
				
			||||||
      "version": "20.17.24",
 | 
					      "version": "20.17.46",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz",
 | 
				
			||||||
      "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
 | 
					      "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==",
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "undici-types": "~6.19.2"
 | 
					        "undici-types": "~6.19.2"
 | 
				
			||||||
| 
						 | 
					@ -4875,6 +4895,19 @@
 | 
				
			||||||
      "devOptional": true,
 | 
					      "devOptional": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cron": {
 | 
				
			||||||
 | 
					      "version": "4.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/luxon": "~3.6.0",
 | 
				
			||||||
 | 
					        "luxon": "~3.6.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=18.x"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/cross-spawn": {
 | 
					    "node_modules/cross-spawn": {
 | 
				
			||||||
      "version": "7.0.6",
 | 
					      "version": "7.0.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 | 
				
			||||||
| 
						 | 
					@ -8335,6 +8368,15 @@
 | 
				
			||||||
        "url": "https://github.com/sponsors/wellwelwel"
 | 
					        "url": "https://github.com/sponsors/wellwelwel"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/luxon": {
 | 
				
			||||||
 | 
					      "version": "3.6.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=12"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/magic-string": {
 | 
					    "node_modules/magic-string": {
 | 
				
			||||||
      "version": "0.30.8",
 | 
					      "version": "0.30.8",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,7 @@
 | 
				
			||||||
    "@nestjs/mapped-types": "*",
 | 
					    "@nestjs/mapped-types": "*",
 | 
				
			||||||
    "@nestjs/platform-express": "^10.4.15",
 | 
					    "@nestjs/platform-express": "^10.4.15",
 | 
				
			||||||
    "@nestjs/platform-socket.io": "^11.0.11",
 | 
					    "@nestjs/platform-socket.io": "^11.0.11",
 | 
				
			||||||
 | 
					    "@nestjs/schedule": "^6.0.0",
 | 
				
			||||||
    "@nestjs/throttler": "^6.4.0",
 | 
					    "@nestjs/throttler": "^6.4.0",
 | 
				
			||||||
    "@nestjs/typeorm": "^11.0.0",
 | 
					    "@nestjs/typeorm": "^11.0.0",
 | 
				
			||||||
    "@nestjs/websockets": "^11.0.11",
 | 
					    "@nestjs/websockets": "^11.0.11",
 | 
				
			||||||
| 
						 | 
					@ -67,7 +68,7 @@
 | 
				
			||||||
    "@types/jest": "^29.5.2",
 | 
					    "@types/jest": "^29.5.2",
 | 
				
			||||||
    "@types/lodash": "^4.17.16",
 | 
					    "@types/lodash": "^4.17.16",
 | 
				
			||||||
    "@types/multer": "^1.4.12",
 | 
					    "@types/multer": "^1.4.12",
 | 
				
			||||||
    "@types/node": "^20.3.1",
 | 
					    "@types/node": "^20.17.46",
 | 
				
			||||||
    "@types/supertest": "^6.0.0",
 | 
					    "@types/supertest": "^6.0.0",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
					    "@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "^6.0.0",
 | 
					    "@typescript-eslint/parser": "^6.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
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';
 | 
					import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { ScheduleModule } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -11,6 +12,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
      wildcard: true,
 | 
					      wildcard: true,
 | 
				
			||||||
      global: true,
 | 
					      global: true,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
					    ScheduleModule.forRoot()
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AppConfigsModule {}
 | 
					export class AppConfigsModule {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,9 @@ import { SendMessageHistoriesService } from './services/send-message-histories.s
 | 
				
			||||||
import { WebBidsService } from './services/web-bids.service';
 | 
					import { WebBidsService } from './services/web-bids.service';
 | 
				
			||||||
import { DashboardService } from './services/dashboard.service';
 | 
					import { DashboardService } from './services/dashboard.service';
 | 
				
			||||||
import { AdminDashboardController } from './controllers/admin/admin-dashboard.controller';
 | 
					import { AdminDashboardController } from './controllers/admin/admin-dashboard.controller';
 | 
				
			||||||
 | 
					import { TasksService } from './services/tasks.servise';
 | 
				
			||||||
 | 
					import { ConfigsService } from './services/configs.service';
 | 
				
			||||||
 | 
					import { Config } from './entities/configs.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -36,6 +39,7 @@ import { AdminDashboardController } from './controllers/admin/admin-dashboard.co
 | 
				
			||||||
      OutBidLog,
 | 
					      OutBidLog,
 | 
				
			||||||
      WebBid,
 | 
					      WebBid,
 | 
				
			||||||
      SendMessageHistory,
 | 
					      SendMessageHistory,
 | 
				
			||||||
 | 
					      Config
 | 
				
			||||||
    ]),
 | 
					    ]),
 | 
				
			||||||
    // AuthModule,
 | 
					    // AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
| 
						 | 
					@ -64,6 +68,8 @@ import { AdminDashboardController } from './controllers/admin/admin-dashboard.co
 | 
				
			||||||
    SendMessageHistoriesService,
 | 
					    SendMessageHistoriesService,
 | 
				
			||||||
    ImapService,
 | 
					    ImapService,
 | 
				
			||||||
    DashboardService,
 | 
					    DashboardService,
 | 
				
			||||||
 | 
					    TasksService,
 | 
				
			||||||
 | 
					    ConfigsService
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
 | 
					  exports: [BotTelegramApi, SendMessageHistoriesService, BidsService],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { IsNumber, IsString, IsUrl } from 'class-validator';
 | 
					import { IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class CreateWebBidDto {
 | 
					export class CreateWebBidDto {
 | 
				
			||||||
  @IsUrl()
 | 
					  @IsUrl()
 | 
				
			||||||
| 
						 | 
					@ -6,4 +6,14 @@ export class CreateWebBidDto {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @IsUrl()
 | 
					  @IsUrl()
 | 
				
			||||||
  url: string;
 | 
					  url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @Min(60)
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  arrival_offset_seconds: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @Min(600)
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  early_tracking_seconds: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('configs')
 | 
				
			||||||
 | 
					export class Config extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ unique: true })
 | 
				
			||||||
 | 
					  key_name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: true })
 | 
				
			||||||
 | 
					  value: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  type: 'string' | 'number';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,9 @@
 | 
				
			||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
 | 
					import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
				
			||||||
import { Timestamp } from './timestamp';
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
import { Bid } from './bid.entity';
 | 
					import { Bid } from './bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('send_message_histories')
 | 
					@Entity('send_message_histories')
 | 
				
			||||||
 | 
					@Unique(['max_price', 'type', 'reserve_price'])
 | 
				
			||||||
export class SendMessageHistory extends Timestamp {
 | 
					export class SendMessageHistory extends Timestamp {
 | 
				
			||||||
  @PrimaryGeneratedColumn('increment')
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
  id: number;
 | 
					  id: number;
 | 
				
			||||||
| 
						 | 
					@ -17,4 +18,10 @@ export class SendMessageHistory extends Timestamp {
 | 
				
			||||||
    onDelete: 'CASCADE',
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  bid: Bid;
 | 
					  bid: Bid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 0, nullable: true })
 | 
				
			||||||
 | 
					  max_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 0, nullable: true })
 | 
				
			||||||
 | 
					  reserve_price: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ import { Column } from 'nestjs-paginate/lib/helper';
 | 
				
			||||||
import { join } from 'path';
 | 
					import { join } from 'path';
 | 
				
			||||||
import AppResponse from 'src/response/app-response';
 | 
					import AppResponse from 'src/response/app-response';
 | 
				
			||||||
import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
 | 
					import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
 | 
				
			||||||
import { In, Repository } from 'typeorm';
 | 
					import { In, IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
 | 
					import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
 | 
				
			||||||
import { CreateBidDto } from '../dto/bid/create-bid.dto';
 | 
					import { CreateBidDto } from '../dto/bid/create-bid.dto';
 | 
				
			||||||
import { UpdateBidDto } from '../dto/bid/update-bid.dto';
 | 
					import { UpdateBidDto } from '../dto/bid/update-bid.dto';
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ export class BidsService {
 | 
				
			||||||
      lot_id: true,
 | 
					      lot_id: true,
 | 
				
			||||||
      close_time: true,
 | 
					      close_time: true,
 | 
				
			||||||
      name: [FilterOperator.ILIKE],
 | 
					      name: [FilterOperator.ILIKE],
 | 
				
			||||||
      status: true
 | 
					      status: true,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
					    query.filter = AppResponse.processFilters(query.filter, filterableColumns);
 | 
				
			||||||
| 
						 | 
					@ -524,4 +524,29 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(true);
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getNextBid(): Promise<Bid | null> {
 | 
				
			||||||
 | 
					    const all = await this.bidsRepo.find({
 | 
				
			||||||
 | 
					      where: { status: 'biding', close_time: Not(IsNull()) },
 | 
				
			||||||
 | 
					      relations: { web_bid: true },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const now = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let nextBid = null;
 | 
				
			||||||
 | 
					    let minDiff = Infinity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const bid of all) {
 | 
				
			||||||
 | 
					      const time = Date.parse(bid.close_time);
 | 
				
			||||||
 | 
					      if (!isNaN(time) && time >= now) {
 | 
				
			||||||
 | 
					        const diff = time - now;
 | 
				
			||||||
 | 
					        if (diff < minDiff) {
 | 
				
			||||||
 | 
					          minDiff = diff;
 | 
				
			||||||
 | 
					          nextBid = bid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return nextBid;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { Config } from '../entities/configs.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class ConfigsService {
 | 
				
			||||||
 | 
					  public static CONFIG_KEYS = {
 | 
				
			||||||
 | 
					    REFRESH_TOOL_TIME: 'REFRESH_TOOL_TIME',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(Config)
 | 
				
			||||||
 | 
					    readonly configRepo: Repository<Config>,
 | 
				
			||||||
 | 
					    private eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS) {
 | 
				
			||||||
 | 
					    return await this.configRepo.findOne({ where: { key_name } }) || null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async setConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS, value: string) {
 | 
				
			||||||
 | 
					    return await this.configRepo.upsert(
 | 
				
			||||||
 | 
					      { key_name, value },
 | 
				
			||||||
 | 
					      ['key_name'] 
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Cron, CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					import { IsNull, Not } from 'typeorm';
 | 
				
			||||||
 | 
					import { BidsService } from './bids.service';
 | 
				
			||||||
 | 
					import { isTimeReached, subtractMinutes, subtractSeconds } from '@/ultils';
 | 
				
			||||||
 | 
					import { ConfigsService } from './configs.service';
 | 
				
			||||||
 | 
					import { DashboardService } from './dashboard.service';
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class TasksService {
 | 
				
			||||||
 | 
					  private readonly logger = new Logger(TasksService.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly bidsService: BidsService,
 | 
				
			||||||
 | 
					    private readonly configsService: ConfigsService,
 | 
				
			||||||
 | 
					    private readonly dashboadService: DashboardService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Cron(CronExpression.EVERY_MINUTE)
 | 
				
			||||||
 | 
					  async handleCron() {
 | 
				
			||||||
 | 
					    const nextBid = await this.bidsService.getNextBid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!nextBid) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const timeReset = subtractMinutes(nextBid.close_time, 20);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const timeToTracking = subtractSeconds(
 | 
				
			||||||
 | 
					      nextBid.close_time,
 | 
				
			||||||
 | 
					      nextBid.web_bid.early_tracking_seconds,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isTimeReached(timeReset) || isTimeReached(timeToTracking)) {
 | 
				
			||||||
 | 
					      console.log('Reset not allowed at this time');
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const lastestResetToolTime =
 | 
				
			||||||
 | 
					      await this.configsService.getConfig('REFRESH_TOOL_TIME');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (lastestResetToolTime?.value) {
 | 
				
			||||||
 | 
					      const lastReset = Date.parse(lastestResetToolTime.value);
 | 
				
			||||||
 | 
					      const now = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const diffInMs = now - lastReset;
 | 
				
			||||||
 | 
					      const diffInHours = diffInMs / (1000 * 60 * 60);
 | 
				
			||||||
 | 
					      const minimumHours = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (diffInHours < minimumHours) {
 | 
				
			||||||
 | 
					        console.log(`Last reset was less than ${minimumHours} hours ago`);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('Proceeding to reset tool for next bid:', nextBid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.dashboadService.resetTool();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const time = new Date().toUTCString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.configsService.setConfig(
 | 
				
			||||||
 | 
					      'REFRESH_TOOL_TIME',
 | 
				
			||||||
 | 
					      time,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('Reset successfully at: ' + time);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -109,10 +109,25 @@ export class NotificationService {
 | 
				
			||||||
      send_to: JSON.stringify(sendToData),
 | 
					      send_to: JSON.stringify(sendToData),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const prevAnyMessage = await this.sendMessageRepo.findOne({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          bid: { id: bid.id },
 | 
				
			||||||
 | 
					          message: notification.message,
 | 
				
			||||||
 | 
					          type: bid.status,
 | 
				
			||||||
 | 
					          max_price: bid.max_price,
 | 
				
			||||||
 | 
					          reserve_price: bid.reserve_price
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (prevAnyMessage) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.sendMessageRepo.save({
 | 
					      await this.sendMessageRepo.save({
 | 
				
			||||||
        bid: { id: bid.id },
 | 
					        bid: { id: bid.id },
 | 
				
			||||||
        message: notification.message,
 | 
					        message: notification.message,
 | 
				
			||||||
        type: bid.status,
 | 
					        type: bid.status,
 | 
				
			||||||
 | 
					        max_price: bid.max_price,
 | 
				
			||||||
 | 
					        reserve_price: bid.reserve_price
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, {
 | 
					      this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, {
 | 
				
			||||||
| 
						 | 
					@ -122,5 +137,12 @@ export class NotificationService {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        notification,
 | 
					        notification,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        '%csrc/modules/notification/notification.service.ts:131 Error',
 | 
				
			||||||
 | 
					        'color: #007acc;',
 | 
				
			||||||
 | 
					        Error,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,12 @@ export function subtractMinutes(timeStr: string, minutes: number) {
 | 
				
			||||||
  return date.toISOString(); // Trả về dạng chuẩn ISO
 | 
					  return date.toISOString(); // Trả về dạng chuẩn ISO
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function subtractSeconds(time: string, seconds: number) {
 | 
				
			||||||
 | 
					  const date = new Date(time);
 | 
				
			||||||
 | 
					  date.setSeconds(date.getSeconds() - seconds);
 | 
				
			||||||
 | 
					  return date.toUTCString();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isTimeReached(targetTime: string) {
 | 
					export function isTimeReached(targetTime: string) {
 | 
				
			||||||
  const targetDate = new Date(targetTime);
 | 
					  const targetDate = new Date(targetTime);
 | 
				
			||||||
  const now = new Date();
 | 
					  const now = new Date();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -184,6 +184,8 @@ const tracking = async () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Kiểm tra URL và điều hướng nếu cần
 | 
					          // Kiểm tra URL và điều hướng nếu cần
 | 
				
			||||||
          if ((await productTab.page_context.url()) !== productTab.url) {
 | 
					          if ((await productTab.page_context.url()) !== productTab.url) {
 | 
				
			||||||
 | 
					            if (global[`IS_PLACE_BID-${productTab.id}`]) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log(
 | 
					            console.log(
 | 
				
			||||||
              `🔄 Redirecting to new URL for Product ID: ${productTab.id}`
 | 
					              `🔄 Redirecting to new URL for Product ID: ${productTab.id}`
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,9 +98,15 @@ export class ApiBid extends Bid {
 | 
				
			||||||
    if (this.snapshot_at) {
 | 
					    if (this.snapshot_at) {
 | 
				
			||||||
      const nearestCloseTime = findNearestClosingChild(this);
 | 
					      const nearestCloseTime = findNearestClosingChild(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!nearestCloseTime) {
 | 
					      if (!nearestCloseTime || this.children.some((item) => !item.close_time)) {
 | 
				
			||||||
        console.log(`❌ [${this.id}] No nearest closing child found.`);
 | 
					        console.log(`🔌 [${this.id}] Connecting to puppeteer...`);
 | 
				
			||||||
        return false;
 | 
					        await this.puppeteer_connect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`✅ [${this.id}] Connected. Executing actions...`);
 | 
				
			||||||
 | 
					        await this.action();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`🎯 [${this.id}] handlePrevListen completed.`);
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const { close_time } = nearestCloseTime;
 | 
					      const { close_time } = nearestCloseTime;
 | 
				
			||||||
| 
						 | 
					@ -137,10 +143,13 @@ export class ApiBid extends Bid {
 | 
				
			||||||
    // Nếu chưa có ảnh chụp working => tab not lazy
 | 
					    // Nếu chưa có ảnh chụp working => tab not lazy
 | 
				
			||||||
    if (!this.snapshot_at) return false;
 | 
					    if (!this.snapshot_at) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nếu có một children chưa có thông tin => tab not lazy
 | 
				
			||||||
 | 
					    if (this.children.some((item) => !item.close_time)) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const nearestCloseTime = findNearestClosingChild(this);
 | 
					    const nearestCloseTime = findNearestClosingChild(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu không có nearest close => tab lazy
 | 
					    // Nếu không có nearest close => tab not lazy
 | 
				
			||||||
    if (!nearestCloseTime) return true;
 | 
					    if (!nearestCloseTime) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { close_time } = nearestCloseTime;
 | 
					    const { close_time } = nearestCloseTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -125,37 +125,37 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
      (bid) => !this.children_processing.some((item) => item.model === bid.Sku)
 | 
					      (bid) => !this.children_processing.some((item) => item.model === bid.Sku)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleChildren = this.children.filter((item) =>
 | 
					    // const handleChildren = this.children.filter((item) =>
 | 
				
			||||||
      bidOutLots.some((i) => i.Sku === item.model)
 | 
					    //   bidOutLots.some((i) => i.Sku === item.model)
 | 
				
			||||||
    );
 | 
					    // );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log({
 | 
					    // console.log({
 | 
				
			||||||
      handleChildren,
 | 
					    //   handleChildren,
 | 
				
			||||||
      children_processing: this.children_processing,
 | 
					    //   children_processing: this.children_processing,
 | 
				
			||||||
      data,
 | 
					    //   data,
 | 
				
			||||||
      bidOutLots,
 | 
					    //   bidOutLots,
 | 
				
			||||||
    });
 | 
					    // });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const product_tab of handleChildren) {
 | 
					    // for (const product_tab of handleChildren) {
 | 
				
			||||||
      if (!isTimeReached(product_tab.start_bid_time)) {
 | 
					    //   if (!isTimeReached(product_tab.start_bid_time)) {
 | 
				
			||||||
        console.log(
 | 
					    //     console.log(
 | 
				
			||||||
          `❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`
 | 
					    //       `❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`
 | 
				
			||||||
        );
 | 
					    //     );
 | 
				
			||||||
        return;
 | 
					    //     return;
 | 
				
			||||||
      }
 | 
					    //   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.children_processing.push(product_tab);
 | 
					    //   this.children_processing.push(product_tab);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!product_tab.page_context) {
 | 
					    //   if (!product_tab.page_context) {
 | 
				
			||||||
        await product_tab.puppeteer_connect();
 | 
					    //     await product_tab.puppeteer_connect();
 | 
				
			||||||
      }
 | 
					    //   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await product_tab.action();
 | 
					    //   await product_tab.action();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.children_processing = this.children_processing.filter(
 | 
					    //   this.children_processing = this.children_processing.filter(
 | 
				
			||||||
        (item) => item.id !== product_tab.id
 | 
					    //     (item) => item.id !== product_tab.id
 | 
				
			||||||
      );
 | 
					    //   );
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isLogin = async () => {
 | 
					  isLogin = async () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,322 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  outBid,
 | 
				
			||||||
 | 
					  pushPrice,
 | 
				
			||||||
 | 
					  updateBid,
 | 
				
			||||||
 | 
					  updateStatusByPrice,
 | 
				
			||||||
 | 
					} from "../../system/apis/bid.js";
 | 
				
			||||||
 | 
					import CONSTANTS from "../../system/constants.js";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  delay,
 | 
				
			||||||
 | 
					  extractNumber,
 | 
				
			||||||
 | 
					  isNumber,
 | 
				
			||||||
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					  removeFalsyValues,
 | 
				
			||||||
 | 
					  safeClosePage,
 | 
				
			||||||
 | 
					  takeSnapshot,
 | 
				
			||||||
 | 
					} from "../../system/utils.js";
 | 
				
			||||||
 | 
					import { ProductBid } from "../product-bid.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GraysProductBidBackup extends ProductBid {
 | 
				
			||||||
 | 
					  constructor({ ...prev }) {
 | 
				
			||||||
 | 
					    super(prev);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async validate({ page, price_value }) {
 | 
				
			||||||
 | 
					    if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
 | 
				
			||||||
 | 
					      console.log(`❌ [${this.id}] It's not time yet`);
 | 
				
			||||||
 | 
					      return { result: false, bid_price: 0 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isNumber(price_value)) {
 | 
				
			||||||
 | 
					      console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
 | 
				
			||||||
 | 
					      await takeSnapshot(page, this, "price-value-null");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { result: false, bid_price: 0 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const bid_price = this.plus_price + Number(price_value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (bid_price > this.max_price) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      await takeSnapshot(page, this, "price-bid-more-than");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await outBid(this.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { result: false, bid_price: 0 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await pushPrice({
 | 
				
			||||||
 | 
					      bid_id: this.id,
 | 
				
			||||||
 | 
					      price: bid_price,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response.status) {
 | 
				
			||||||
 | 
					      return { result: false, bid_price: 0 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.histories = response.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // RESET first bid
 | 
				
			||||||
 | 
					    if (this.histories.length > 0 && this.first_bid) {
 | 
				
			||||||
 | 
					      this.first_bid = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { result: true, bid_price };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getCloseTime = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (!this.page_context) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.page_context.waitForSelector("#lot-closing-datetime", {
 | 
				
			||||||
 | 
					        timeout: 3000,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return await this.page_context.$eval(
 | 
				
			||||||
 | 
					        "#lot-closing-datetime",
 | 
				
			||||||
 | 
					        (el) => el.value
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getPriceWasBid = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (!this.page_context) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.page_context.waitForSelector(
 | 
				
			||||||
 | 
					        "#biddableLot form div div:nth-child(1) span span",
 | 
				
			||||||
 | 
					        { timeout: 3000 }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const element = await this.page_context.$(
 | 
				
			||||||
 | 
					        "#biddableLot form div div:nth-child(1) span span"
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const textPrice = await this.page_context.evaluate(
 | 
				
			||||||
 | 
					        (el) => el.textContent,
 | 
				
			||||||
 | 
					        element
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return extractNumber(textPrice) || null;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async isCloseProduct() {
 | 
				
			||||||
 | 
					    const close_time = await this.getCloseTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!close_time) {
 | 
				
			||||||
 | 
					      const priceWasBid = await this.getPriceWasBid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await updateStatusByPrice(this.id, priceWasBid);
 | 
				
			||||||
 | 
					      return { result: true, close_time: null };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
 | 
				
			||||||
 | 
					      console.log(`❌ [${this.id}] Product is close ${close_time}`);
 | 
				
			||||||
 | 
					      return { result: true, close_time };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { result: false, close_time };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleWritePrice(page, bid_price) {
 | 
				
			||||||
 | 
					    await page.type("#price", String(bid_price));
 | 
				
			||||||
 | 
					    await delay(500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async placeBid(page) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await page.click("#bid-type-standard");
 | 
				
			||||||
 | 
					      await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.click("#btnSubmit");
 | 
				
			||||||
 | 
					      await delay(1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.waitForSelector("button", { timeout: 5000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.click("button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.waitForNavigation({ timeout: 5000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await takeSnapshot(
 | 
				
			||||||
 | 
					        page,
 | 
				
			||||||
 | 
					        this,
 | 
				
			||||||
 | 
					        "bid-success",
 | 
				
			||||||
 | 
					        CONSTANTS.TYPE_IMAGE.SUCCESS
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(`❌ [${this.id}] Timeout to loading`);
 | 
				
			||||||
 | 
					      await takeSnapshot(page, this, "timeout to loading");
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleReturnProductPage(page) {
 | 
				
			||||||
 | 
					    await page.goto(this.url);
 | 
				
			||||||
 | 
					    await delay(1000);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleUpdateBid({
 | 
				
			||||||
 | 
					    lot_id,
 | 
				
			||||||
 | 
					    close_time,
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    current_price,
 | 
				
			||||||
 | 
					    reserve_price,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const response = await updateBid(this.id, {
 | 
				
			||||||
 | 
					      lot_id,
 | 
				
			||||||
 | 
					      close_time,
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					      current_price,
 | 
				
			||||||
 | 
					      reserve_price: Number(reserve_price) || 0,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response) {
 | 
				
			||||||
 | 
					      this.lot_id = response.lot_id;
 | 
				
			||||||
 | 
					      this.close_time = response.close_time;
 | 
				
			||||||
 | 
					      this.start_bid_time = response.start_bid_time;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update = async () => {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const close_time = await this.getCloseTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Chờ phần tử xuất hiện trước khi lấy giá trị
 | 
				
			||||||
 | 
					      await page
 | 
				
			||||||
 | 
					        .waitForSelector("#priceValue", { timeout: 5000 })
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const price_value = await page
 | 
				
			||||||
 | 
					        .$eval("#priceValue", (el) => el.value)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null);
 | 
				
			||||||
 | 
					      const lot_id = await page
 | 
				
			||||||
 | 
					        .$eval("#lotId", (el) => el.value)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page
 | 
				
			||||||
 | 
					        .waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", {
 | 
				
			||||||
 | 
					          timeout: 5000,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const name = await page
 | 
				
			||||||
 | 
					        .$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page
 | 
				
			||||||
 | 
					        .waitForSelector(
 | 
				
			||||||
 | 
					          "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
 | 
				
			||||||
 | 
					          { timeout: 5000 }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const current_price = await page
 | 
				
			||||||
 | 
					        .$eval(
 | 
				
			||||||
 | 
					          "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
 | 
				
			||||||
 | 
					          (el) => el.innerText
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const data = removeFalsyValues(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          lot_id,
 | 
				
			||||||
 | 
					          reserve_price: price_value,
 | 
				
			||||||
 | 
					          close_time: close_time ? String(close_time) : null,
 | 
				
			||||||
 | 
					          name,
 | 
				
			||||||
 | 
					          current_price: current_price ? extractNumber(current_price) : null,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ["close_time"]
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.handleUpdateBid(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return { price_value, lot_id, name, current_price };
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`🚨 Error updating product info: ${error.message}`);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  action = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.gotoLink();
 | 
				
			||||||
 | 
					      console.log(`🌍 [${this.id}] Navigated to link.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await delay(1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { close_time, ...isCloseProduct } = await this.isCloseProduct();
 | 
				
			||||||
 | 
					      if (isCloseProduct.result) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `❌ [${this.id}] The product is closed, cannot place a bid.`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { price_value } = await this.update();
 | 
				
			||||||
 | 
					      if (!price_value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { result, bid_price } = await this.validate({ page, price_value });
 | 
				
			||||||
 | 
					      if (!result) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `❌ [${this.id}] Validation failed. Unable to proceed with bidding.`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
				
			||||||
 | 
					      if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (price_value != bid_price) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        await this.handleWritePrice(page, bid_price);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(`🚀 [${this.id}] Placing the bid...`);
 | 
				
			||||||
 | 
					      const resultPlaceBid = await this.placeBid(page);
 | 
				
			||||||
 | 
					      if (!resultPlaceBid) {
 | 
				
			||||||
 | 
					        console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
 | 
				
			||||||
 | 
					        await takeSnapshot(page, this, "place-bid-action");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      await this.handleReturnProductPage(page);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(
 | 
				
			||||||
 | 
					        `🚨 [${this.id}] Error navigating the page: ${error.message}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -21,51 +21,6 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
    super(prev);
 | 
					    super(prev);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async validate({ page, price_value }) {
 | 
					 | 
				
			||||||
    if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
 | 
					 | 
				
			||||||
      console.log(`❌ [${this.id}] It's not time yet`);
 | 
					 | 
				
			||||||
      return { result: false, bid_price: 0 };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!isNumber(price_value)) {
 | 
					 | 
				
			||||||
      console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
 | 
					 | 
				
			||||||
      await takeSnapshot(page, this, "price-value-null");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return { result: false, bid_price: 0 };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const bid_price = this.plus_price + Number(price_value);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (bid_price > this.max_price) {
 | 
					 | 
				
			||||||
      console.log(
 | 
					 | 
				
			||||||
        `❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      await takeSnapshot(page, this, "price-bid-more-than");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await outBid(this.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return { result: false, bid_price: 0 };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const response = await pushPrice({
 | 
					 | 
				
			||||||
      bid_id: this.id,
 | 
					 | 
				
			||||||
      price: bid_price,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!response.status) {
 | 
					 | 
				
			||||||
      return { result: false, bid_price: 0 };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.histories = response.data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // RESET first bid
 | 
					 | 
				
			||||||
    if (this.histories.length > 0 && this.first_bid) {
 | 
					 | 
				
			||||||
      this.first_bid = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { result: true, bid_price };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getCloseTime = async () => {
 | 
					  getCloseTime = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      if (!this.page_context) return null;
 | 
					      if (!this.page_context) return null;
 | 
				
			||||||
| 
						 | 
					@ -127,43 +82,61 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
    return { result: false, close_time };
 | 
					    return { result: false, close_time };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleWritePrice(page, bid_price) {
 | 
					  async placeBid() {
 | 
				
			||||||
    await page.type("#price", String(bid_price));
 | 
					    try {
 | 
				
			||||||
    await delay(500);
 | 
					      await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					        document.querySelector("#price").value = "";
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.page_context.type("#price", String(this.max_price));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await delay(5000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const currentValue = await this.page_context.$eval(
 | 
				
			||||||
 | 
					        "#price",
 | 
				
			||||||
 | 
					        (el) => el.value
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (currentValue !== String(this.max_price)) {
 | 
				
			||||||
 | 
					        console.warn(
 | 
				
			||||||
 | 
					          `[${this.id}] Value not match #price: ${currentValue} !== ${this.max_price}`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async placeBid(page) {
 | 
					      await this.page_context.click("#btnSubmit");
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await page.click("#bid-type-standard");
 | 
					 | 
				
			||||||
      await delay(500);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await page.click("#btnSubmit");
 | 
					 | 
				
			||||||
      await delay(1000);
 | 
					      await delay(1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await page.waitForSelector("button", { timeout: 5000 });
 | 
					      await this.page_context.waitForSelector("button", { timeout: 5000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await delay(500);
 | 
					      await delay(500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await page.click("button");
 | 
					      await this.page_context.click("button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await page.waitForNavigation({ timeout: 5000 });
 | 
					      await this.page_context.waitForNavigation({ timeout: 5000 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await takeSnapshot(
 | 
					      await this.page_context.waitForFunction(
 | 
				
			||||||
        page,
 | 
					        () => document.body.innerText.includes("Successfully"),
 | 
				
			||||||
        this,
 | 
					        { timeout: 5000 } // hoặc lâu hơn nếu cần
 | 
				
			||||||
        "bid-success",
 | 
					 | 
				
			||||||
        CONSTANTS.TYPE_IMAGE.SUCCESS
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					      console.log("✅ Found 'Successfully'");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await pushPrice({
 | 
				
			||||||
 | 
					        bid_id: this.id,
 | 
				
			||||||
 | 
					        price: this.max_price,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.handleReturnProductPage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.log(`❌ [${this.id}] Timeout to loading`);
 | 
					      console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`);
 | 
				
			||||||
      await takeSnapshot(page, this, "timeout to loading");
 | 
					 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleReturnProductPage(page) {
 | 
					  async handleReturnProductPage() {
 | 
				
			||||||
    await page.goto(this.url);
 | 
					    await this.page_context.goto(this.url);
 | 
				
			||||||
    await delay(1000);
 | 
					    await delay(1000);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -256,63 +229,146 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getCurrentData = async () => {
 | 
				
			||||||
 | 
					    if (!this.page_context) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Lấy thời gian đóng
 | 
				
			||||||
 | 
					      const close_time = await this.getCloseTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Giá trị reserve price
 | 
				
			||||||
 | 
					      await this.page_context
 | 
				
			||||||
 | 
					        .waitForSelector("#priceValue", { timeout: 5000 })
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const price_value = await this.page_context
 | 
				
			||||||
 | 
					        .$eval("#priceValue", (el) => el.value)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Lot ID
 | 
				
			||||||
 | 
					      await this.page_context
 | 
				
			||||||
 | 
					        .waitForSelector("#lotId", { timeout: 5000 })
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const lot_id = await this.page_context
 | 
				
			||||||
 | 
					        .$eval("#lotId", (el) => el.value)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Tên sản phẩm
 | 
				
			||||||
 | 
					      await this.page_context
 | 
				
			||||||
 | 
					        .waitForSelector(".dls-heading-3.lotPageTitle", { timeout: 5000 })
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const name = await this.page_context
 | 
				
			||||||
 | 
					        .$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Giá hiện tại
 | 
				
			||||||
 | 
					      await this.page_context
 | 
				
			||||||
 | 
					        .waitForSelector(
 | 
				
			||||||
 | 
					          "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
 | 
				
			||||||
 | 
					          { timeout: 5000 }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      const current_price_raw = await this.page_context
 | 
				
			||||||
 | 
					        .$eval(
 | 
				
			||||||
 | 
					          "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
 | 
				
			||||||
 | 
					          (el) => el.innerText
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const current_price = current_price_raw
 | 
				
			||||||
 | 
					        ? extractNumber(current_price_raw)
 | 
				
			||||||
 | 
					        : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return removeFalsyValues(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          lot_id,
 | 
				
			||||||
 | 
					          reserve_price: Number(price_value) || 0,
 | 
				
			||||||
 | 
					          close_time: close_time ? String(close_time) : null,
 | 
				
			||||||
 | 
					          name,
 | 
				
			||||||
 | 
					          current_price,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ["close_time"]
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`🚨 Error fetching current product data: ${error.message}`);
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handlePlaceBid() {
 | 
				
			||||||
 | 
					    if (!this.page_context) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `⚠️ [${this.id}] No page context found, aborting bid process.`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (global[`IS_PLACE_BID-${this.id}`]) {
 | 
				
			||||||
 | 
					      console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      console.log(`🔄 [${this.id}] Starting bid process...`);
 | 
				
			||||||
 | 
					      global[`IS_PLACE_BID-${this.id}`] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Tắt clearLazyTab vì web này phải navigate để bid
 | 
				
			||||||
 | 
					      global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isCloseProduct = await this.isCloseProduct();
 | 
				
			||||||
 | 
					      if (isCloseProduct.result) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `⚠️ [${this.id}] Outbid detected, calling outBid function.`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        await outBid(this.id);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
 | 
				
			||||||
 | 
					      if (this.current_price > this.max_price + this.plus_price) {
 | 
				
			||||||
 | 
					        console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
 | 
				
			||||||
 | 
					        return; // Dừng hàm nếu giá đã vượt qua giới hạn
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Kiểm tra thời gian bid
 | 
				
			||||||
 | 
					      if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
 | 
				
			||||||
 | 
					            this.name || "None"
 | 
				
			||||||
 | 
					          }`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.histories.length > 0) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `[${this.id}] Already biding with price ${this.histories[0].price}`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const result = await this.placeBid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					      global[`IS_PLACE_BID-${this.id}`] = false;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      console.log(`🔚 [${this.id}] Resetting bid flag.`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  action = async () => {
 | 
					  action = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const page = this.page_context;
 | 
					      const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
 | 
				
			||||||
 | 
					      if (!page.url() || !page.url().includes(this.url)) {
 | 
				
			||||||
 | 
					        console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
 | 
				
			||||||
        await this.gotoLink();
 | 
					        await this.gotoLink();
 | 
				
			||||||
      console.log(`🌍 [${this.id}] Navigated to link.`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await delay(1000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const { close_time, ...isCloseProduct } = await this.isCloseProduct();
 | 
					 | 
				
			||||||
      if (isCloseProduct.result) {
 | 
					 | 
				
			||||||
        console.log(
 | 
					 | 
				
			||||||
          `❌ [${this.id}] The product is closed, cannot place a bid.`
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await delay(500);
 | 
					      await this.handlePlaceBid();
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const { price_value } = await this.update();
 | 
					 | 
				
			||||||
      if (!price_value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const { result, bid_price } = await this.validate({ page, price_value });
 | 
					 | 
				
			||||||
      if (!result) {
 | 
					 | 
				
			||||||
        console.log(
 | 
					 | 
				
			||||||
          `❌ [${this.id}] Validation failed. Unable to proceed with bidding.`
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
					 | 
				
			||||||
      if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
 | 
					 | 
				
			||||||
        console.log(
 | 
					 | 
				
			||||||
          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (price_value != bid_price) {
 | 
					 | 
				
			||||||
        console.log(
 | 
					 | 
				
			||||||
          `✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        await this.handleWritePrice(page, bid_price);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      console.log(`🚀 [${this.id}] Placing the bid...`);
 | 
					 | 
				
			||||||
      const resultPlaceBid = await this.placeBid(page);
 | 
					 | 
				
			||||||
      if (!resultPlaceBid) {
 | 
					 | 
				
			||||||
        console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
 | 
					 | 
				
			||||||
        await takeSnapshot(page, this, "place-bid-action");
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      console.log(
 | 
					 | 
				
			||||||
        `✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      await this.handleReturnProductPage(page);
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error(
 | 
					      console.error(
 | 
				
			||||||
        `🚨 [${this.id}] Error navigating the page: ${error.message}`
 | 
					        `🚨 [${this.id}] Error navigating the page: ${error.message}`
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue