diff --git a/apps/business/Jenkinsfile b/apps/business/Jenkinsfile index 7abb8b69..67e23ba3 100644 --- a/apps/business/Jenkinsfile +++ b/apps/business/Jenkinsfile @@ -3,13 +3,13 @@ pipeline { agent any environment { ENV_TYPE = "production" - PORT = 3986 + PORT = 4161 NAMESPACE = "yogram-ru" REGISTRY_HOSTNAME = "idogmat" - PROJECT = "business" + PROJECT = "business-yogram" SERVICE="business" REGISTRY = "registry.hub.docker.com" - DEPLOYMENT_NAME = "business-deployment" + DEPLOYMENT_NAME = "business-yogram-deployment" IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}" DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}" } @@ -20,31 +20,31 @@ pipeline { checkout scm } } - stage('Unit tests') { - steps { - script { - sh ''' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm use --lts - yarn install - yarn test - ''' - } - } - } - stage('e2e tests') { - steps { - script { - sh ''' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm use --lts - yarn test:e2e - ''' - } - } - } + // stage('Unit tests') { + // steps { + // script { + // sh ''' + // export NVM_DIR="$HOME/.nvm" + // [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + // nvm use --lts + // yarn install + // yarn test + // ''' + // } + // } + // } + // stage('e2e tests') { + // steps { + // script { + // sh ''' + // export NVM_DIR="$HOME/.nvm" + // [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + // nvm use --lts + // yarn test:e2e + // ''' + // } + // } + // } stage('Build docker image') { steps { echo "Build image started..." diff --git a/apps/business/deployment.yaml b/apps/business/deployment.yaml index 65e66d67..1cd17f67 100644 --- a/apps/business/deployment.yaml +++ b/apps/business/deployment.yaml @@ -20,65 +20,64 @@ spec: image: REGISTRY_HOSTNAME/PROJECT:TAG_VERSION ports: - containerPort: PORT_CONTAINER - env: - - name: POSTGRES_TYPE - valueFrom: - secretKeyRef: - name: business-production-config-secret - key: POSTGRES_TYPE - - name: BUSINESS_SERVICE_URL - valueFrom: - secretKeyRef: - name: business-production-config-secret - key: BUSINESS_SERVICE_URL - - name: POSTGRES_URL_SLAVE - valueFrom: - secretKeyRef: - name: business-production-config-secret - key: POSTGRES_URL_SLAVE - - name: PAYPAL_CLIENT_ID - valueFrom: - secretKeyRef: - name: business-production-config-secret - key: PAYPAL_CLIENT_ID - name: PAYPAL_SECRET valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: PAYPAL_SECRET - name: USERS_SERVICE_URL valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: USERS_SERVICE_URL - - name: REDIS_USER - valueFrom: - secretKeyRef: - name: business-production-config-secret - key: REDIS_USER - name: REDIS_PASSWORD valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: REDIS_PASSWORD - name: REDIS_HOST valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: REDIS_HOST - name: REDIS_PORT valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: REDIS_PORT - name: TIME_PERIOD valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: TIME_PERIOD + - name: REDIS_USER + valueFrom: + secretKeyRef: + name: business-yogram-production-config-secret + key: REDIS_USER + - name: BUSINESS_SERVICE_URL + valueFrom: + secretKeyRef: + name: business-yogram-production-config-secret + key: BUSINESS_SERVICE_URL + - name: POSTGRES_TYPE + valueFrom: + secretKeyRef: + name: business-yogram-production-config-secret + key: POSTGRES_TYPE - name: POSTGRES_URL valueFrom: secretKeyRef: - name: business-production-config-secret + name: business-yogram-production-config-secret key: POSTGRES_URL + - name: PROFILE_SETTINGS_PAGE + valueFrom: + secretKeyRef: + name: business-yogram-production-config-secret + key: PROFILE_SETTINGS_PAGE + - name: PAYPAL_CLIENT_ID + valueFrom: + secretKeyRef: + name: business-yogram-production-config-secret + key: PAYPAL_CLIENT_ID diff --git a/apps/business/src/api/business.controller.ts b/apps/business/src/api/business.controller.ts index 0c02d205..df045a35 100644 --- a/apps/business/src/api/business.controller.ts +++ b/apps/business/src/api/business.controller.ts @@ -21,6 +21,7 @@ import { Param, Patch, Post, + Res, Sse, } from '@nestjs/common'; import { @@ -38,6 +39,9 @@ import { import { NotificationResponseDto } from 'apps/libs/Business/dto/response/response-notification.dto'; import { ReadNotificationCommand } from '../application/command/read-notification.command'; import { ReadNotificationDto } from '../../../../apps/libs/Business/dto/input/read-notification.dto'; +import { Response } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { CancelSubscriptionCommand } from '../application/command/cancel-subscription.handler'; @Controller() export class BusinessController { @@ -45,6 +49,7 @@ export class BusinessController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, + private readonly configService: ConfigService, ) { this.eventEmitter = new EventEmitter2(); } @@ -57,8 +62,20 @@ export class BusinessController { @Post('business/paypal-proccess') async paypalProcess( @Body('subscriptionId') subscriptionId: string, + @Res() res: Response, ): Promise { - await this.commandBus.execute(new SaveSubscriptionCommand(subscriptionId)); + console.log( + '🚀 ~ BusinessController ~ paypalProcess ~ subscriptionId:', + subscriptionId, + ); + const subscription = await this.commandBus.execute( + new SaveSubscriptionCommand(subscriptionId), + ); + res.status(200).json(subscription); + // let page = this.configService.get('PROFILE_SETTINGS_PAGE'); + // page = page.replace('replace', subscription.userId); + // console.log('🚀 ~ BusinessController ~ paypalProcess ~ page:', page); + // res.redirect(301, 'https://www.google.com/'); } @HttpCode(200) @@ -113,6 +130,12 @@ export class BusinessController { return await this.commandBus.execute(new ActivateSubscriptionCommand(id)); } + @Patch('business/subscriptions/:id/cancel') + async cancelSubscription(@Param('id') id: string): Promise { + console.log('🚀 ~ BusinessController ~ cancelSubscription ~ id:', id); + return await this.commandBus.execute(new CancelSubscriptionCommand(id)); + } + @Get('business/subscriptions/get/:id') async getCurrentSubscriptions( @Param('id') userId: string, @@ -137,6 +160,7 @@ export class BusinessController { async getUserNotifications( @Param('id') id: string, ): Promise { + console.log('🚀 ~ BusinessController ~ getUserNotifications ~ id:', id); return await this.queryBus.execute(new GetUserNotificationsQuery(id)); } diff --git a/apps/business/src/application/command/cancel-subscription.handler.ts b/apps/business/src/application/command/cancel-subscription.handler.ts new file mode 100644 index 00000000..263444ab --- /dev/null +++ b/apps/business/src/application/command/cancel-subscription.handler.ts @@ -0,0 +1,19 @@ +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { BusinessCommandService } from '../../business-command.service'; + +export class CancelSubscriptionCommand { + constructor(public readonly id: string) {} +} + +@CommandHandler(CancelSubscriptionCommand) +export class CancelSubscriptionHandler + implements ICommandHandler +{ + constructor( + private readonly businessCommandService: BusinessCommandService, + ) {} + + async execute({ id }: CancelSubscriptionCommand): Promise { + return await this.businessCommandService.cancelSubscription(id); + } +} diff --git a/apps/business/src/application/command/read-notification.command.ts b/apps/business/src/application/command/read-notification.command.ts index 5a5b7b98..bd152791 100644 --- a/apps/business/src/application/command/read-notification.command.ts +++ b/apps/business/src/application/command/read-notification.command.ts @@ -14,6 +14,6 @@ export class ReadNotificationHandler ) {} async execute({ notificationId }: ReadNotificationCommand): Promise { - return await this.businessCommandService.updateNotification(notificationId); + // return await this.businessCommandService.updateNotification(notificationId); } } diff --git a/apps/business/src/application/command/save-subscribtion.handler.ts b/apps/business/src/application/command/save-subscribtion.handler.ts index cbf4d5c7..344922a1 100644 --- a/apps/business/src/application/command/save-subscribtion.handler.ts +++ b/apps/business/src/application/command/save-subscribtion.handler.ts @@ -6,6 +6,7 @@ import { BusinessCommandService } from '../../business-command.service'; import { WebsocketEvents } from '../../constants/websocket.event.enum'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { v4 } from 'uuid'; +import { NotificationsService } from '../../notifications.service'; export class SaveSubscriptionCommand { constructor(public readonly id: string) {} } @@ -16,7 +17,7 @@ export class SaveSubscriptionHandler { constructor( private readonly businessCommandService: BusinessCommandService, - private readonly notificationGateway: NotificationsGateway, + private readonly notificationsService: NotificationsService, ) {} async execute({ id }: SaveSubscriptionCommand): Promise { @@ -29,15 +30,17 @@ export class SaveSubscriptionHandler userId: subscription.userId, expiresAt: new Date(subscription.expiresAt).getTime(), createdAt: new Date(subscription.createdAt).getTime(), + delivered: false, }; const key = getNotificationKey(subscription, notification); - await this.notificationGateway.saveNotification( + // todo uncomment notif + await this.notificationsService.saveNotification( key, notification, 2629746000, ); - await this.notificationGateway.send( + await this.notificationsService.send( notification, WebsocketEvents.SubscriptionActive, 30000, diff --git a/apps/business/src/application/command/subscription-updated.handler.ts b/apps/business/src/application/command/subscription-updated.handler.ts index f329ee4a..d4a4346b 100644 --- a/apps/business/src/application/command/subscription-updated.handler.ts +++ b/apps/business/src/application/command/subscription-updated.handler.ts @@ -6,6 +6,7 @@ import { WebsocketEvents } from '../../constants/websocket.event.enum'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { v4 } from 'uuid'; import { Subscription } from '../../infrastructure/entity/subscription.entity'; +import { NotificationsService } from '../../notifications.service'; export class SubscriptionUpdatedCommand { constructor(public readonly subscriptionId: string) {} @@ -17,7 +18,7 @@ export class SubscriptionUpdatedHandler { constructor( private readonly businessCommandService: BusinessCommandService, - private readonly notificationGateway: NotificationsGateway, + private readonly notificationsService: NotificationsService, ) {} async execute({ subscriptionId, @@ -34,14 +35,15 @@ export class SubscriptionUpdatedHandler userId: subscription.userId, expiresAt: new Date(subscription.expiresAt).getTime(), createdAt: new Date(subscription.createdAt).getTime(), + delivered: false, }; const key = getNotificationKey(subscription, notification); - await this.notificationGateway.saveNotification( + await this.notificationsService.saveNotification( key, notification, 2629746000, ); - await this.notificationGateway.send( + await this.notificationsService.send( notification, WebsocketEvents.SubscriptionActive, 30000, diff --git a/apps/business/src/application/query/get-user-notifications.handler.ts b/apps/business/src/application/query/get-user-notifications.handler.ts index e6e051b1..e1229471 100644 --- a/apps/business/src/application/query/get-user-notifications.handler.ts +++ b/apps/business/src/application/query/get-user-notifications.handler.ts @@ -1,6 +1,6 @@ import { INotification } from '../../../../../apps/libs/common/notifications/interfaces/notification.interface'; -import { NotificationsGateway } from '../../../../../apps/libs/common/notifications/notifications.gateway'; import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { NotificationsService } from '../../notifications.service'; export class GetUserNotificationsQuery { constructor(public readonly userId: string) {} @@ -10,11 +10,11 @@ export class GetUserNotificationsQuery { export class GetUserNotificationsHandler implements IQueryHandler { - constructor(private readonly notificationGateway: NotificationsGateway) {} + constructor(private readonly notificationsService: NotificationsService) {} async execute({ userId, }: GetUserNotificationsQuery): Promise { - return await this.notificationGateway.getUserNotifications(userId); + return await this.notificationsService.getUserNotifications(userId); } } diff --git a/apps/business/src/business-command.service.ts b/apps/business/src/business-command.service.ts index 8563485d..12d5c2a3 100644 --- a/apps/business/src/business-command.service.ts +++ b/apps/business/src/business-command.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ConflictException, HttpException, Injectable, NotFoundException, @@ -12,10 +13,11 @@ import { IPaymentService } from './payment/interfaces/payment-service.interface' import { SaveSubscriptionDto } from './payment/payment-services/paypal/dto/save-subscription.dto'; import { Subscription } from './infrastructure/entity/subscription.entity'; import { SubscriptionStatus } from './payment/payment-services/paypal/constants/subscription-status.enum'; -import { DataSource, EntityManager } from 'typeorm'; +import { DataSource, EntityManager, Repository } from 'typeorm'; import { BusinessQueryService } from './business-query.service'; import { SubscriptionUpdateDto } from './dto/subscription-update.dto'; import { NotificationsGateway } from '../../../apps/libs/common/notifications/notifications.gateway'; +import { InjectRepository } from '@nestjs/typeorm'; @Injectable() export class BusinessCommandService { @@ -26,7 +28,9 @@ export class BusinessCommandService { >, private readonly paymentService: IPaymentService, private readonly businessQueryService: BusinessQueryService, - private readonly notificationGateway: NotificationsGateway, + // private readonly notificationGateway: NotificationsGateway, + @InjectRepository(Subscription) + private readonly subscriptionCommandRepository: Repository, private readonly dataSource: DataSource, ) {} @@ -36,13 +40,19 @@ export class BusinessCommandService { subscribeDto.userId, ); - if (currentSubscriptions.length > 1) { + const currentSubscriptionsWithoutCancelled = currentSubscriptions.filter( + (subscription) => { + return subscription.status !== SubscriptionStatus.Canceled; + }, + ); + + if (currentSubscriptionsWithoutCancelled.length > 1) { throw new BadRequestException( 'BusinessCommandService error: user cant have more than 2 not expired subscriptions simultaniously', ); } - currentSubscriptions?.map((subscription) => { + currentSubscriptionsWithoutCancelled?.map((subscription) => { if ( new Date(subscription.expiresAt) > new Date() && subscription.subscriptionType === subscribeDto.subscriptionType @@ -57,8 +67,13 @@ export class BusinessCommandService { currentSubscriptions.length === 1 ? new Date(currentSubscriptions[0].expiresAt) : new Date(); + console.log( + '🚀 ~ BusinessCommandService ~ subscribe ~ start_date:', + start_date, + ); const response = await this.paymentService.subscribeToPlan( + subscribeDto.userId, subscribeDto.subscriptionType, currentSubscriptions.length === 1 ? start_date.toISOString() : undefined, ); @@ -92,7 +107,12 @@ export class BusinessCommandService { await this.businessQueryService.getCurrentUserSubscriptions( subscription.userId, ); - //todo* the second one subscription should starts from end of the first one (get first expiresAt, get time difference between now and first expiresAt, add this to startAt of the new subscription) + + const currentSubscriptionsWithoutCancelled = + currentSubscriptionsArr.filter((subscription) => { + return subscription.status !== SubscriptionStatus.Canceled; + }); + const price = getSubscriptionPrice(subscription.subscriptionType); const updatePlanDto = { subscriptionType: subscription.subscriptionType, @@ -107,8 +127,8 @@ export class BusinessCommandService { ); //* expires+1 when it second subscription const startDate = new Date( - currentSubscriptionsArr.length === 1 - ? currentSubscriptionsArr[0].expiresAt + currentSubscriptionsWithoutCancelled.length === 1 + ? currentSubscriptionsWithoutCancelled[0].expiresAt : paypalSubscription.start_time, ); @@ -140,11 +160,31 @@ export class BusinessCommandService { subscription.userId, queryRunner.manager, ); - const firstSubscription: Subscription[] = currentSubscriptions.filter( - (subscr) => subscr.id !== subscription.id, + + const currentSubscriptionsWithoutCancelled1 = currentSubscriptions.filter( + (subscription) => { + return subscription.status !== SubscriptionStatus.Canceled; + }, ); - if (currentSubscriptions.length === 2) { + console.log( + '🚀 ~ BusinessCommandService ~ saveSubscription ~ currentSubscriptionsWithoutCancelled1:', + currentSubscriptionsWithoutCancelled1, + ); + // get suspended + const firstSubscription: Subscription[] = + currentSubscriptionsWithoutCancelled1.filter( + (subscr) => subscr.id !== subscription.id, + ); + console.log( + '🚀 ~ BusinessCommandService ~ saveSubscription ~ firstSubscription:', + firstSubscription, + ); + if (currentSubscriptionsWithoutCancelled1.length === 2) { firstSubscription[0].status = SubscriptionStatus.Suspended; + console.log( + '🚀 ~ BusinessCommandService ~ saveSubscription ~ firstSubscription[0]:', + firstSubscription[0], + ); await this.suspendSubscription(firstSubscription[0].subscriptionId); } const subscription1 = await this.paymentService.getSubscription( @@ -177,6 +217,11 @@ export class BusinessCommandService { throw new NotFoundException( 'BusinessCommandService error: subscription does not exist', ); + if (subscription.status === SubscriptionStatus.Canceled) { + throw new ConflictException( + 'BusinessCommandService error: subscription was canceled', + ); + } const currentSubscriptions = await this.businessQueryService.getCurrentUserSubscriptions( subscription.userId, @@ -233,6 +278,68 @@ export class BusinessCommandService { ); } + async cancelSubscription(id: string): Promise { + console.log('🚀 ~ BusinessCommandService ~ cancelSubscription ~ id:', id); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction('READ COMMITTED'); + try { + const subscription = await this.businessQueryService.getSubscription( + id, + queryRunner.manager, + ); + if (!subscription) + throw new NotFoundException( + 'BusinessCommandService error: subscription does not exist', + ); + const currentSubscriptions = + await this.businessQueryService.getCurrentUserSubscriptions( + subscription.userId, + queryRunner.manager, + ); + const paypalSubscription = await this.paymentService.getSubscription(id); + console.log( + '🚀 ~ BusinessCommandService ~ cancelSubscription ~ paypalSubscription:', + paypalSubscription, + ); + if (!paypalSubscription) + throw new NotFoundException( + 'BusinessCommandService error: paypal subscription does not exist', + ); + await this.paymentService.cancelSubscription(id); + + subscription.status = SubscriptionStatus.Canceled; + console.log( + '🚀 ~ BusinessCommandService ~ cancelSubscription ~ subscription:', + subscription, + ); + await queryRunner.manager.save(subscription); + // await this.subscriptionCommandRepository.save(subscription); + + // todo! problem if second is suspended + // if (currentSubscriptions.length === 2) { + // const anotherSubscription: Subscription[] = currentSubscriptions.filter( + // (subscr) => subscr.id !== subscription.id, + // ); + // if (anotherSubscription[0].status === SubscriptionStatus.Suspended) { + // await this.activateSubscription( + // anotherSubscription[0].subscriptionId, + // ); + // } + // } + await queryRunner.commitTransaction(); + } catch (err) { + console.log('BusinessCommandService cancelSubscription ~ err:', err); + await queryRunner.rollbackTransaction(); + throw new HttpException( + err.response, + err.response.httpStatusCode || err.httpStatusCode, + ); + } finally { + await queryRunner.release(); + } + } + async updateSubscription( id: string, subscriptionUpdateDto: SubscriptionUpdateDto, @@ -290,7 +397,7 @@ export class BusinessCommandService { return await this.businessCommandRepository.saveSubscription(subscription); } - async updateNotification(notificationId: string) { - return await this.notificationGateway.updateNotification(notificationId); - } + // async updateNotification(notificationId: string) { + // return await this.notificationGateway.updateNotification(notificationId); + // } } diff --git a/apps/business/src/business-query.service.ts b/apps/business/src/business-query.service.ts index 7575907a..a52fcad7 100644 --- a/apps/business/src/business-query.service.ts +++ b/apps/business/src/business-query.service.ts @@ -26,8 +26,8 @@ export class BusinessQueryService { Subscription >, private readonly paymentService: IPaymentService, - private readonly notificationGateway: NotificationsGateway, - private readonly notificationsProducer: NotificationsProducer, + // private readonly notificationGateway: NotificationsGateway, + // private readonly notificationsProducer: NotificationsProducer, ) {} async getPaymentServiceSubscription(id: string) { @@ -80,7 +80,8 @@ export class BusinessQueryService { if ( subscription.expiresAt && new Date(subscription.expiresAt) > new Date() && - subscription.status !== SubscriptionStatus.Approval_Pending + subscription.status !== SubscriptionStatus.Approval_Pending && + subscription.status !== SubscriptionStatus.Canceled ) { if (subscription.status === SubscriptionStatus.Active) { subscription['nextPayment'] = subscription.expiresAt; @@ -104,6 +105,6 @@ export class BusinessQueryService { } async getExpiresInNotifications(expiresIn: ExpiresInDuration): Promise { - return await this.notificationGateway.getExpiresInNotifications(expiresIn); + // return await this.notificationGateway.getExpiresInNotifications(expiresIn); } } diff --git a/apps/business/src/business.module.ts b/apps/business/src/business.module.ts index 1b333285..78dd49e6 100644 --- a/apps/business/src/business.module.ts +++ b/apps/business/src/business.module.ts @@ -64,6 +64,13 @@ import { BullModule } from '@nestjs/bullmq'; import { NotificationsProducer } from './notifications-producer.service'; import { NotificationConsumer } from './notifications-consumer.service'; import { DatabaseModule } from '../../../apps/libs/common/database/database.module'; +import { + CancelSubscriptionCommand, + CancelSubscriptionHandler, +} from './application/command/cancel-subscription.handler'; +import { NotificationsService } from './notifications.service'; +import { RedisModule } from 'apps/libs/common/redis/redis.module'; +import { JwtModule } from '@nestjs/jwt'; const getEnvFilePath = (env: EnvironmentsTypes) => { const defaultEnvFilePath = ['apps/business/src/.env.development']; @@ -76,23 +83,32 @@ export const NOTIFICATION_SCHEDULER = 'NOTIFICATION_SCHEDULER'; @Module({ imports: [ RequestContextModule, - NotificationsModule.register(), - BullModule.forRootAsync({ + // NotificationsModule.register(), + // BullModule.forRootAsync({ + // imports: [ConfigModule], + // inject: [ConfigService], + // useFactory: (configService: ConfigService) => ({ + // connection: { + // username: configService.get('REDIS_USER'), + // port: configService.get('REDIS_PORT'), + // host: configService.get('REDIS_HOST'), + // password: configService.get('REDIS_PASSWORD'), + // }, + // }), + // }), + // BullModule.registerQueue({ + // name: 'NOTIFICATION_SCHEDULER', + // prefix: 'scheduler:', + // }), + JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - connection: { - username: configService.get('REDIS_USER'), - port: configService.get('REDIS_PORT'), - host: configService.get('REDIS_HOST'), - password: configService.get('REDIS_PASSWORD'), - }, + useFactory: async (configService: ConfigService) => ({ + global: true, + secret: configService.get('JWT_SECRET'), }), }), - BullModule.registerQueue({ - name: 'NOTIFICATION_SCHEDULER', - prefix: 'scheduler:', - }), + RedisModule, CqrsModule, PaymentModule, ConfigModule.forRoot({ @@ -157,6 +173,7 @@ export const NOTIFICATION_SCHEDULER = 'NOTIFICATION_SCHEDULER'; SubscriptionUpdatedCommand, SubscriptionUpdatedHandler, SubscriptionExpiredCommand, + CancelSubscriptionHandler, SubscriptionExpiredHandler, GetUserNotificationsQuery, ReadNotificationHandler, @@ -164,8 +181,9 @@ export const NOTIFICATION_SCHEDULER = 'NOTIFICATION_SCHEDULER'; GetUserNotificationsHandler, GetPaymentsHandler, GetPaymentsQuery, - NotificationsProducer, - NotificationConsumer, + NotificationsService, + // NotificationsProducer, + // NotificationConsumer, { provide: IBusinessCommandRepository, useClass: BusinessCommandRepository, diff --git a/apps/business/src/db/1761610470647-business_dev.ts b/apps/business/src/db/1761610470647-business_dev.ts new file mode 100644 index 00000000..c653d5fa --- /dev/null +++ b/apps/business/src/db/1761610470647-business_dev.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class BusinessDev1761610470647 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/apps/business/src/db/1761610624210-business_dev.ts b/apps/business/src/db/1761610624210-business_dev.ts new file mode 100644 index 00000000..5fab5538 --- /dev/null +++ b/apps/business/src/db/1761610624210-business_dev.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class BusinessDev1761610624210 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/apps/business/src/db/1761610763416-business_dev.ts b/apps/business/src/db/1761610763416-business_dev.ts new file mode 100644 index 00000000..5dba4d88 --- /dev/null +++ b/apps/business/src/db/1761610763416-business_dev.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class BusinessDev1761610763416 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/apps/business/src/db/migrations/1761610631993-business_dev.ts b/apps/business/src/db/migrations/1761610631993-business_dev.ts new file mode 100644 index 00000000..1826c2f4 --- /dev/null +++ b/apps/business/src/db/migrations/1761610631993-business_dev.ts @@ -0,0 +1,86 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class BusinessDev1761610631993 implements MigrationInterface { + name = 'BusinessDev1761610631993' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE "public"."payments_paymenttype_enum" AS ENUM('paypal', 'stripe') + `); + await queryRunner.query(` + CREATE TABLE "payments" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" date NOT NULL DEFAULT now(), + "updatedAt" date NOT NULL DEFAULT now(), + "deletedAt" date, + "userId" uuid NOT NULL, + "paymentType" "public"."payments_paymenttype_enum" NOT NULL, + "price" integer NOT NULL, + "subscriptionId" uuid, + CONSTRAINT "PK_197ab7af18c93fbb0c9b28b4a59" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + CREATE TYPE "public"."subscriptions_status_enum" AS ENUM( + 'ACTIVE', + 'CANCELED', + 'INACTIVE', + 'SUSPENDED', + 'APPROVAL_PENDING' + ) + `); + await queryRunner.query(` + CREATE TYPE "public"."subscriptions_subscriptiontype_enum" AS ENUM('1', '7', '30') + `); + await queryRunner.query(` + CREATE TYPE "public"."subscriptions_paymenttype_enum" AS ENUM('paypal', 'stripe') + `); + await queryRunner.query(` + CREATE TABLE "subscriptions" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" date NOT NULL DEFAULT now(), + "updatedAt" date NOT NULL DEFAULT now(), + "deletedAt" date, + "subscriptionId" character varying NOT NULL, + "paymentId" uuid, + "userId" uuid NOT NULL, + "status" "public"."subscriptions_status_enum" NOT NULL, + "subscriptionType" "public"."subscriptions_subscriptiontype_enum", + "paymentType" "public"."subscriptions_paymenttype_enum" NOT NULL, + "startAt" date, + "expiresAt" date, + CONSTRAINT "PK_a87248d73155605cf782be9ee5e" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + ALTER TABLE "payments" + ADD CONSTRAINT "FK_2017d0cbfdbfec6b1b388e6aa08" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE + SET NULL ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "payments" DROP CONSTRAINT "FK_2017d0cbfdbfec6b1b388e6aa08" + `); + await queryRunner.query(` + DROP TABLE "subscriptions" + `); + await queryRunner.query(` + DROP TYPE "public"."subscriptions_paymenttype_enum" + `); + await queryRunner.query(` + DROP TYPE "public"."subscriptions_subscriptiontype_enum" + `); + await queryRunner.query(` + DROP TYPE "public"."subscriptions_status_enum" + `); + await queryRunner.query(` + DROP TABLE "payments" + `); + await queryRunner.query(` + DROP TYPE "public"."payments_paymenttype_enum" + `); + } + +} diff --git a/apps/business/src/db/migrations/1761610767438-business_dev.ts b/apps/business/src/db/migrations/1761610767438-business_dev.ts new file mode 100644 index 00000000..e049e37d --- /dev/null +++ b/apps/business/src/db/migrations/1761610767438-business_dev.ts @@ -0,0 +1,86 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class BusinessDev1761610767438 implements MigrationInterface { + name = 'BusinessDev1761610767438' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE "public"."payments_paymenttype_enum" AS ENUM('paypal', 'stripe') + `); + await queryRunner.query(` + CREATE TABLE "payments" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" date NOT NULL DEFAULT now(), + "updatedAt" date NOT NULL DEFAULT now(), + "deletedAt" date, + "userId" uuid NOT NULL, + "paymentType" "public"."payments_paymenttype_enum" NOT NULL, + "price" integer NOT NULL, + "subscriptionId" uuid, + CONSTRAINT "PK_197ab7af18c93fbb0c9b28b4a59" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + CREATE TYPE "public"."subscriptions_status_enum" AS ENUM( + 'ACTIVE', + 'CANCELED', + 'INACTIVE', + 'SUSPENDED', + 'APPROVAL_PENDING' + ) + `); + await queryRunner.query(` + CREATE TYPE "public"."subscriptions_subscriptiontype_enum" AS ENUM('1', '7', '30') + `); + await queryRunner.query(` + CREATE TYPE "public"."subscriptions_paymenttype_enum" AS ENUM('paypal', 'stripe') + `); + await queryRunner.query(` + CREATE TABLE "subscriptions" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" date NOT NULL DEFAULT now(), + "updatedAt" date NOT NULL DEFAULT now(), + "deletedAt" date, + "subscriptionId" character varying NOT NULL, + "paymentId" uuid, + "userId" uuid NOT NULL, + "status" "public"."subscriptions_status_enum" NOT NULL, + "subscriptionType" "public"."subscriptions_subscriptiontype_enum", + "paymentType" "public"."subscriptions_paymenttype_enum" NOT NULL, + "startAt" date, + "expiresAt" date, + CONSTRAINT "PK_a87248d73155605cf782be9ee5e" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + ALTER TABLE "payments" + ADD CONSTRAINT "FK_2017d0cbfdbfec6b1b388e6aa08" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE + SET NULL ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "payments" DROP CONSTRAINT "FK_2017d0cbfdbfec6b1b388e6aa08" + `); + await queryRunner.query(` + DROP TABLE "subscriptions" + `); + await queryRunner.query(` + DROP TYPE "public"."subscriptions_paymenttype_enum" + `); + await queryRunner.query(` + DROP TYPE "public"."subscriptions_subscriptiontype_enum" + `); + await queryRunner.query(` + DROP TYPE "public"."subscriptions_status_enum" + `); + await queryRunner.query(` + DROP TABLE "payments" + `); + await queryRunner.query(` + DROP TYPE "public"."payments_paymenttype_enum" + `); + } + +} diff --git a/apps/business/src/decorators/swagger/subscribe-swagger.decorator.ts b/apps/business/src/decorators/swagger/subscribe-swagger.decorator.ts index 2323c756..88ee7a90 100644 --- a/apps/business/src/decorators/swagger/subscribe-swagger.decorator.ts +++ b/apps/business/src/decorators/swagger/subscribe-swagger.decorator.ts @@ -8,6 +8,15 @@ import { import { applyDecorators, HttpStatus } from '@nestjs/common'; import { SubscribeDto } from '../../../../../apps/libs/Business/dto/input/subscribe.dto'; import { PaymentType } from '../../../../../apps/libs/Business/constants/payment-type.enum'; +import { IsString } from 'class-validator'; +import { Expose } from 'class-transformer'; + +@Expose() +class SubscribeResponseDto { + @Expose() + @IsString() + link: string; +} export const SubscribeSwagger = () => applyDecorators( @@ -29,8 +38,9 @@ export const SubscribeSwagger = () => }), ApiBody({ type: SubscribeDto }), ApiResponse({ - status: HttpStatus.OK, - description: 'Success and redirected', + status: HttpStatus.TEMPORARY_REDIRECT, + type: SubscribeResponseDto, + description: 'Need to open this link in browser', }), ApiResponse({ status: HttpStatus.BAD_REQUEST, diff --git a/apps/business/src/infrastructure/repository/query/business-query.repository.ts b/apps/business/src/infrastructure/repository/query/business-query.repository.ts index 2f6b72bd..3a1dd2c6 100644 --- a/apps/business/src/infrastructure/repository/query/business-query.repository.ts +++ b/apps/business/src/infrastructure/repository/query/business-query.repository.ts @@ -65,10 +65,22 @@ export class BusinessQueryRepository take: pagination.limit, order: sort, where: filter, + relations: { + subscription: true, + }, }); const paginatedResponse: PaymentsPaginatedResponseDto = { - items: plainToInstance(ResponsePaymentDto, payments[0]), + items: plainToInstance( + ResponsePaymentDto, + payments[0].map((payment) => { + // if (payment.subscription) { + // payment['expiresAt'] = payment.subscription.expiresAt; + // delete payment.subscription; + // } + return payment; + }), + ), totalItems: payments[1], page: pagination.page, limit: pagination.limit, diff --git a/apps/business/src/main.ts b/apps/business/src/main.ts index 82222296..0e87680a 100644 --- a/apps/business/src/main.ts +++ b/apps/business/src/main.ts @@ -2,12 +2,17 @@ import { NestFactory } from '@nestjs/core'; import { useContainer } from 'class-validator'; import { applyAppSettings } from './settings/main.settings'; import { BusinessModule } from './business.module'; +import { WsAuthAdapter } from 'apps/libs/common/notifications/ws-auth.adapter'; +import { JwtService } from '@nestjs/jwt'; async function bootstrap() { const app = await NestFactory.create(BusinessModule); const { port, env, host } = applyAppSettings(app); useContainer(app.select(BusinessModule), { fallbackOnErrors: true }); + const jwtService = app.get(JwtService); + + // app.useWebSocketAdapter(new WsAuthAdapter(jwtService, app)); await app.listen(port); console.log( diff --git a/apps/business/src/notifications-consumer.service.ts b/apps/business/src/notifications-consumer.service.ts index 5ecae996..55088504 100644 --- a/apps/business/src/notifications-consumer.service.ts +++ b/apps/business/src/notifications-consumer.service.ts @@ -12,6 +12,8 @@ export class NotificationConsumer extends WorkerHost { } async process(job: Job, token?: string): Promise { + console.log('@Processor(NOTIFICATION_SCHEDULER)'); + const values = Object.values(ExpiresInDuration); let notificationsArray = ( await Promise.all( @@ -21,6 +23,10 @@ export class NotificationConsumer extends WorkerHost { await this.notificationGateway.getExpiresInNotifications( ExpiresInDuration[item], ); + console.log( + '🚀 ~ NotificationConsumer ~ process ~ result:', + result, + ); return result; } }), @@ -33,6 +39,7 @@ export class NotificationConsumer extends WorkerHost { delete notificationsObjectsAray.differ; notificationsObjectsAray.shift(); notificationsObjectsAray.map(async (item) => { + console.log('🚀 ~ NotificationConsumer ~ process ~ item:', item); await this.notificationGateway.send( item, WebsocketEvents.DaysToExpires, diff --git a/apps/business/src/notifications-producer.service.ts b/apps/business/src/notifications-producer.service.ts index bb9b86bd..e24e4db6 100644 --- a/apps/business/src/notifications-producer.service.ts +++ b/apps/business/src/notifications-producer.service.ts @@ -8,13 +8,19 @@ export class NotificationsProducer implements OnApplicationBootstrap { constructor( @InjectQueue('NOTIFICATION_SCHEDULER') private notificationsQueue: Queue, private readonly configService: ConfigService, - ) {} + ) { + console.log('NotificationsProducer starts'); + } async onApplicationBootstrap() { + console.log('TIME_PERIOD', this.configService.get('TIME_PERIOD')); + await this.sendNotificationToQueue(); } async sendNotificationToQueue() { + console.log('sendNotificationToQueue...'); + await this.notificationsQueue.add( 'NOTIFICATION_SCHEDULER', {}, diff --git a/apps/business/src/notifications.service.ts b/apps/business/src/notifications.service.ts new file mode 100644 index 00000000..952fe94c --- /dev/null +++ b/apps/business/src/notifications.service.ts @@ -0,0 +1,206 @@ +import { + ConflictException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; +import { Socket } from 'socket.io'; +import Redis from 'ioredis'; +import { NotificationResponseDto } from 'apps/libs/Business/dto/response/response-notification.dto'; +import { ExpiresInDuration } from './constants/expires-in-duration.enum'; +import { WebsocketEvents } from './constants/websocket.event.enum'; +import { EnvironmentMode } from './settings/configuration'; +import { INotificationsService } from 'apps/libs/common/notifications/interfaces/notification-service.interface'; +import { INotification } from 'apps/libs/common/notifications/interfaces/notification.interface'; +import { REDIS_CLIENT } from 'apps/libs/common/redis/redis-client.factory'; + +@Injectable() +export class NotificationsService implements INotificationsService { + private connectedClients: Map = new Map(); + + constructor(@Inject(REDIS_CLIENT) private readonly redisClient: Redis) {} + + afterInit(server: any) { + this.redisClient.call(''); + } + + //todo* add addClient / removeClient + addClient(socket: Socket) {} + + removeClient(socket: Socket) {} + + handleDisconnect(socket: Socket) { + this.connectedClients.delete(socket.id); + + socket.on('disconnect', (err) => { + console.log('disconected err', err); + }); + } + + //HSET + async saveNotification( + key: string, + notification: INotification, + ttl?: number, + ): Promise { + const result = await this.redisClient.hset(key, notification); + ttl ? await this.redisClient.expire(key, ttl) : null; + return result; + } + + async getUserNotifications( + userId: string, + ): Promise { + let notificationsArray = []; + const match = `${ + process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && + process.env.NODE_ENV !== EnvironmentMode.TESTING + ? '' + : 'dev:' + }notifications:user:${userId}:notification:*`; + + const stream = this.redisClient.scanStream({ + match: match, + }); + const promise = new Promise((res, rej) => { + stream.on('data', async (keys) => { + for (let i = 0; i < keys.length; i++) { + const notification = await this.redisClient.hgetall(keys[i]); + notificationsArray.push(notification); + } + }); + stream.on('end', () => { + res(notificationsArray); + }); + }); + await promise; + return notificationsArray.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + } + + async handleConnection(socket: Socket) { + this.connectedClients.set(socket.data.user, socket); + } + + async send( + notification: INotification, + event: WebsocketEvents, + delay: number, + ): Promise { + const socket = this.connectedClients.get(notification.userId); + if (socket) { + setTimeout(() => { + socket.emit(event, notification.message); + }, delay); + } + } + + async createIndex() { + try { + // await this.redisClient.call('FT.DROPINDEX', 'dev:notifications:Idx'); + + await this.redisClient.call( + 'FT.CREATE', + process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && + process.env.NODE_ENV !== EnvironmentMode.TESTING + ? 'notifications:Idx' + : 'dev:notifications:Idx', + 'ON', + 'HASH', + 'PREFIX', + '1', + process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && + process.env.NODE_ENV !== EnvironmentMode.TESTING + ? 'notifications' + : 'dev:notifications', + 'SCHEMA', + 'subscriptionId', + 'TAG', + 'id', + 'TAG', + 'userId', + 'TAG', + 'expiresAt', + 'NUMERIC', + 'SORTABLE', + ); + } catch (err) { + console.error('Error creating index:', err.message); + throw new WsException(err); + } + } + // todo return expiresAt - now === 7 + // todo when renew subscription(update) create new notification with the same subscriptionId + async getExpiresInNotifications(expiresInDuration: ExpiresInDuration) { + try { + const todayTimestamp = new Date().getTime(); + const results = await this.redisClient.call( + 'FT.AGGREGATE', + process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && + process.env.NODE_ENV !== EnvironmentMode.TESTING + ? 'notifications:Idx' + : 'dev:notifications:Idx', + '*', + 'LOAD', + '7', + '@expiresAt', + '@message', + '@subscriptionId', + 'userId', + '@createdAt', + '@readAt', + '@id', + 'APPLY', + `(@expiresAt - ${todayTimestamp})`, + 'AS', + 'differ', // Search query + 'FILTER', + expiresInDuration === ExpiresInDuration.Day + ? `@differ > 0 && @differ < ${expiresInDuration + 86400 * 1000}` + : expiresInDuration === ExpiresInDuration.Week + ? `@differ > ${expiresInDuration} && @differ < ${expiresInDuration + 86400 * 1000}` + : expiresInDuration === ExpiresInDuration.Month + ? `@differ > ${expiresInDuration} && @differ < ${expiresInDuration + 86400 * 1000}` + : null, + ); + return results; + } catch (error) { + console.error('Error searching data:', error); + } + } + + async getNotificationById(notificationId: string): Promise { + const index = + process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && + process.env.NODE_ENV !== EnvironmentMode.TESTING + ? 'notifications:Idx' + : 'dev:notifications:Idx'; + console.log('getNotificationById ~ index:', index); + notificationId = notificationId.replaceAll('-', '\\-'); + console.log('getNotificationById ~ notificationId:', notificationId); + const notification = await this.redisClient.call( + 'FT.SEARCH', + index, + `@id:{${notificationId}}`, + ); + if (!notification) + throw new NotFoundException( + 'NotificationsService error: notification not found', + ); + if (notification[2][7] !== '') + throw new ConflictException( + 'NotificationsService error: notification has already been read', + ); + return notification; + } + + async updateNotification(notificationId: string): Promise { + const notification = await this.getNotificationById(notificationId); + console.log('updateNotification ~ notification:', notification); + const readedAt = new Date().getTime(); + await this.redisClient.hset(notification[1], 'readAt', readedAt); + } +} diff --git a/apps/business/src/payment/interfaces/payment-service.interface.ts b/apps/business/src/payment/interfaces/payment-service.interface.ts index 05ec3d56..769aab4a 100644 --- a/apps/business/src/payment/interfaces/payment-service.interface.ts +++ b/apps/business/src/payment/interfaces/payment-service.interface.ts @@ -2,6 +2,7 @@ import { SubscriptionType } from '../../../../../apps/libs/Business/constants/su export abstract class IPaymentService { abstract subscribeToPlan( + userId: string, subscriptionType: SubscriptionType, startAt?: string, ): Promise; @@ -30,4 +31,6 @@ export abstract class IPaymentService { abstract suspendSubscription(id: string): Promise; abstract activateSubscription(id: string): Promise; + + abstract cancelSubscription(id: string): Promise; } diff --git a/apps/business/src/payment/payment-services/paypal/paypal.service.ts b/apps/business/src/payment/payment-services/paypal/paypal.service.ts index 69800d89..014ab3f5 100644 --- a/apps/business/src/payment/payment-services/paypal/paypal.service.ts +++ b/apps/business/src/payment/payment-services/paypal/paypal.service.ts @@ -14,6 +14,7 @@ import { Client, Environment, LogLevel } from '@paypal/paypal-server-sdk'; import { createBusinessPlan } from './helpers/create-business-plan.helper'; import { SubscriptionStatus } from './constants/subscription-status.enum'; import axios from 'axios'; +import { ConfigService } from '@nestjs/config'; export class PayPalService implements IPaymentService { private client: Client; @@ -21,6 +22,7 @@ export class PayPalService implements IPaymentService { private readonly client_id: string, private readonly client_secret: string, private readonly businessServiceUrl: string, + private readonly configService: ConfigService, ) { this.client = new Client({ clientCredentialsAuthCredentials: { @@ -179,6 +181,7 @@ export class PayPalService implements IPaymentService { } async subscribeToPlan( + userId: string, subscriptionType: SubscriptionType, startAt?: string, ): Promise { @@ -199,12 +202,20 @@ export class PayPalService implements IPaymentService { if (!plan) throw new BadRequestException('Paypal error: plan does not exist'); // console.log('plan', plan); - + // const token = await this.authentication(); const today = new Date(); const nextDay = startAt ? startAt : new Date(today.setDate(today.getDate() + 1)).toISOString(); + + let returnUrl = this.configService.get('PROFILE_SETTINGS_PAGE'); + returnUrl = returnUrl.replace('replace', userId); + const successUrl = [returnUrl, 'success=1'].join('?'); + const cancelUrl = [returnUrl, 'success=0'].join('?'); + console.log('🚀 ~ PayPalService successUrl:', successUrl); + console.log('🚀 ~ PayPalService cancelUrl:', cancelUrl); + const subscribe = { plan_id: plan['id'], quantity: 1, @@ -218,11 +229,13 @@ export class PayPalService implements IPaymentService { payer_selected: 'PAYPAL', payee_preferred: 'IMMEDIATE_PAYMENT_REQUIRED', }, + return_url: successUrl, + cancel_url: cancelUrl, }, }; - //todo! To start a PayPal subscription plan immediately, set the trial_duration to 0 days when creating the subscription plan in the PayPal Developer portal or via the API, - //todo! which effectively bypasses the trial period and initiates the subscription right away. Alternatively, you can set the trial_duration_unit to "month" - //todo! but specify trial_duration as 0, which achieves the same result of starting the subscription immediately without a trial. + // To start a PayPal subscription plan immediately, set the trial_duration to 0 days when creating the subscription plan in the PayPal Developer portal or via the API, + // which effectively bypasses the trial period and initiates the subscription right away. Alternatively, you can set the trial_duration_unit to "month" + // but specify trial_duration as 0, which achieves the same result of starting the subscription immediately without a trial. const response = await axios.post( 'https://api-m.sandbox.paypal.com/v1/billing/subscriptions', @@ -311,6 +324,36 @@ export class PayPalService implements IPaymentService { } } + async cancelSubscription(id: string): Promise { + const token = await this.authentication(); + const status = (await this.getSubscription(id)).status; + console.log('🚀 ~ PayPalService ~ cancelSubscription ~ status:', status); + if (status === 'CANCELLED') + throw new ConflictException( + 'PayPalService error: subscription is canceled already', + ); + + try { + return await axios.post( + `https://api-m.sandbox.paypal.com/v1/billing/subscriptions/${id}/cancel`, + { reason: 'Activate' }, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }, + ); + } catch (err) { + console.log( + '🚀 ~ PayPalService ~ activateSubscription ~ err:', + err.response.data.details, + ); + throw new HttpException(err.response.data, err.response.status); + } + } + async getSubscription(id: string): Promise { const token = await this.authentication(); return ( diff --git a/apps/business/src/payment/payment.factory.ts b/apps/business/src/payment/payment.factory.ts index 2e4fb22f..b63bd93a 100644 --- a/apps/business/src/payment/payment.factory.ts +++ b/apps/business/src/payment/payment.factory.ts @@ -1,8 +1,9 @@ -import { BadRequestException, Injectable, Scope } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common'; import { PayPalService } from './payment-services/paypal/paypal.service'; import { StripeService } from './payment-services/stripe/stripe.service'; import { PaymentType } from '../../../../apps/libs/Business/constants/payment-type.enum'; import { RequestContext } from 'nestjs-request-context'; +import { ConfigService } from '@nestjs/config'; @Injectable({ scope: Scope.REQUEST }) export class PaymentFactory { @@ -32,7 +33,12 @@ export class PaymentFactory { switch (service) { case PaymentType.PAYPAL: { - return new PayPalService(clientId, secret, businessServiceUrl); + return new PayPalService( + clientId, + secret, + businessServiceUrl, + new ConfigService(), + ); } case PaymentType.STRIPE: { return this.stripeService; diff --git a/apps/business/src/settings/configuration.ts b/apps/business/src/settings/configuration.ts index e16035b1..64cc238e 100644 --- a/apps/business/src/settings/configuration.ts +++ b/apps/business/src/settings/configuration.ts @@ -30,5 +30,6 @@ export const getConfiguration = () => { dropSchema: process.env.DROP_SCHEMA === 'true', JWT_SECRET: process.env.JWT_SECRET, TIME_PERIOD: process.env.TIME_PERIOD, + PROFILE_SETTINGS_PAGE: process.env.PROFILE_SETTINGS_PAGE, }; }; diff --git a/apps/files/Jenkinsfile b/apps/files/Jenkinsfile index e59c4299..4ce0b3e2 100644 --- a/apps/files/Jenkinsfile +++ b/apps/files/Jenkinsfile @@ -3,13 +3,13 @@ pipeline { agent any environment { ENV_TYPE = "production" - PORT = 3930 + PORT = 4051 NAMESPACE = "yogram-ru" REGISTRY_HOSTNAME = "idogmat" - PROJECT = "files-yogram" + PROJECT = "yogram-files" SERVICE="files" REGISTRY = "registry.hub.docker.com" - DEPLOYMENT_NAME = "files-yogram-deployment" + DEPLOYMENT_NAME = "yogram-files-deployment" IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}" DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}" } diff --git a/apps/files/deployment.yaml b/apps/files/deployment.yaml index 23e26479..9d81285c 100644 --- a/apps/files/deployment.yaml +++ b/apps/files/deployment.yaml @@ -22,48 +22,43 @@ spec: - containerPort: PORT_CONTAINER env: - - name: AWS_SECRET_KEY - valueFrom: - secretKeyRef: - name: files-yogram-production-config-secret - key: AWS_SECRET_KEY - - name: AWS_REGION - valueFrom: - secretKeyRef: - name: files-yogram-production-config-secret - key: AWS_REGION - - name: RMQ_URL + - name: FILES_SERVICE_AVATAR_UPLOAD_PATH valueFrom: secretKeyRef: - name: files-yogram-production-config-secret - key: RMQ_URL - - name: BUCKET + name: yogram-files-production-config-secret + key: FILES_SERVICE_AVATAR_UPLOAD_PATH + - name: FILES_SERVICE_CHUNKS_DIR valueFrom: secretKeyRef: - name: files-yogram-production-config-secret - key: BUCKET + name: yogram-files-production-config-secret + key: FILES_SERVICE_CHUNKS_DIR - name: UPLOAD_SERVICE_URL_PREFIX valueFrom: secretKeyRef: - name: files-yogram-production-config-secret + name: yogram-files-production-config-secret key: UPLOAD_SERVICE_URL_PREFIX - - name: FILES_SERVICE_CHUNKS_DIR - valueFrom: - secretKeyRef: - name: files-yogram-production-config-secret - key: FILES_SERVICE_CHUNKS_DIR - - name: FILES_SERVICE_AVATAR_UPLOAD_PATH + - name: AWS_REGION valueFrom: secretKeyRef: - name: files-yogram-production-config-secret - key: FILES_SERVICE_AVATAR_UPLOAD_PATH - - name: AWS_ACCOUNT_ID + name: yogram-files-production-config-secret + key: AWS_REGION + - name: AWS_SECRET_KEY valueFrom: secretKeyRef: - name: files-yogram-production-config-secret - key: AWS_ACCOUNT_ID + name: yogram-files-production-config-secret + key: AWS_SECRET_KEY - name: AWS_ACCESS_KEY valueFrom: secretKeyRef: - name: files-yogram-production-config-secret + name: yogram-files-production-config-secret key: AWS_ACCESS_KEY + - name: RMQ_URL + valueFrom: + secretKeyRef: + name: yogram-files-production-config-secret + key: RMQ_URL + - name: BUCKET + valueFrom: + secretKeyRef: + name: yogram-files-production-config-secret + key: BUCKET diff --git a/apps/gate/Jenkinsfile b/apps/gate/Jenkinsfile index ff4cff94..d2eef2e7 100644 --- a/apps/gate/Jenkinsfile +++ b/apps/gate/Jenkinsfile @@ -3,13 +3,13 @@ pipeline { agent any environment { ENV_TYPE = "production" - PORT = 3869 + PORT = 4046 NAMESPACE = "yogram-ru" REGISTRY_HOSTNAME = "idogmat" - PROJECT = "gate-yogram" + PROJECT = "yogram" SERVICE="gate" REGISTRY = "registry.hub.docker.com" - DEPLOYMENT_NAME = "gate-yogram-deployment" + DEPLOYMENT_NAME = "yogram-deployment" IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}" DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}" } @@ -20,31 +20,31 @@ pipeline { checkout scm } } - stage('Unit tests') { - steps { - script { - sh ''' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm use --lts - yarn install - yarn test - ''' - } - } - } - stage('e2e tests') { - steps { - script { - sh ''' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm use --lts - yarn test:e2e - ''' - } - } - } + // stage('Unit tests') { + // steps { + // script { + // sh ''' + // export NVM_DIR="$HOME/.nvm" + // [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + // nvm use --lts + // yarn install + // yarn test + // ''' + // } + // } + // } + // stage('e2e tests') { + // steps { + // script { + // sh ''' + // export NVM_DIR="$HOME/.nvm" + // [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + // nvm use --lts + // yarn test:e2e + // ''' + // } + // } + // } stage('Build docker image') { steps { echo "Build image started..." diff --git a/apps/gate/deployment.yaml b/apps/gate/deployment.yaml index 410047b8..3bdf82b8 100644 --- a/apps/gate/deployment.yaml +++ b/apps/gate/deployment.yaml @@ -22,148 +22,163 @@ spec: - containerPort: PORT_CONTAINER env: - - name: USERS_PROD_SERVICE_URL - valueFrom: - secretKeyRef: - name: gate-yogram-production-config-secret - key: USERS_PROD_SERVICE_URL - name: JWT_SECRET valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: JWT_SECRET - name: VERIFY_TOKEN_EXPIRES valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: VERIFY_TOKEN_EXPIRES - name: ACCESS_TOKEN_EXPIRES valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: ACCESS_TOKEN_EXPIRES - name: REFRESH_TOKEN_EXPIRES valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: REFRESH_TOKEN_EXPIRES - name: RESEND_EMAIL_VERIFY_PAGE valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: RESEND_EMAIL_VERIFY_PAGE - name: SEND_RESTORE_PASSWORD_EMAIL_PAGE valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: SEND_RESTORE_PASSWORD_EMAIL_PAGE - name: RESTORE_PASSWORD_PAGE valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: RESTORE_PASSWORD_PAGE - name: LOGIN_PAGE valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: LOGIN_PAGE - name: RECAPTCHA_HOSTNAME valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: RECAPTCHA_HOSTNAME - name: RECAPTCHA_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: RECAPTCHA_URL - name: RECAPTCHA_SECRET_KEY valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: RECAPTCHA_SECRET_KEY - name: GOOGLE_OAUTH_URI valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: GOOGLE_OAUTH_URI - name: GOOGLE_CLIENT_ID valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: GOOGLE_CLIENT_ID - name: GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: GOOGLE_CLIENT_SECRET - name: GOOGLE_REDIRECT_URI valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: GOOGLE_REDIRECT_URI - name: RMQ_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: RMQ_URL - name: FORGOT_PASSWORD_TOKEN_EXPIRES valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: FORGOT_PASSWORD_TOKEN_EXPIRES - name: POSTS_PROD_SERVICE_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: POSTS_PROD_SERVICE_URL - name: REDIS_USER valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: REDIS_USER - name: REDIS_PASSWORD valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: REDIS_PASSWORD - name: REDIS_HOST valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: REDIS_HOST - name: REDIS_PORT valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: REDIS_PORT - name: USERS_SERVICE_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: USERS_SERVICE_URL - name: POSTS_SERVICE_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: POSTS_SERVICE_URL - name: FILES_SERVICE_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: FILES_SERVICE_URL + - name: BUSINESS_SERVICE_URL + valueFrom: + secretKeyRef: + name: yogram-production-config-secret + key: BUSINESS_SERVICE_URL + - name: PORT + valueFrom: + secretKeyRef: + name: yogram-production-config-secret + key: PORT - name: FILES_PROD_SERVICE_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: FILES_PROD_SERVICE_URL - - name: BUSINESS_SERVICE_URL + - name: POSTS_PROD_SERVICE_URL valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret - key: BUSINESS_SERVICE_URL + name: yogram-production-config-secret + key: POSTS_PROD_SERVICE_URL + - name: USERS_PROD_SERVICE_URL + valueFrom: + secretKeyRef: + name: yogram-production-config-secret + key: USERS_PROD_SERVICE_URL + - name: PROFILE_SETTINGS_PAGE + valueFrom: + secretKeyRef: + name: yogram-production-config-secret + key: PROFILE_SETTINGS_PAGE - name: NODE_ENV valueFrom: secretKeyRef: - name: gate-yogram-production-config-secret + name: yogram-production-config-secret key: NODE_ENV diff --git a/apps/gate/src/auth/auth.module.ts b/apps/gate/src/auth/auth.module.ts index 8535abc6..72930f47 100644 --- a/apps/gate/src/auth/auth.module.ts +++ b/apps/gate/src/auth/auth.module.ts @@ -18,7 +18,7 @@ import { GoogleOauth } from './oauth/google.oauth'; import { SessionProvider } from './session/session.provider'; import { RedisModule } from '../../../../apps/libs/common/redis/redis.module'; import { RefreshGuard } from './guards/refresh.guard'; - +// @Module({ imports: [ forwardRef(() => UsersModule), diff --git a/apps/gate/src/business/business.controller.ts b/apps/gate/src/business/business.controller.ts index 496ab5f5..fe1c3e30 100644 --- a/apps/gate/src/business/business.controller.ts +++ b/apps/gate/src/business/business.controller.ts @@ -42,6 +42,7 @@ import { Request, Response } from 'express'; import axios from 'axios'; import { ReadNotificationDto } from '../../../../apps/libs/Business/dto/input/read-notification.dto'; import { ReadNotificationSwagger } from './decorators/swagger/read-notification.swagger.decorator'; +import { CancelSubscriptionSwagger } from './decorators/swagger/cancel-subscription.swagger'; @Controller('business') export class BusinessController { @@ -117,7 +118,8 @@ export class BusinessController { payment, ); console.log('link:', response.link); - res.status(200).redirect(303, response.link); + // res.status(200).redirect(303, response.link); + return res.status(307).json({ link: response.link }); } @Public() @@ -126,12 +128,24 @@ export class BusinessController { async paypalProcess( @Req() req: Request, @Query('payment') payment: PaymentType, - ): Promise { + @Res() res: Response, + ): Promise { if (req.body.event_type === PaypalEvents.BillingSubscriptionActivated) { - return await this.businessService.paypalProccess( + const subscription = await this.businessService.paypalProccess( req.body.resource.id, payment, ); + console.log( + '🚀 ~ BusinessController ~ paypalProcess ~ subscription:', + subscription['userId'], + ); + let page = this.configService.get('PROFILE_SETTINGS_PAGE'); + page = page.replace('replace', subscription['userId']); + console.log('🚀 ~ BusinessController ~ paypalProcess ~ page:', page); + // await this.businessService.subscriptionActivatedEvent(subscription) + // res.redirect(301, page); + // res.sendStatus(200); + return 200; } } @@ -191,6 +205,15 @@ export class BusinessController { return await this.businessService.activateSubscription(id, payment); } + @CancelSubscriptionSwagger() + @Patch('subscriptions/:id/cancel') + async cancelSubscription( + @Param('id') id: string, + @Query('payment') payment: PaymentType, + ): Promise { + return await this.businessService.cancelSubscription(id, payment); + } + @GetPaymentsSwagger() @Get('payments') async getPayments( diff --git a/apps/gate/src/business/business.module.ts b/apps/gate/src/business/business.module.ts index 52e71c3b..f2801bdf 100644 --- a/apps/gate/src/business/business.module.ts +++ b/apps/gate/src/business/business.module.ts @@ -3,10 +3,51 @@ import { BusinessService } from './business.service'; import { BusinessController } from './business.controller'; import { HttpModule } from '@nestjs/axios'; import { GateService } from '../../../../apps/libs/gateService'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { NotificationsModule } from 'apps/libs/common/notifications/notifications.module'; +import { RequestContextModule } from 'nestjs-request-context'; +import { BullModule } from '@nestjs/bullmq'; +import { NotificationConsumer } from './notifications-consumer.service'; +import { NotificationsProducer } from './notifications-producer.service'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ - imports: [HttpModule], + imports: [ + RequestContextModule, + ScheduleModule.forRoot(), + BullModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + connection: { + username: configService.get('REDIS_USER'), + port: configService.get('REDIS_PORT'), + host: configService.get('REDIS_HOST'), + password: configService.get('REDIS_PASSWORD'), + }, + }), + }), + BullModule.registerQueue({ + name: 'NOTIFICATION_SCHEDULER', + prefix: 'scheduler:', + }), + HttpModule, + NotificationsModule.register(), + JwtModule.registerAsync({ + inject: [ConfigService], + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + }), + }), + ], controllers: [BusinessController], - providers: [BusinessService, GateService], + providers: [ + BusinessService, + GateService, + NotificationConsumer, + NotificationsProducer, + ], }) export class BusinessModule {} diff --git a/apps/gate/src/business/business.service.ts b/apps/gate/src/business/business.service.ts index ffdff6f7..2a1d804e 100644 --- a/apps/gate/src/business/business.service.ts +++ b/apps/gate/src/business/business.service.ts @@ -10,10 +10,14 @@ import { PaymentType } from '../../../../apps/libs/Business/constants/payment-ty import { SubscribeDto } from '../../../../apps/libs/Business/dto/input/subscribe.dto'; import { GateService } from '../../../../apps/libs/gateService'; import { Injectable } from '@nestjs/common'; +import { NotificationsService } from 'apps/libs/common/notifications/notifications.service'; @Injectable() export class BusinessService { - constructor(private readonly gateService: GateService) {} + constructor( + private readonly gateService: GateService, + private readonly notificationsService: NotificationsService, + ) {} async subscribe( subscribeDto: SubscribeDto, @@ -32,9 +36,15 @@ export class BusinessService { subscriptionId: string, payment: PaymentType, ): Promise { + console.log('🚀 ~ BusinessService ~ paypalProccess ~ payment:', payment); + console.log( + '🚀 ~ BusinessService ~ paypalProccess ~ subscriptionId:', + subscriptionId, + ); const path = [HttpBusinessPath.PaypalProcess, `payment=${payment}`].join( '?', ); + console.log('🚀 ~ BusinessService ~ paypalProccess ~ path:', path); return await this.gateService.requestHttpServicePost( HttpServices.Business, path, @@ -126,6 +136,19 @@ export class BusinessService { ); } + async cancelSubscription(id: string, payment: PaymentType): Promise { + const path = [ + [HttpBusinessPath.CancelSubscription.replace(':id', id)].join('/'), + `payment=${payment}`, + ].join('?'); + return await this.gateService.requestHttpServicePatch( + HttpServices.Business, + path, + {}, + {}, + ); + } + async getPayments( payment: PaymentType, pagination: IPagination, @@ -146,21 +169,27 @@ export class BusinessService { ); } + // async subscriptionActivatedEvent(subscription: void) { + // await this + // } + async getUserNotifications(userId: string): Promise { - const path = [HttpBusinessPath.GetNotifications, userId].join('/'); - return await this.gateService.requestHttpServiceGet( - HttpServices.Business, - path, - {}, - ); + // const path = [HttpBusinessPath.GetNotifications, userId].join('/'); + // return await this.gateService.requestHttpServiceGet( + // HttpServices.Business, + // path, + // {}, + // ); + return await this.notificationsService.getUserNotifications(userId); } async readNotification(notificationId: string): Promise { - return await this.gateService.requestHttpServicePatch( - HttpServices.Business, - HttpBusinessPath.ReadNotification, - { notificationId }, - {}, - ); + // return await this.gateService.requestHttpServicePatch( + // HttpServices.Business, + // HttpBusinessPath.ReadNotification, + // { notificationId }, + // {}, + // ); + return await this.notificationsService.updateNotification(notificationId); } } diff --git a/apps/gate/src/business/constants/business-sse.enum.ts b/apps/gate/src/business/constants/business-sse.enum.ts new file mode 100644 index 00000000..93ad8748 --- /dev/null +++ b/apps/gate/src/business/constants/business-sse.enum.ts @@ -0,0 +1,4 @@ +export enum BusinessSse { + PaymentSuccess = 'paymentSuccess', + PaymentError = 'paymentError', +} diff --git a/apps/gate/src/business/constants/expires-in-duration.enum.ts b/apps/gate/src/business/constants/expires-in-duration.enum.ts new file mode 100644 index 00000000..3454ddb7 --- /dev/null +++ b/apps/gate/src/business/constants/expires-in-duration.enum.ts @@ -0,0 +1,5 @@ +export enum ExpiresInDuration { + Day = 86400 * 1000, + Week = 86400 * 7 * 1000, + Month = 86400 * 30 * 1000, +} diff --git a/apps/gate/src/business/constants/websocket.event.enum.ts b/apps/gate/src/business/constants/websocket.event.enum.ts new file mode 100644 index 00000000..57196d0e --- /dev/null +++ b/apps/gate/src/business/constants/websocket.event.enum.ts @@ -0,0 +1,4 @@ +export enum WebsocketEvents { + SubscriptionActive = 'subscription.active', + DaysToExpires = 'days.to.expires', +} diff --git a/apps/gate/src/business/decorators/swagger/activate-subscription-swagger.decorator.ts b/apps/gate/src/business/decorators/swagger/activate-subscription-swagger.decorator.ts index 3db64839..e36c1eef 100644 --- a/apps/gate/src/business/decorators/swagger/activate-subscription-swagger.decorator.ts +++ b/apps/gate/src/business/decorators/swagger/activate-subscription-swagger.decorator.ts @@ -39,7 +39,7 @@ export function ActivateSubscriptionSwagger() { }), ApiResponse({ status: 409, - description: 'PayPalService error: subscription is active already', + description: 'PayPalService error: subscription is active already ', }), ); } diff --git a/apps/gate/src/business/decorators/swagger/cancel-subscription.swagger.ts b/apps/gate/src/business/decorators/swagger/cancel-subscription.swagger.ts new file mode 100644 index 00000000..6509f89b --- /dev/null +++ b/apps/gate/src/business/decorators/swagger/cancel-subscription.swagger.ts @@ -0,0 +1,44 @@ +import { + ApiHeader, + ApiOperation, + ApiParam, + ApiQuery, + ApiResponse, +} from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; +import { PaymentType } from '../../../../../../apps/libs/Business/constants/payment-type.enum'; + +export function CancelSubscriptionSwagger() { + return applyDecorators( + ApiHeader({ + name: 'Authorization', + description: 'Authorization with bearer token', + }), + ApiParam({ + name: 'id', + type: 'string', + example: 'I-1CWCLXSVTX7R', + }), + ApiQuery({ + name: 'payment', + required: true, + type: 'string', + example: 'payment=paypal', + enum: PaymentType, + }), + ApiOperation({ + summary: 'Cancel subscription.', + }), + ApiResponse({ + status: 200, + }), + ApiResponse({ + status: 404, + description: 'BusinessCommandService error: subscription does not exist', + }), + ApiResponse({ + status: 409, + description: 'PayPalService error: subscription is canceled already', + }), + ); +} diff --git a/apps/gate/src/business/helper/create-array-from-object.helper.ts b/apps/gate/src/business/helper/create-array-from-object.helper.ts new file mode 100644 index 00000000..e027e10d --- /dev/null +++ b/apps/gate/src/business/helper/create-array-from-object.helper.ts @@ -0,0 +1,13 @@ +export const createObjectFromArrayReduce = (arr: any) => { + for (let i = 1; i < arr.length; i++) { + arr[i] = arr[i].reduce((acc, current, index, array) => { + if (index % 2 === 0 && index + 1 < array.length) { + acc[current] = array[index + 1]; + } else if (index % 2 === 0 && index + 1 >= array.length) { + acc[current] = undefined; + } + return acc; + }, {}); + } + return arr; +}; diff --git a/apps/gate/src/business/helper/get-notification-key.helper.ts b/apps/gate/src/business/helper/get-notification-key.helper.ts new file mode 100644 index 00000000..c9a3a347 --- /dev/null +++ b/apps/gate/src/business/helper/get-notification-key.helper.ts @@ -0,0 +1,14 @@ +import { INotification } from 'apps/libs/common/notifications/interfaces/notification.interface'; +import { EnvironmentMode } from '../../settings/configuration'; + +export const getNotificationKey = ( + subscription: any, + notification: INotification, +): string => { + return `${ + process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && + process.env.NODE_ENV !== EnvironmentMode.TESTING + ? '' + : 'dev:' + }notifications:user:${subscription.userId}:notification:${notification.id}`; +}; diff --git a/apps/gate/src/business/helper/get-subscription-price.helper.ts b/apps/gate/src/business/helper/get-subscription-price.helper.ts new file mode 100644 index 00000000..dd809620 --- /dev/null +++ b/apps/gate/src/business/helper/get-subscription-price.helper.ts @@ -0,0 +1,15 @@ +import { BadRequestException } from '@nestjs/common'; +import { SubscriptionType } from 'apps/libs/Business/constants/subscription-type.enum'; + +export function getSubscriptionPrice(subscriptionType: SubscriptionType) { + switch (subscriptionType) { + case SubscriptionType.OneDay: + return 10; + case SubscriptionType.SevenDays: + return 50; + case SubscriptionType.Month: + return 100; + default: + throw new BadRequestException('error: invalid subscriptionPrice value'); + } +} diff --git a/apps/gate/src/business/notifications-consumer.service.ts b/apps/gate/src/business/notifications-consumer.service.ts new file mode 100644 index 00000000..05c464ae --- /dev/null +++ b/apps/gate/src/business/notifications-consumer.service.ts @@ -0,0 +1,55 @@ +import { NotificationsGateway } from 'apps/libs/common/notifications/notifications.gateway'; +import { createObjectFromArrayReduce } from './helper/create-array-from-object.helper'; +import { ExpiresInDuration } from './constants/expires-in-duration.enum'; +import { WebsocketEvents } from './constants/websocket.event.enum'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Job } from 'bullmq'; + +@Processor('NOTIFICATION_SCHEDULER') +export class NotificationConsumer extends WorkerHost { + constructor(private readonly notificationGateway: NotificationsGateway) { + super(); + } + + async process(job: Job, token?: string): Promise { + console.log('@Processor(NOTIFICATION_SCHEDULER)'); + + const values = Object.values(ExpiresInDuration); + let notificationsArray = ( + await Promise.all( + values.map(async (item, index) => { + if (index < Object.values(ExpiresInDuration).length / 2) { + const result = + await this.notificationGateway.getExpiresInNotifications( + ExpiresInDuration[item], + ); + return result; + } + }), + ) + ).filter((item) => item !== undefined); + console.log( + '🚀 ~ NotificationConsumer ~ process ~ notificationsArray:', + notificationsArray, + ); + + await Promise.all( + notificationsArray.map(async (item) => { + const notificationsObjectsAray = createObjectFromArrayReduce(item); + delete notificationsObjectsAray.differ; + notificationsObjectsAray.shift(); + for (const item of notificationsObjectsAray) { + console.log('🚀 ~ NotificationConsumer ~ process ~ item:', item); + if (item.delivered === 'false') { + await this.notificationGateway.send( + item, + WebsocketEvents.DaysToExpires, + 0, + ); + } + } + }), + ); + return {}; + } +} diff --git a/apps/gate/src/business/notifications-producer.service.ts b/apps/gate/src/business/notifications-producer.service.ts new file mode 100644 index 00000000..3f058535 --- /dev/null +++ b/apps/gate/src/business/notifications-producer.service.ts @@ -0,0 +1,39 @@ +import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Queue } from 'bullmq'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { NotificationsGateway } from 'apps/libs/common/notifications/notifications.gateway'; + +@Injectable() +export class NotificationsProducer implements OnApplicationBootstrap { + constructor( + @InjectQueue('NOTIFICATION_SCHEDULER') private notificationsQueue: Queue, + private readonly configService: ConfigService, + private readonly notificationGateway: NotificationsGateway, + ) { + console.log('NotificationsProducer starts'); + } + + async onApplicationBootstrap() { + console.log('TIME_PERIOD', this.configService.get('TIME_PERIOD')); + + await this.sendNotificationToQueue(); + } + + @Cron(CronExpression.EVERY_10_SECONDS) + async sendNotificationToQueue() { + console.log('sendNotificationToQueue...'); + + await this.notificationsQueue.add( + 'NOTIFICATION_SCHEDULER', + {}, + { + // repeat: { every: this.configService.get('TIME_PERIOD') }, + removeOnComplete: true, + removeOnFail: true, + }, + ); + console.log('after'); + } +} diff --git a/apps/gate/src/business/notifications.gateway.ts b/apps/gate/src/business/notifications.gateway.ts new file mode 100644 index 00000000..14d319f5 --- /dev/null +++ b/apps/gate/src/business/notifications.gateway.ts @@ -0,0 +1,46 @@ +import { socketAuthMiddleware } from '../../../../apps/libs/common/notifications/helper/socket-auth.helper'; +import { OnModuleInit } from '@nestjs/common'; +import { Server, Socket } from 'socket.io'; +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { JwtService } from '@nestjs/jwt'; + +@WebSocketGateway(3007, { namespace: 'notifications' }) +export class NotificationsGateway implements OnModuleInit, OnGatewayConnection { + @WebSocketServer() server: Server; + private connectedSockets: string[] = []; + constructor(private readonly jwtService: JwtService) {} + + handleConnection(socket: Socket) { + console.log(`${socket.id} connected`); + //todo* make auth -> connectedSockets.push({socket.id, userId}) -> send to notifications -> add there to array + this.connectedSockets.push(socket.id); + this.server.emit('connectedSocket', { + userId: socket.data.user, + socketId: socket.id, + }); + } + + afterInit(server: Server) { + const authMiddleware = socketAuthMiddleware(this.jwtService); + server.use(authMiddleware); + } + + onModuleInit() { + this.server.on('message_from_gateway', (data: any) => { + console.log('Received from gateway:', data); + }); + } + + @SubscribeMessage('send_to_gateway') + handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) { + console.log('message', data); + client.emit('message_from_gateway', 'Hello from Gateway!'); + } +} diff --git a/apps/gate/src/main.ts b/apps/gate/src/main.ts index cd628351..633ab974 100644 --- a/apps/gate/src/main.ts +++ b/apps/gate/src/main.ts @@ -30,10 +30,13 @@ async function bootstrap() { 'Connection', 'User-Agent', 'x-recaptcha-token', + '*', ], exposedHeaders: ['Set-cookie'], origin: [ + 'https://developer.paypal.com', + 'https://www.sandbox.paypal.com', 'https://yogram.ru', 'http://localhost:5173', 'http://localhost:56938', @@ -48,7 +51,7 @@ async function bootstrap() { const { port, env } = applyAppSettings(app); useContainer(app.select(AppModule), { fallbackOnErrors: true }); - await app.init(); + // await app.init(); await app.listen(port, () => { console.log('App starting service GATE listen port: ', port, 'ENV: ', env); }); diff --git a/apps/gate/src/settings/configuration.ts b/apps/gate/src/settings/configuration.ts index 96a78170..a3a5785a 100644 --- a/apps/gate/src/settings/configuration.ts +++ b/apps/gate/src/settings/configuration.ts @@ -27,7 +27,7 @@ export const getConfiguration = () => { }, {}, ); - console.log('SERVICES_URLS', SERVICES_URLS); + console.log('SERVICES_URLS:', SERVICES_URLS); return { NODE_ENV: (Environments.includes(process.env.NODE_ENV?.trim()) diff --git a/apps/index.ts b/apps/index.ts new file mode 100644 index 00000000..917a90c1 --- /dev/null +++ b/apps/index.ts @@ -0,0 +1,125 @@ +export {}; + +// Метод для получения списка сотрудников через API1 +const getEmployees = async () => { + // Эмулируем вызов API через возврат промиса + return Promise.resolve([ + { id: 1, name: 'Вася', department: 'Frontend' }, + { id: 2, name: 'Петя', department: 'Backend' }, + { id: 3, name: 'Дима', department: 'Frontend' }, + { id: 4, name: 'Оля', department: 'Backend' }, + { id: 5, name: 'Саша', department: 'Frontend' }, + { id: 6, name: 'Олег', department: 'Testing' }, + ]); +}; + +// Зарплаты сотрудников из API +const getEmployeeSalary = async (employeeId: number) => { + // Эмулируем вызов API через возврат промиса + const salaryByEmployeeId = { + '1': 10000, + '2': 12000, + '3': 10500, + '4': 15000, + '5': 8000, + '6': 9000, + }; + + return Promise.resolve(salaryByEmployeeId[employeeId]); +}; + +// ********************************************************* + +const getAllEmployees = async () => { + const result = await getEmployees(); + const backend = result.filter((item) => { + if (item.department === 'Backend') return item; + }); + console.log('getAllEmployees', backend); +}; + +const getMaxSalary = async () => { + let maxSalary = 0; + let i = 1; + let process = true; + while (process) { + try { + const salary = await getEmployeeSalary(i); + if (!salary) process = false; + + if (salary === 0) ++i; + + if (salary >= maxSalary) { + maxSalary = salary; + ++i; + } else ++i; + } catch (error) { + process = false; + return maxSalary; + } + } + return maxSalary; +}; + +const getF = async () => { + let totalDepartmentsSpends = []; + + for (let i = 1; i <= 6; i++) { + const salary = await getEmployeeSalary(i); + totalDepartmentsSpends.push([i, salary]); + } + + const sortedSalary = totalDepartmentsSpends.sort((a, b) => b[1] - a[1]); + + const employees = await getEmployees(); + const names = sortedSalary.map((salary) => { + const empl = employees.find((emp) => { + if (emp.id === salary[0]) { + return emp; + } + }); + return { id: empl.id, name: empl.name, salary: salary[1] }; + }); + console.log('🚀 ~ getF ~ names:', names); +}; + +const superLastTask = async () => { + const Departments = ['Frontend', 'Backend', 'Testing']; + + const employees = await getEmployees(); + let totalDepartmentsSpends = []; + for (let dep of Departments) { + let depRecord = { department: dep, spends: 0 }; + + for (let empl of employees) { + if (empl.department === dep) { + const spend = await getEmployeeSalary(empl.id); + depRecord.spends += spend; + } + } + + totalDepartmentsSpends.push(depRecord); + } + console.log('totalDepartmentsSpends:', totalDepartmentsSpends); + + const totalEmployees = Departments.map((dep) => { + const totalEmployees = employees.reduce((acc, cur, i) => { + return cur.department === dep ? (acc += 1) : acc; + }, 0); + return { department: dep, totalEmployees }; + }); + + console.log('totalEmployees:', totalEmployees); +}; + +// await superLastTask(); + +// Дано: +// 1. Функция для получения списка сотрудников через API1 +// 2. Функция для получения зарплаты каждого сотрудника через API2 + +// Необходимо реализовать функции, которые вернут в качестве результата: +// 1. Список сотрудников только Backend +// 2. Размер максимальной зарплаты +// 3. Список имен сотрудников, отсортированный по размеру зарплаты +// 4. Статистика по каждому отделу: сумма затрат, количество сотрудников, средняя з/п, максимальная з/п diff --git a/apps/libs/Business/constants/path.constant.ts b/apps/libs/Business/constants/path.constant.ts index 8605829b..a9279245 100644 --- a/apps/libs/Business/constants/path.constant.ts +++ b/apps/libs/Business/constants/path.constant.ts @@ -3,6 +3,7 @@ export const HttpBusinessPath = { CurrentSubscriptions: 'business/subscriptions/get', SuspendSubscription: 'business/subscriptions/:id/suspend', ActivateSubscription: 'business/subscriptions/:id/activate', + CancelSubscription: 'business/subscriptions/:id/cancel', PaypalProcess: 'business/paypal-proccess', SubscriptionsExpired: 'business/subscriptions/expired', SubscriptionsUpdated: 'business/subscriptions/updated', diff --git a/apps/libs/Business/dto/response/response-notification.dto.ts b/apps/libs/Business/dto/response/response-notification.dto.ts index b32725ef..b8c8f29a 100644 --- a/apps/libs/Business/dto/response/response-notification.dto.ts +++ b/apps/libs/Business/dto/response/response-notification.dto.ts @@ -15,4 +15,6 @@ export class NotificationResponseDto implements INotification { expiresAt: number; @ApiProperty({ description: 'timestamp' }) createdAt: number; + @ApiProperty({ description: 'boolean' }) + delivered: boolean; } diff --git a/apps/libs/Business/dto/response/response-payment.dto.ts b/apps/libs/Business/dto/response/response-payment.dto.ts index f4519204..64c59785 100644 --- a/apps/libs/Business/dto/response/response-payment.dto.ts +++ b/apps/libs/Business/dto/response/response-payment.dto.ts @@ -1,3 +1,11 @@ +import { Expose, Type } from 'class-transformer'; import { Payment } from '../../../../../apps/business/src/infrastructure/entity/payment.entity'; +import { Subscription } from 'apps/business/src/infrastructure/entity/subscription.entity'; -export class ResponsePaymentDto extends Payment {} +export class ResponsePaymentDto extends Payment { + @Expose() + expiresAt: Date; + @Expose() + @Type(() => Subscription) + subscription: Subscription; +} diff --git a/apps/libs/common/database/database.module.ts b/apps/libs/common/database/database.module.ts index b6fb6d4c..91a8cec8 100644 --- a/apps/libs/common/database/database.module.ts +++ b/apps/libs/common/database/database.module.ts @@ -36,9 +36,9 @@ export class DatabaseModule { migrationsTableName: configService.get('migrationsTableName'), entities: [`${__dirname}/infrastructure/**/*.entity{.ts,.js}`], migrations: [`${__dirname}/../../db/migrations/*{.ts,.js}`], - autoLoadEntities: configService.get('autoLoadEntities'), - synchronize: configService.get('synchronize'), - dropSchema: configService.get('dropSchema'), + autoLoadEntities: true, + synchronize: false, + dropSchema: false, }, dataSourceFactory: async (options) => { const dataSource = await new DataSource(options).initialize(); diff --git a/apps/libs/common/notifications/helper/socket-auth.helper.ts b/apps/libs/common/notifications/helper/socket-auth.helper.ts index b29cd787..e7fcbe82 100644 --- a/apps/libs/common/notifications/helper/socket-auth.helper.ts +++ b/apps/libs/common/notifications/helper/socket-auth.helper.ts @@ -10,9 +10,11 @@ export const socketAuthMiddleware = ( ): SocketMiddleware => { return async (socket: Socket, next) => { try { - const token = socket.handshake.headers?.authorization; + console.log('authorization:', socket.handshake.headers.authorization); + const token = socket.handshake.headers.authorization; + console.log('🚀 ~ socketAuthMiddleware ~ token:', token); if (!token) next(new WsException('Socket Unauthorized Exception')); - const payload = jwtService.verify(token.trim()); + const payload = jwtService.verify(token); if (!payload) next(new WsException('Socket Unauthorized Exception')); socket.data.user = payload['id']; next(); diff --git a/apps/libs/common/notifications/interfaces/notification-service.interface.ts b/apps/libs/common/notifications/interfaces/notification-service.interface.ts index ed70c61c..84c884b3 100644 --- a/apps/libs/common/notifications/interfaces/notification-service.interface.ts +++ b/apps/libs/common/notifications/interfaces/notification-service.interface.ts @@ -8,10 +8,7 @@ import { } from '@nestjs/websockets'; import { ExpiresInDuration } from 'apps/business/src/constants/expires-in-duration.enum'; -export interface INotificationsService - extends OnGatewayConnection, - OnGatewayInit, - OnGatewayDisconnect { +export interface INotificationsService { send( notification: INotification, event: WebsocketEvents, diff --git a/apps/libs/common/notifications/interfaces/notification.interface.ts b/apps/libs/common/notifications/interfaces/notification.interface.ts index fa189219..7999354e 100644 --- a/apps/libs/common/notifications/interfaces/notification.interface.ts +++ b/apps/libs/common/notifications/interfaces/notification.interface.ts @@ -6,4 +6,5 @@ export interface INotification { createdAt: number; readAt: number; expiresAt: number; + delivered: boolean; } diff --git a/apps/libs/common/notifications/notifications.gateway.ts b/apps/libs/common/notifications/notifications.gateway.ts index 78098058..61c4a202 100644 --- a/apps/libs/common/notifications/notifications.gateway.ts +++ b/apps/libs/common/notifications/notifications.gateway.ts @@ -1,34 +1,73 @@ +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + OnGatewayDisconnect, + OnGatewayInit, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; import { NotificationResponseDto } from '../../../../apps/libs/Business/dto/response/response-notification.dto'; import { ExpiresInDuration } from '../../../../apps/business/src/constants/expires-in-duration.enum'; import { WebsocketEvents } from '../../../../apps/business/src/constants/websocket.event.enum'; import { INotificationsService } from './interfaces/notification-service.interface'; -import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { INotification } from './interfaces/notification.interface'; -import { socketAuthMiddleware } from './helper/socket-auth.helper'; import { NotificationsService } from './notifications.service'; +import { OnModuleInit } from '@nestjs/common'; import { Socket, Server } from 'socket.io'; +import { socketAuthMiddleware } from './helper/socket-auth.helper'; import { JwtService } from '@nestjs/jwt'; -@WebSocketGateway() -export class NotificationsGateway implements INotificationsService { - private connectedClients: Map = new Map(); +@WebSocketGateway(0, { namespace: 'event/notification' }) +export class NotificationsGateway + implements + INotificationsService, + OnModuleInit, + OnGatewayConnection, + OnGatewayDisconnect, + OnGatewayInit +{ + private connectedSockets: string[] = []; + @WebSocketServer() - private server: Server; + server: Server; // The Socket.IO server instance constructor( private readonly notificationsService: NotificationsService, private readonly jwtService: JwtService, - ) {} + ) { + console.log('WebSocketServer'); + } + + @SubscribeMessage('message') + handleMessage( + @ConnectedSocket() client: Socket, + @MessageBody() data: string, + ): void { + console.log( + `Received message from client ${client.id} on 'message' channel: ${data}`, + ); + + client.emit('messageReceived', `Server received your message: ${data}`); + } - afterInit(server: any) { + async onModuleInit() { + console.log('websocket'); + } + + afterInit(server: Server) { const authMiddleware = socketAuthMiddleware(this.jwtService); server.use(authMiddleware); } - handleDisconnect(socket: Socket) { - this.notificationsService.handleDisconnect(socket); + + handleConnection(@ConnectedSocket() client: Socket) { + this.notificationsService.addClient(client); + client.emit('connection', `${client.data.user} connected`); } - handleConnection(socket: Socket) { - this.notificationsService.handleConnection(socket); + handleDisconnect(@ConnectedSocket() client: Socket) { + this.notificationsService.removeClient(client); + client.emit('disconnect', `${client.data.user} disconnected`); } async saveNotification( diff --git a/apps/libs/common/notifications/notifications.module.ts b/apps/libs/common/notifications/notifications.module.ts index eb7b6d4c..d5de38c1 100644 --- a/apps/libs/common/notifications/notifications.module.ts +++ b/apps/libs/common/notifications/notifications.module.ts @@ -4,6 +4,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { DynamicModule, Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { RedisModule } from '../redis/redis.module'; +import { WsAuthAdapter } from './ws-auth.adapter'; @Module({}) export class NotificationsModule { @@ -22,7 +23,7 @@ export class NotificationsModule { }), ], providers: [NotificationsService, NotificationsGateway], - exports: [NotificationsGateway], + exports: [NotificationsGateway, NotificationsService], }; } } diff --git a/apps/libs/common/notifications/notifications.service.ts b/apps/libs/common/notifications/notifications.service.ts index ed4de7ee..870a3cee 100644 --- a/apps/libs/common/notifications/notifications.service.ts +++ b/apps/libs/common/notifications/notifications.service.ts @@ -25,6 +25,15 @@ export class NotificationsService implements INotificationsService { this.redisClient.call(''); } + //todo* add addClient / removeClient + addClient(socket: Socket) { + this.connectedClients.set(socket.data.user, socket); + } + + removeClient(socket: Socket) { + this.connectedClients.delete(socket.data.user); + } + handleDisconnect(socket: Socket) { this.connectedClients.delete(socket.id); @@ -54,6 +63,7 @@ export class NotificationsService implements INotificationsService { ? '' : 'dev:' }notifications:user:${userId}:notification:*`; + const stream = this.redisClient.scanStream({ match: match, }); @@ -77,6 +87,7 @@ export class NotificationsService implements INotificationsService { async handleConnection(socket: Socket) { this.connectedClients.set(socket.data.user, socket); + console.log('connectedClients', this.connectedClients); } async send( @@ -85,10 +96,12 @@ export class NotificationsService implements INotificationsService { delay: number, ): Promise { const socket = this.connectedClients.get(notification.userId); + console.log('🚀 ~ NotificationsService ~ send ~ socket:', socket.id); if (socket) { setTimeout(() => { socket.emit(event, notification.message); }, delay); + await this.updateNotificationDelivery(notification.id); } } @@ -129,9 +142,10 @@ export class NotificationsService implements INotificationsService { // todo return expiresAt - now === 7 // todo when renew subscription(update) create new notification with the same subscriptionId async getExpiresInNotifications(expiresInDuration: ExpiresInDuration) { + // await this.createIndex(); try { const todayTimestamp = new Date().getTime(); - const results = await this.redisClient.call( + let results = await this.redisClient.call( 'FT.AGGREGATE', process.env.NODE_ENV !== EnvironmentMode.DEVELOPMENT && process.env.NODE_ENV !== EnvironmentMode.TESTING @@ -139,27 +153,31 @@ export class NotificationsService implements INotificationsService { : 'dev:notifications:Idx', '*', 'LOAD', - '7', + '8', '@expiresAt', '@message', '@subscriptionId', - 'userId', + '@userId', '@createdAt', '@readAt', '@id', + '@delivered', 'APPLY', + //todo! end of month has problems, usually use 86400 `(@expiresAt - ${todayTimestamp})`, 'AS', 'differ', // Search query 'FILTER', expiresInDuration === ExpiresInDuration.Day - ? `@differ > 0 && @differ < ${expiresInDuration + 86400 * 1000}` + ? //todo! end of month has problems, usually use 86400 + `@differ > 0 && @differ < ${expiresInDuration + 1164400 * 1000}` : expiresInDuration === ExpiresInDuration.Week ? `@differ > ${expiresInDuration} && @differ < ${expiresInDuration + 86400 * 1000}` : expiresInDuration === ExpiresInDuration.Month ? `@differ > ${expiresInDuration} && @differ < ${expiresInDuration + 86400 * 1000}` : null, ); + return results; } catch (error) { console.error('Error searching data:', error); @@ -172,26 +190,36 @@ export class NotificationsService implements INotificationsService { process.env.NODE_ENV !== EnvironmentMode.TESTING ? 'notifications:Idx' : 'dev:notifications:Idx'; + notificationId = notificationId.replaceAll('-', '\\-'); + const notification = await this.redisClient.call( 'FT.SEARCH', index, `@id:{${notificationId}}`, ); - if (!notification) - throw new NotFoundException( - 'NotificationsService error: notification not found', - ); - if (notification[2][7] !== '') - throw new ConflictException( - 'NotificationsService error: notification has already been read', - ); + + // if (!notification) + // throw new NotFoundException( + // 'NotificationsService error: notification not found', + // ); + // if (notification[2][8] !== '') + // throw new ConflictException( + // 'NotificationsService error: notification has already been read', + // ); return notification; } async updateNotification(notificationId: string): Promise { const notification = await this.getNotificationById(notificationId); + console.log('updateNotification ~ notification:', notification); const readedAt = new Date().getTime(); await this.redisClient.hset(notification[1], 'readAt', readedAt); } + + async updateNotificationDelivery(notificationId: string): Promise { + const notification = await this.getNotificationById(notificationId); + + await this.redisClient.hset(notification[1], 'delivered', 'true'); + } } diff --git a/apps/libs/common/notifications/ws-auth.adapter.ts b/apps/libs/common/notifications/ws-auth.adapter.ts new file mode 100644 index 00000000..8a3a1057 --- /dev/null +++ b/apps/libs/common/notifications/ws-auth.adapter.ts @@ -0,0 +1,41 @@ +import { IoAdapter } from '@nestjs/platform-socket.io'; +import { INestApplicationContext, Logger } from '@nestjs/common'; +import { Server, Socket } from 'socket.io'; +import { JwtService } from '@nestjs/jwt'; + +export class WsAuthAdapter extends IoAdapter { + private readonly logger = new Logger(WsAuthAdapter.name); + + constructor( + private jwtService: JwtService, + private readonly app: INestApplicationContext, + ) { + super(app); + } + + createIOServer(port: number, options?: any): Server { + const server = super.createIOServer(port, options); + server.use(async (socket: Socket, next) => { + const token = socket.handshake.auth.token; + console.log('🚀 ~ WsAuthAdapter ~ createIOServer ~ token:', token); + if (!token) { + this.logger.error('No authorization token provided'); + return next(new Error('Authentication error')); + } + try { + const user = await this.jwtService.verifyAsync(token); + + if (!user) { + return next(new Error('Authentication error')); + } + + (socket as any).user = user; + next(); + } catch (error) { + this.logger.error('Token validation failed', error.stack); + next(new Error('Authentication error')); + } + }); + return server; + } +} diff --git a/apps/libs/gateService/index.ts b/apps/libs/gateService/index.ts index 77860c57..e5bb7f85 100644 --- a/apps/libs/gateService/index.ts +++ b/apps/libs/gateService/index.ts @@ -23,12 +23,17 @@ export class GateService { // : `${this.configService.get('USERS_PROD_SERVICE_URL')}/api/v1`; // remove all constructor before this line // example for normal switch service throw config in micro + Object.assign(this.services, { POSTS: this.configService.get('POSTS_SERVICE_URL'), USERS: this.configService.get('USERS_SERVICE_URL'), FILES: this.configService.get('FILES_SERVICE_URL'), BUSINESS: this.configService.get('BUSINESS_SERVICE_URL'), }); + console.log( + 'BUSINESS_SERVICE_URL index', + this.configService.get('BUSINESS_SERVICE_URL'), + ); } async requestHttpServicePost(service, path, payload, headers) { @@ -51,6 +56,11 @@ export class GateService { async requestHttpServiceGet(service, path, headers) { try { + console.log( + 'requestHttpServiceGet', + [this.services[service], path].join('/'), + ); + const { data } = await lastValueFrom( this.httpService.get([this.services[service], path].join('/'), { headers, diff --git a/apps/mailer/Jenkinsfile b/apps/mailer/Jenkinsfile index e600a5e7..8ebe6fdb 100644 --- a/apps/mailer/Jenkinsfile +++ b/apps/mailer/Jenkinsfile @@ -3,13 +3,13 @@ pipeline { agent any environment { ENV_TYPE = "production" - PORT = 3884 + PORT = 4053 NAMESPACE = "yogram-ru" REGISTRY_HOSTNAME = "idogmat" - PROJECT = "mailer" + PROJECT = "yogram-mailer" SERVICE="mailer" REGISTRY = "registry.hub.docker.com" - DEPLOYMENT_NAME = "mailer-deployment" + DEPLOYMENT_NAME = "yogram-mailer-deployment" IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}" DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}" } diff --git a/apps/mailer/deployment.yaml b/apps/mailer/deployment.yaml index 37f6e317..8fb166a5 100644 --- a/apps/mailer/deployment.yaml +++ b/apps/mailer/deployment.yaml @@ -25,35 +25,35 @@ spec: - name: SMTP_USER valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: SMTP_USER - name: SMTP_HOST valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: SMTP_HOST - name: SMTP_PASS valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: SMTP_PASS - name: SMTP_PORT valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: SMTP_PORT - name: JWT_SECRET valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: JWT_SECRET - name: VERIFY_TOKEN_EXPIRES valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: VERIFY_TOKEN_EXPIRES - name: RMQ_URL valueFrom: secretKeyRef: - name: mailer-production-config-secret + name: yogram-mailer-production-config-secret key: RMQ_URL diff --git a/apps/posts/Jenkinsfile b/apps/posts/Jenkinsfile index a4eeaaa6..e4c68b72 100644 --- a/apps/posts/Jenkinsfile +++ b/apps/posts/Jenkinsfile @@ -3,13 +3,13 @@ pipeline { agent any environment { ENV_TYPE = "production" - PORT = 3914 + PORT = 4050 NAMESPACE = "yogram-ru" REGISTRY_HOSTNAME = "idogmat" - PROJECT = "posts-yogram" + PROJECT = "yogram-posts" SERVICE="posts" REGISTRY = "registry.hub.docker.com" - DEPLOYMENT_NAME = "posts-yogram-deployment" + DEPLOYMENT_NAME = "yogram-posts-deployment" IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}" DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}" } diff --git a/apps/posts/deployment.yaml b/apps/posts/deployment.yaml index afeb95dc..05b53ed8 100644 --- a/apps/posts/deployment.yaml +++ b/apps/posts/deployment.yaml @@ -16,69 +16,49 @@ spec: project: PROJECT spec: containers: - - name: PROJECT - image: REGISTRY_HOSTNAME/PROJECT:TAG_VERSION - ports: - - containerPort: PORT_CONTAINER + - name: PROJECT + image: REGISTRY_HOSTNAME/PROJECT:TAG_VERSION + ports: + - containerPort: PORT_CONTAINER - env: - - name: POSTGRES_TYPE - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: POSTGRES_TYPE - - name: POSTGRES_MIGRATION_TABLE - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: POSTGRES_MIGRATION_TABLE - - name: SYNCHRONIZE - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: SYNCHRONIZE - - name: AUTOLOAD_ENTITIES - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: AUTOLOAD_ENTITIES - - name: DROP_SCHEMA - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: DROP_SCHEMA - - name: RMQ_URL - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: RMQ_URL - - name: FILES_SERVICE_URL - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: FILES_SERVICE_URL - - name: BUCKET - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: BUCKET - - name: FILES_SERVICE_POSTS_UPLOAD_PATH - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: FILES_SERVICE_POSTS_UPLOAD_PATH - - name: FILES_SERVICE_CHUNKS_DIR - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: FILES_SERVICE_CHUNKS_DIR - - name: USERS_SERVICE_URL - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: USERS_SERVICE_URL - - name: POSTGRES_URL - valueFrom: - secretKeyRef: - name: posts-yogram-production-config-secret - key: POSTGRES_URL + env: + - name: POSTGRES_TYPE + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: POSTGRES_TYPE + - name: RMQ_URL + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: RMQ_URL + - name: FILES_SERVICE_URL + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: FILES_SERVICE_URL + - name: BUCKET + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: BUCKET + - name: FILES_SERVICE_POSTS_UPLOAD_PATH + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: FILES_SERVICE_POSTS_UPLOAD_PATH + - name: FILES_SERVICE_CHUNKS_DIR + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: FILES_SERVICE_CHUNKS_DIR + - name: USERS_SERVICE_URL + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: USERS_SERVICE_URL + - name: POSTGRES_URL + valueFrom: + secretKeyRef: + name: yogram-posts-production-config-secret + key: POSTGRES_URL diff --git a/apps/users/Jenkinsfile b/apps/users/Jenkinsfile index c09a9c6a..e33f7d5a 100644 --- a/apps/users/Jenkinsfile +++ b/apps/users/Jenkinsfile @@ -3,13 +3,13 @@ pipeline { agent any environment { ENV_TYPE = "production" - PORT = 3870 + PORT = 4049 NAMESPACE = "yogram-ru" REGISTRY_HOSTNAME = "idogmat" - PROJECT = "users-yogram" + PROJECT = "yogram-users" SERVICE="users" REGISTRY = "registry.hub.docker.com" - DEPLOYMENT_NAME = "users-yogram-deployment" + DEPLOYMENT_NAME = "yogram-users-deployment" IMAGE_NAME = "${env.BUILD_ID}_${env.ENV_TYPE}_${env.GIT_COMMIT}" DOCKER_BUILD_NAME = "${env.REGISTRY_HOSTNAME}/${env.PROJECT}:${env.IMAGE_NAME}" } diff --git a/apps/users/deployment.yaml b/apps/users/deployment.yaml index ba25cb30..20fc4abd 100644 --- a/apps/users/deployment.yaml +++ b/apps/users/deployment.yaml @@ -22,63 +22,33 @@ spec: - containerPort: PORT_CONTAINER env: - - name: POSTGRES_TYPE - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: POSTGRES_TYPE - - name: POSTGRES_MIGRATION_TABLE - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: POSTGRES_MIGRATION_TABLE - - name: SYNCHRONIZE - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: SYNCHRONIZE - - name: AUTOLOAD_ENTITIES - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: AUTOLOAD_ENTITIES - - name: DROP_SCHEMA - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: DROP_SCHEMA - - name: USERS_PROD_SERVICE_URL - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: USERS_PROD_SERVICE_URL - name: RMQ_URL valueFrom: secretKeyRef: - name: users-yogram-production-config-secret + name: yogram-users-production-config-secret key: RMQ_URL - - name: FILES_SERVICE_URL - valueFrom: - secretKeyRef: - name: users-yogram-production-config-secret - key: FILES_SERVICE_URL - name: BUCKET valueFrom: secretKeyRef: - name: users-yogram-production-config-secret + name: yogram-users-production-config-secret key: BUCKET + - name: FILES_SERVICE_URL + valueFrom: + secretKeyRef: + name: yogram-users-production-config-secret + key: FILES_SERVICE_URL - name: FILES_SERVICE_AVATAR_UPLOAD_PATH valueFrom: secretKeyRef: - name: users-yogram-production-config-secret + name: yogram-users-production-config-secret key: FILES_SERVICE_AVATAR_UPLOAD_PATH - name: FILES_SERVICE_CHUNKS_DIR valueFrom: secretKeyRef: - name: users-yogram-production-config-secret + name: yogram-users-production-config-secret key: FILES_SERVICE_CHUNKS_DIR - name: POSTGRES_URL valueFrom: secretKeyRef: - name: users-yogram-production-config-secret + name: yogram-users-production-config-secret key: POSTGRES_URL diff --git a/apps/users/src/db/1759504948061-InitTable.ts b/apps/users/src/db/1761615094093-users_dev.ts similarity index 75% rename from apps/users/src/db/1759504948061-InitTable.ts rename to apps/users/src/db/1761615094093-users_dev.ts index 5dfd6887..fd90d536 100644 --- a/apps/users/src/db/1759504948061-InitTable.ts +++ b/apps/users/src/db/1761615094093-users_dev.ts @@ -1,6 +1,6 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class InitTable1759504948061 implements MigrationInterface { +export class UsersDev1761615094093 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { } diff --git a/apps/users/src/db/migrations/1759504952602-InitTable.ts b/apps/users/src/db/migrations/1761615103355-users_dev.ts similarity index 97% rename from apps/users/src/db/migrations/1759504952602-InitTable.ts rename to apps/users/src/db/migrations/1761615103355-users_dev.ts index 62f791df..551caabc 100644 --- a/apps/users/src/db/migrations/1759504952602-InitTable.ts +++ b/apps/users/src/db/migrations/1761615103355-users_dev.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class InitTable1759504952602 implements MigrationInterface { - name = 'InitTable1759504952602' +export class UsersDev1761615103355 implements MigrationInterface { + name = 'UsersDev1761615103355' public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` diff --git a/apps/users/src/users.controller.ts b/apps/users/src/users.controller.ts index 89b2f304..3a9ef8e7 100644 --- a/apps/users/src/users.controller.ts +++ b/apps/users/src/users.controller.ts @@ -102,6 +102,7 @@ export class UsersController { @Get('users/login/:email') async userLogin(@Param() email: string): Promise { + console.log('🚀 ~ UsersController ~ userLogin ~ email:', email); return await this.queryBus.execute(new UserLoginQuery(email['email'])); } diff --git a/package.json b/package.json index 60e89671..45922551 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,16 @@ "start:users": "cross-env NODE_ENV=PRODUCTION node dist/apps/users/main", "start:dev:users": "cross-env NODE_ENV=DEVELOPMENT nest start users --watch", "build:mailer": "nest build mailer", - "start:mailer": "node dist/apps/mailer/main", + "start:mailer": "cross-env NODE_ENV=PRODUCTION node dist/apps/mailer/main", "start:dev:mailer": "cross-env NODE_ENV=DEVELOPMENT nest start mailer --watch", "build:posts": "nest build posts", - "start:posts": "node dist/apps/posts/main", + "start:posts": "cross-env NODE_ENV=PRODUCTION node dist/apps/posts/main", "start:dev:posts": "cross-env NODE_ENV=DEVELOPMENT nest start posts --watch", "build:files": "nest build files", - "start:files": "node dist/apps/files/main", + "start:files": "cross-env NODE_ENV=PRODUCTION node dist/apps/files/main", "start:dev:files": "cross-env NODE_ENV=DEVELOPMENT nest start files --watch", "build:business": "nest build business", - "start:business": "node dist/apps/business/main", + "start:business": "cross-env NODE_ENV=PRODUCTION node dist/apps/business/main", "start:dev:business": "cross-env NODE_ENV=DEVELOPMENT nest start business --watch", "migration": "cross-env node scripts/typeorm-migration.js", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", @@ -63,6 +63,7 @@ "@nestjs/microservices": "^11.1.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^11.1.6", + "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^11.1.5", "@nestjs/typeorm": "^11.0.0", "@nestjs/websockets": "^11.1.6", @@ -95,6 +96,7 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.34.2", + "socket.io-client": "^4.8.1", "typeorm": "^0.3.23", "uuid": "^11.1.0", "uuidv4": "^6.2.13" diff --git a/tsconfig.json b/tsconfig.json index 59bc2e8b..ff60c275 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,4 +30,4 @@ // "@files/*": ["apps/files/*"] } } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index f0695ab4..dbd62400 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2092,6 +2092,13 @@ socket.io "4.8.1" tslib "2.8.1" +"@nestjs/schedule@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-6.1.0.tgz#4653383c6aaf82e19754eae281ec1036da41febe" + integrity sha512-W25Ydc933Gzb1/oo7+bWzzDiOissE+h/dhIAPugA39b9MuIzBbLybuXpc1AjoQLczO3v0ldmxaffVl87W0uqoQ== + dependencies: + cron "4.3.5" + "@nestjs/schematics@^10.0.0", "@nestjs/schematics@^10.0.1": version "10.2.3" resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-10.2.3.tgz#6053f43c5065b9e825cd08c4db1bf6bcbc9a6a62" @@ -3046,6 +3053,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/luxon@~3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.7.1.tgz#ef51b960ff86801e4e2de80c68813a96e529d531" + integrity sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg== + "@types/methods@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" @@ -4466,6 +4478,14 @@ cron-parser@^4.9.0: dependencies: luxon "^3.2.1" +cron@4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/cron/-/cron-4.3.5.tgz#53112ec0f5260722b89633ed72f005dc782864f1" + integrity sha512-hKPP7fq1+OfyCqoePkKfVq7tNAdFwiQORr4lZUHwrf0tebC65fYEeWgOrXOL6prn1/fegGOdTfrM6e34PJfksg== + dependencies: + "@types/luxon" "~3.7.0" + luxon "~3.7.0" + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -4883,6 +4903,17 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" +engine.io-client@~6.6.1: + version "6.6.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.3.tgz#815393fa24f30b8e6afa8f77ccca2f28146be6de" + integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.1.1" + engine.io-parser@~5.2.1: version "5.2.3" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" @@ -7163,7 +7194,7 @@ lru-cache@^7.10.1, lru-cache@^7.14.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -luxon@^3.2.1: +luxon@^3.2.1, luxon@~3.7.0: version "3.7.2" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== @@ -9161,6 +9192,16 @@ socket.io-adapter@~2.5.2: debug "~4.3.4" ws "~8.17.1" +socket.io-client@^4.8.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb" + integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.6.1" + socket.io-parser "~4.2.4" + socket.io-parser@~4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" @@ -10150,6 +10191,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlhttprequest-ssl@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23" + integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ== + xss@^1.0.8: version "1.0.15" resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.15.tgz#96a0e13886f0661063028b410ed1b18670f4e59a"