diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..29888c3d3 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# SubOS Configuration +NEXT_PUBLIC_PAYMENT_GATEWAY_URL= +NEXT_PUBLIC_SUBOS_PROJECT_ID= +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= + +# App Configuration +NEXT_PUBLIC_APP_NAME=Impler.io +NEXT_PUBLIC_APP_VERSION=1.0.0 + +# Environment is automatically set by NODE_ENV +# NODE_ENV=development diff --git a/apps/api/package.json b/apps/api/package.json index 132220b43..d9761138e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@impler/api", - "version": "1.8.1", + "version": "1.9.0", "author": "implerhq", "license": "MIT", "private": true, diff --git a/apps/api/src/app/column/dtos/column-request.dto.ts b/apps/api/src/app/column/dtos/column-request.dto.ts index a373fa4a2..050b9ad64 100644 --- a/apps/api/src/app/column/dtos/column-request.dto.ts +++ b/apps/api/src/app/column/dtos/column-request.dto.ts @@ -17,7 +17,7 @@ import { ValidationTypesEnum } from '@impler/client'; import { IsValidRegex } from '@shared/framework/is-valid-regex.validator'; import { IsValidDigitsConstraint } from '@shared/framework/is-valid-digits.validator'; import { IsNumberOrString } from '@shared/framework/number-or-string.validator'; -import { ColumnDelimiterEnum, ColumnTypesEnum, Defaults } from '@impler/shared'; +import { ColumnDelimiterEnum, ColumnTypesEnum } from '@impler/shared'; export class ValidationDto { @ApiProperty({ @@ -154,7 +154,7 @@ export class ColumnRequestDto { }) @ValidateIf((object) => object.type === ColumnTypesEnum.DATE) @Type(() => Array) - dateFormats: string[] = Defaults.DATE_FORMATS; + dateFormats: string[]; @ApiProperty({ description: 'Sequence of column', diff --git a/apps/api/src/app/common/common.controller.ts b/apps/api/src/app/common/common.controller.ts index d2dd0be93..cdf676844 100644 --- a/apps/api/src/app/common/common.controller.ts +++ b/apps/api/src/app/common/common.controller.ts @@ -12,9 +12,9 @@ import { UploadedFile, } from '@nestjs/common'; -import { ACCESS_KEY_NAME } from '@impler/shared'; +import { ACCESS_KEY_NAME, IImportConfig } from '@impler/shared'; import { JwtAuthGuard } from '@shared/framework/auth.gaurd'; -import { ValidRequestDto, SignedUrlDto, ImportConfigResponseDto } from './dtos'; +import { ValidRequestDto, SignedUrlDto } from './dtos'; import { ValidImportFile } from '@shared/validations/valid-import-file.validation'; import { ValidRequestCommand, GetSignedUrl, ValidRequest, GetImportConfig, GetSheetNames } from './usecases'; @@ -60,7 +60,7 @@ export class CommonController { async getImportConfigRoute( @Query('projectId') projectId: string, @Query('templateId') templateId: string - ): Promise { + ): Promise { if (!projectId) { throw new BadRequestException(); } diff --git a/apps/api/src/app/common/dtos/Schema.dto.ts b/apps/api/src/app/common/dtos/Schema.dto.ts index 8066450f3..aa0f821e0 100644 --- a/apps/api/src/app/common/dtos/Schema.dto.ts +++ b/apps/api/src/app/common/dtos/Schema.dto.ts @@ -13,7 +13,7 @@ import { ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; -import { ColumnTypesEnum, Defaults } from '@impler/shared'; +import { ColumnTypesEnum } from '@impler/shared'; import { IsValidRegex } from '@shared/framework/is-valid-regex.validator'; import { ValidationDto } from 'app/column/dtos/column-request.dto'; @@ -101,7 +101,7 @@ export class SchemaDto { @Type(() => Array) @IsArray({ message: "'dateFormats' must be an array, when type is Date" }) @ArrayMinSize(1, { message: "'dateFormats' must not be empty, when type is Date" }) - dateFormats: string[] = Defaults.DATE_FORMATS; + dateFormats: string[]; @ApiProperty({ description: 'Sequence of column', diff --git a/apps/api/src/app/common/dtos/import-config-response.dto.ts b/apps/api/src/app/common/dtos/import-config-response.dto.ts deleted file mode 100644 index 8639a15fb..000000000 --- a/apps/api/src/app/common/dtos/import-config-response.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsDefined, IsString } from 'class-validator'; - -export class ImportConfigResponseDto { - @ApiPropertyOptional({ - description: 'Whether to show branding', - }) - @IsDefined() - @IsBoolean() - showBranding: boolean; - - @ApiPropertyOptional({ - description: 'Whether the current Import is Manual is Atomatic', - }) - @IsDefined() - @IsString() - mode?: string; -} diff --git a/apps/api/src/app/common/dtos/index.ts b/apps/api/src/app/common/dtos/index.ts index 1ca28b7c1..d193f3f5d 100644 --- a/apps/api/src/app/common/dtos/index.ts +++ b/apps/api/src/app/common/dtos/index.ts @@ -1,4 +1,3 @@ export { SignedUrlDto } from './signed-url.dto'; export { ValidRequestDto } from './valid.dto'; export { SheetNamesDto } from './sheet-names.dto'; -export { ImportConfigResponseDto } from './import-config-response.dto'; diff --git a/apps/api/src/app/common/usecases/get-import-config/get-import-config.usecase.ts b/apps/api/src/app/common/usecases/get-import-config/get-import-config.usecase.ts index cf33a891f..40a8ecfe5 100644 --- a/apps/api/src/app/common/usecases/get-import-config/get-import-config.usecase.ts +++ b/apps/api/src/app/common/usecases/get-import-config/get-import-config.usecase.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { UserRepository, TemplateRepository, TemplateEntity } from '@impler/dal'; -import { AVAILABLE_BILLABLEMETRIC_CODE_ENUM, IImportConfig } from '@impler/shared'; +import { BILLABLEMETRIC_CODE_ENUM, IImportConfig } from '@impler/shared'; import { PaymentAPIService } from '@impler/services'; import { APIMessages } from '@shared/constants'; @@ -14,12 +14,22 @@ export class GetImportConfig { async execute(projectId: string, templateId?: string): Promise { const userEmail = await this.userRepository.findUserEmailFromProjectId(projectId); + const isFeatureAvailableMap = new Map(); - const removeBrandingAvailable = await this.paymentAPIService.checkEvent({ - email: userEmail, - billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.REMOVE_BRANDING, + Object.values(BILLABLEMETRIC_CODE_ENUM).forEach((code) => { + isFeatureAvailableMap.set(code, false); }); + for (const billableMetricCode of Object.values(BILLABLEMETRIC_CODE_ENUM)) { + try { + const isAvailable = await this.paymentAPIService.checkEvent({ + email: userEmail, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM[billableMetricCode], + }); + isFeatureAvailableMap.set(billableMetricCode, isAvailable); + } catch (error) {} + } + let template: TemplateEntity; if (templateId) { template = await this.templateRepository.findOne({ @@ -32,6 +42,11 @@ export class GetImportConfig { } } - return { showBranding: !removeBrandingAvailable, mode: template?.mode, title: template?.name }; + return { + ...Object.fromEntries(isFeatureAvailableMap), + showBranding: !isFeatureAvailableMap.get(BILLABLEMETRIC_CODE_ENUM.REMOVE_BRANDING), + mode: template?.mode, + title: template?.name, + }; } } diff --git a/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts b/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts index 3cf01f80a..cc1b88ae5 100644 --- a/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts +++ b/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts @@ -8,7 +8,7 @@ import { PaymentAPIService } from '@impler/services'; import { ValidRequestCommand } from './valid-request.command'; import { ProjectRepository, TemplateRepository, UserEntity } from '@impler/dal'; import { UniqueColumnException } from '@shared/exceptions/unique-column.exception'; -import { AVAILABLE_BILLABLEMETRIC_CODE_ENUM, ColumnTypesEnum } from '@impler/shared'; +import { BILLABLEMETRIC_CODE_ENUM, ColumnTypesEnum } from '@impler/shared'; import { DocumentNotFoundException } from '@shared/exceptions/document-not-found.exception'; @Injectable() @@ -98,7 +98,7 @@ export class ValidRequest { if (hasImageColumns && email) { const imageImportAvailable = await this.paymentAPIService.checkEvent({ email, - billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.IMAGE_IMPORT, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.IMAGE_IMPORT, }); if (!imageImportAvailable) { @@ -108,7 +108,7 @@ export class ValidRequest { if (hasValidations && email) { const validationsAvailable = await this.paymentAPIService.checkEvent({ email, - billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.ADVANCED_VALIDATORS, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.ADVANCED_VALIDATORS, }); if (!validationsAvailable) { diff --git a/apps/api/src/app/mapping/usecases/index.ts b/apps/api/src/app/mapping/usecases/index.ts index b1bd26415..3886f8125 100644 --- a/apps/api/src/app/mapping/usecases/index.ts +++ b/apps/api/src/app/mapping/usecases/index.ts @@ -8,6 +8,7 @@ import { ReanameFileHeadings } from './rename-file-headings/rename-file-headings import { DoMappingCommand } from './do-mapping/do-mapping.command'; import { ValidateMappingCommand } from './validate-mapping/validate-mapping.command'; +import { PaymentAPIService } from '@impler/services'; export const USE_CASES = [ DoMapping, @@ -17,9 +18,19 @@ export const USE_CASES = [ ValidateMapping, ReanameFileHeadings, GetUpload, + PaymentAPIService, // ]; -export { DoMapping, ValidateMapping, GetMappings, UpdateMappings, FinalizeUpload, ReanameFileHeadings, GetUpload }; +export { + DoMapping, + ValidateMapping, + GetMappings, + UpdateMappings, + FinalizeUpload, + ReanameFileHeadings, + GetUpload, + PaymentAPIService, +}; export { DoMappingCommand, ValidateMappingCommand }; diff --git a/apps/api/src/app/project/usecases/create-project/create-project.command.ts b/apps/api/src/app/project/usecases/create-project/create-project.command.ts index 188026f5c..1ff8068ec 100644 --- a/apps/api/src/app/project/usecases/create-project/create-project.command.ts +++ b/apps/api/src/app/project/usecases/create-project/create-project.command.ts @@ -9,4 +9,8 @@ export class CreateProjectCommand extends AuthenticatedCommand { @IsBoolean() @IsOptional() onboarding?: boolean; + + @IsBoolean() + @IsOptional() + autoGenerated?: boolean; } diff --git a/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts b/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts index 90b3310c6..ffe0cfb5a 100644 --- a/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts +++ b/apps/api/src/app/review/usecases/do-review/do-review.usecase.ts @@ -1,9 +1,10 @@ +/* eslint-disable multiline-comment-style */ import { Model } from 'mongoose'; import { Writable } from 'stream'; import { Injectable, BadRequestException, InternalServerErrorException } from '@nestjs/common'; import { APIMessages } from '@shared/constants'; -import { EMAIL_SUBJECT } from '@impler/shared'; +import { BILLABLEMETRIC_CODE_ENUM, EMAIL_SUBJECT } from '@impler/shared'; import { BaseReview } from './base-review.usecase'; import { UniqueWithValidationType, ValidationTypesEnum } from '@impler/client'; import { BATCH_LIMIT } from '@shared/services/sandbox'; @@ -16,7 +17,9 @@ import { DalService, TemplateEntity, TemplateRepository, + EnvironmentRepository, } from '@impler/dal'; +import { UsageLimitExceededException } from '@shared/exceptions/import-limit-exceeded.exception'; interface ISaveResults { uploadId: string; @@ -32,6 +35,7 @@ export class DoReview extends BaseReview { constructor( private templateRepository: TemplateRepository, + private environmentRepository: EnvironmentRepository, private storageService: StorageService, private uploadRepository: UploadRepository, private validatorRepository: ValidatorRepository, @@ -44,6 +48,7 @@ export class DoReview extends BaseReview { } async execute(_uploadId: string) { + console.log('Called The Do Review.execute'); this._modal = this.dalService.getRecordCollection(_uploadId); const userEmail = await this.uploadRepository.getUserEmailFromUploadId(_uploadId); @@ -205,7 +210,28 @@ export class DoReview extends BaseReview { throw new InternalServerErrorException(APIMessages.ERROR_DURING_VALIDATION); } - await this.saveResults(response); + try { + await this.saveResults(response); + } catch (error) { + const emailContents = this.emailService.getEmailContent({ + type: 'IMPORT_LIMIT_EXCEEDED_EMAIL', + data: { + limitType: 'Import Rows', + currentUsage: 'All available units including grace percentage', + planName: 'Current Plan', + upgradeUrl: `${process.env.WEB_BASE_URL}/pricing`, + }, + }); + await this.emailService.sendEmail({ + from: process.env.EMAIL_FROM, + html: emailContents, + subject: 'Usage limit exceeded', + to: userEmail, + senderName: process.env.EMAIL_FROM_NAME, + }); + + throw new UsageLimitExceededException(error.message); + } return response; } @@ -235,32 +261,65 @@ export class DoReview extends BaseReview { } private async saveResults({ uploadId, totalRecords, validRecords, invalidRecords, _templateId }: ISaveResults) { - await this.uploadRepository.update( - { _id: uploadId }, - { - status: UploadStatusEnum.REVIEWING, - totalRecords, - validRecords, - invalidRecords, - } - ); - await this.templateRepository.findOneAndUpdate( - { - _id: _templateId, - }, - { - $inc: { - totalUploads: 1, - totalRecords: totalRecords, - totalInvalidRecords: invalidRecords, - }, - } - ); const userExternalIdOrEmail = await this.uploadRepository.getUserEmailFromUploadId(uploadId); - await this.paymentAPIService.createEvent( - { uploadId, totalRecords, validRecords, invalidRecords }, - userExternalIdOrEmail - ); + try { + await this.paymentAPIService.createEvent( + { units: totalRecords, billableMetricCode: BILLABLEMETRIC_CODE_ENUM.ROWS }, + userExternalIdOrEmail + ); + + // Only update database if payment event creation succeeds + await this.uploadRepository.update( + { _id: uploadId }, + { + status: UploadStatusEnum.REVIEWING, + totalRecords, + validRecords, + invalidRecords, + } + ); + + await this.templateRepository.findOneAndUpdate( + { + _id: _templateId, + }, + { + $inc: { + totalUploads: 1, + totalRecords: totalRecords, + totalInvalidRecords: invalidRecords, + }, + } + ); + } catch (error) { + const template = await this.templateRepository.findById(_templateId); + const environment = await this.environmentRepository.getProjectTeamMembers(template._projectId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const teamMemberEmails = environment.map((teamMember) => teamMember._userId.email); + + const emailContents = this.emailService.getEmailContent({ + type: 'IMPORT_LIMIT_EXCEEDED_EMAIL', + data: { + limitType: 'Import Rows', + currentUsage: 'All available units including grace percentage', + planName: 'Current Plan', + upgradeUrl: `${process.env.WEB_BASE_URL}/pricing`, + }, + }); + + teamMemberEmails.forEach(async (email) => { + await this.emailService.sendEmail({ + to: email, + subject: EMAIL_SUBJECT.IMPORT_LIMIT_EXCEEDED, + html: emailContents, + from: process.env.ALERT_EMAIL_FROM, + senderName: process.env.EMAIL_FROM_NAME, + }); + }); + + throw new UsageLimitExceededException(error.message); + } } } diff --git a/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts b/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts index 016b833fd..4f5f44c8a 100644 --- a/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts +++ b/apps/api/src/app/review/usecases/start-process/start-process.usecase.ts @@ -7,11 +7,20 @@ import { replaceVariablesInObject, DestinationsEnum, ColumnDelimiterEnum, + EMAIL_SUBJECT, + BILLABLEMETRIC_CODE_ENUM, } from '@impler/shared'; -import { PaymentAPIService } from '@impler/services'; +import { PaymentAPIService, EmailService } from '@impler/services'; import { QueueService } from '@shared/services/queue.service'; import { AmplitudeService } from '@shared/services/amplitude.service'; -import { DalService, TemplateEntity, TemplateRepository, UploadEntity, UploadRepository } from '@impler/dal'; +import { + DalService, + TemplateEntity, + TemplateRepository, + UploadEntity, + UploadRepository, + EnvironmentRepository, +} from '@impler/dal'; import { MaxRecordsExceededException } from '@shared/exceptions/max-records.exception'; @Injectable() @@ -22,7 +31,9 @@ export class StartProcess { private uploadRepository: UploadRepository, private amplitudeService: AmplitudeService, private paymentAPIService: PaymentAPIService, - private templateRepository: TemplateRepository + private templateRepository: TemplateRepository, + private environmentRepository: EnvironmentRepository, + private emailService: EmailService ) {} async execute(_uploadId: string, maxRecords?: number) { @@ -141,7 +152,6 @@ export class StartProcess { // Validate that we're not updating with negative values const recordsToAdd = Math.max(0, uploadInfo.totalRecords || 0); - const invalidRecordsToAdd = Math.max(0, uploadInfo.invalidRecords || 0); const validRecordsToAdd = Math.max(0, uploadInfo.validRecords || 0); // Only update if we have positive values to add @@ -160,17 +170,43 @@ export class StartProcess { ); } - // Only create payment event if we have valid records if (validRecordsToAdd > 0 || recordsToAdd > 0) { - await this.paymentAPIService.createEvent( - { - uploadId: uploadInfo._id, - totalRecords: recordsToAdd, - validRecords: validRecordsToAdd, - invalidRecords: invalidRecordsToAdd, - }, - userEmail - ); + try { + await this.paymentAPIService.createEvent( + { + units: recordsToAdd, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.ROWS, + }, + userEmail + ); + } catch (error) { + const template = await this.templateRepository.findById(uploadInfo._templateId); + const projectId = template._projectId; + const environment = await this.environmentRepository.getProjectTeamMembers(projectId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const teamMemberEmails = environment.map((teamMember) => teamMember._userId.email); + + const emailContents = this.emailService.getEmailContent({ + type: 'IMPORT_LIMIT_EXCEEDED_EMAIL', + data: { + limitType: 'Import Rows', + currentUsage: 'All available units including grace percentage', + planName: 'Current Plan', + upgradeUrl: `${process.env.WEB_BASE_URL}/pricing`, + }, + }); + + teamMemberEmails.forEach(async (email) => { + await this.emailService.sendEmail({ + to: email, + subject: EMAIL_SUBJECT.IMPORT_LIMIT_EXCEEDED, + html: emailContents, + from: process.env.ALERT_EMAIL_FROM, + senderName: process.env.EMAIL_FROM_NAME, + }); + }); + } } } } diff --git a/apps/api/src/app/shared/exceptions/import-limit-exceeded.exception.ts b/apps/api/src/app/shared/exceptions/import-limit-exceeded.exception.ts new file mode 100644 index 000000000..bd05365d5 --- /dev/null +++ b/apps/api/src/app/shared/exceptions/import-limit-exceeded.exception.ts @@ -0,0 +1,7 @@ +import { BadRequestException } from '@nestjs/common'; + +export class UsageLimitExceededException extends BadRequestException { + constructor(message?: string) { + super(message || 'You have exceeded your usage limit.'); + } +} diff --git a/apps/api/src/app/shared/exceptions/max-records.exception.ts b/apps/api/src/app/shared/exceptions/max-records.exception.ts index 0efd8e873..0b42416c7 100644 --- a/apps/api/src/app/shared/exceptions/max-records.exception.ts +++ b/apps/api/src/app/shared/exceptions/max-records.exception.ts @@ -3,6 +3,6 @@ import { numberFormatter } from '@impler/shared'; export class MaxRecordsExceededException extends BadRequestException { constructor({ maxAllowed, customMessage = null }: { maxAllowed: number; customMessage?: string }) { - super(customMessage || `You can nor import records more than ${numberFormatter(maxAllowed)} records.`); + super(customMessage || `You can not import more than ${numberFormatter(maxAllowed)} records.`); } } diff --git a/apps/api/src/app/shared/exceptions/user-verification.exception.ts b/apps/api/src/app/shared/exceptions/user-verification.exception.ts new file mode 100644 index 000000000..d8cb97174 --- /dev/null +++ b/apps/api/src/app/shared/exceptions/user-verification.exception.ts @@ -0,0 +1,15 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { APIMessages } from '../constants'; + +export class InvalidVerificationCodeException extends HttpException { + constructor() { + super( + { + message: APIMessages.INVALID_VERIFICATION_CODE, + error: 'InvalidVerificationCode', + statusCode: HttpStatus.BAD_REQUEST, + }, + HttpStatus.BAD_REQUEST + ); + } +} diff --git a/apps/api/src/app/team/usecase/invite/invite.usecase.ts b/apps/api/src/app/team/usecase/invite/invite.usecase.ts index 204aa7f43..94349c58b 100644 --- a/apps/api/src/app/team/usecase/invite/invite.usecase.ts +++ b/apps/api/src/app/team/usecase/invite/invite.usecase.ts @@ -1,7 +1,7 @@ import { randomBytes } from 'crypto'; import { Injectable, BadRequestException } from '@nestjs/common'; -import { EmailService } from '@impler/services'; -import { EMAIL_SUBJECT } from '@impler/shared'; +import { EmailService, PaymentAPIService } from '@impler/services'; +import { BILLABLEMETRIC_CODE_ENUM, EMAIL_SUBJECT } from '@impler/shared'; import { ProjectInvitationRepository, EnvironmentRepository } from '@impler/dal'; import { InviteCommand } from './invite.command'; @@ -11,65 +11,85 @@ export class Invite { constructor( private emailService: EmailService, private projectInvitationRepository: ProjectInvitationRepository, - private environmentRepository: EnvironmentRepository + private environmentRepository: EnvironmentRepository, + private paymentAPIService: PaymentAPIService ) {} async exec(command: InviteCommand) { - const existingInvitationsCount = await this.projectInvitationRepository.count({ - _projectId: command.projectId, - invitedBy: command.invitatedBy, - }); + try { + /* + * const subscription = await this.paymentAPIService.fetchActiveSubscription(command.invitedBy); + * const allocated = subscription.meta.TEAM_MEMBERS; + */ - const totalInvitationsCount = existingInvitationsCount + command.invitationEmailsTo.length; - if (totalInvitationsCount > 4) { - throw new BadRequestException( - 'You cannot invite more than 4 emails at a time, including already sent invitations.' - ); - } - - const teamMembers = await this.environmentRepository.getProjectTeamMembers(command.projectId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const memberEmails = teamMembers.map((teamMember) => teamMember._userId.email); - - for (const invitationEmailTo of command.invitationEmailsTo) { - if (memberEmails.includes(invitationEmailTo)) { - throw new BadRequestException(`The email ${invitationEmailTo} is already a member of the project.`); - } - - const existingInvitation = await this.projectInvitationRepository.findOne({ - invitationToEmail: invitationEmailTo, + const existingInvitationsCount = await this.projectInvitationRepository.count({ _projectId: command.projectId, + invitedBy: command.invitatedBy, }); - if (existingInvitation) { - throw new BadRequestException(`The email ${invitationEmailTo} has already been invited.`); + const totalInvitationsCount = existingInvitationsCount + command.invitationEmailsTo.length; + if (totalInvitationsCount > 4) { + throw new BadRequestException( + 'You cannot invite more than 4 emails at a time, including already sent invitations.' + ); } - const invitation = await this.projectInvitationRepository.create({ - invitationToEmail: invitationEmailTo, - invitedOn: new Date().toDateString(), - role: command.role, - invitedBy: command.invitatedBy, - _projectId: command.projectId, - token: randomBytes(16).toString('hex'), - }); + const teamMembers = await this.environmentRepository.getProjectTeamMembers(command.projectId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const memberEmails = teamMembers.map((teamMember) => teamMember._userId.email); - const emailContents = this.emailService.getEmailContent({ - type: 'TEAM_INVITATION_EMAIL', - data: { + for (const invitationEmailTo of command.invitationEmailsTo) { + if (memberEmails.includes(invitationEmailTo)) { + throw new BadRequestException(`The email ${invitationEmailTo} is already a member of the project.`); + } + + const existingInvitation = await this.projectInvitationRepository.findOne({ + invitationToEmail: invitationEmailTo, + _projectId: command.projectId, + }); + + if (existingInvitation) { + throw new BadRequestException(`The email ${invitationEmailTo} has already been invited.`); + } + + const invitation = await this.projectInvitationRepository.create({ + invitationToEmail: invitationEmailTo, + invitedOn: new Date().toDateString(), + role: command.role, invitedBy: command.invitatedBy, - projectName: command.projectName, - invitationUrl: `${process.env.WEB_BASE_URL}/auth/invitation/${invitation._id}`, - }, - }); - await this.emailService.sendEmail({ - to: invitationEmailTo, - subject: `${EMAIL_SUBJECT.PROJECT_INVITATION} ${command.projectName}`, - html: emailContents, - from: process.env.EMAIL_FROM, - senderName: process.env.EMAIL_FROM_NAME, - }); + _projectId: command.projectId, + token: randomBytes(16).toString('hex'), + }); + + const emailContents = this.emailService.getEmailContent({ + type: 'TEAM_INVITATION_EMAIL', + data: { + invitedBy: command.invitatedBy, + projectName: command.projectName, + invitationUrl: `${process.env.WEB_BASE_URL}/auth/invitation/${invitation._id}`, + }, + }); + const sentEmail = await this.emailService.sendEmail({ + to: invitationEmailTo, + subject: `${EMAIL_SUBJECT.PROJECT_INVITATION} ${command.projectName}`, + html: emailContents, + from: process.env.EMAIL_FROM, + senderName: process.env.EMAIL_FROM_NAME, + }); + + if (sentEmail) { + await this.paymentAPIService.createEvent( + { + units: 1, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.TEAM_MEMBERS, + }, + command.invitatedBy + ); + } + } + } catch (error) { + throw new BadRequestException(error); } } } diff --git a/apps/api/src/app/template/usecases/update-template-columns/update-template-columns.usecase.ts b/apps/api/src/app/template/usecases/update-template-columns/update-template-columns.usecase.ts index 2c97a8498..f7b3ead55 100644 --- a/apps/api/src/app/template/usecases/update-template-columns/update-template-columns.usecase.ts +++ b/apps/api/src/app/template/usecases/update-template-columns/update-template-columns.usecase.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { APIMessages } from '@shared/constants'; import { PaymentAPIService } from '@impler/services'; import { UpdateImageColumns, SaveSampleFile } from '@shared/usecases'; -import { AVAILABLE_BILLABLEMETRIC_CODE_ENUM, ColumnTypesEnum } from '@impler/shared'; +import { BILLABLEMETRIC_CODE_ENUM, ColumnTypesEnum } from '@impler/shared'; import { ColumnEntity, ColumnRepository, CustomizationRepository, TemplateRepository } from '@impler/dal'; import { AddColumnCommand } from 'app/column/commands/add-column.command'; import { UniqueColumnException } from '@shared/exceptions/unique-column.exception'; @@ -33,11 +33,11 @@ export class UpdateTemplateColumns { column.sequence = index; column.dateFormats = column.dateFormats?.map((format) => format.toUpperCase()) || []; - column.isRequired = existingUserColumns?.isRequired || false; - column.isUnique = existingUserColumns?.isUnique || false; - column.selectValues = existingUserColumns?.selectValues || []; - column.dateFormats = existingUserColumns?.dateFormats || []; - column.validations = existingUserColumns?.validations || []; + column.isRequired = existingUserColumns?.isRequired || column.isRequired || false; + column.isUnique = existingUserColumns?.isUnique || column.isUnique || false; + column.selectValues = column.selectValues || existingUserColumns?.selectValues || []; + column.dateFormats = existingUserColumns?.dateFormats || column.dateFormats || []; + column.validations = existingUserColumns?.validations || column.validations || []; }); const columns = await this.columnRepository.createMany(userColumns); await this.saveSampleFile.execute(columns, _templateId); @@ -76,13 +76,13 @@ export class UpdateTemplateColumns { if (hasImageColumns && email) { const imageImportAvailable = await this.paymentAPIService.checkEvent({ email, - billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.IMAGE_IMPORT, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.IMAGE_IMPORT, }); if (!imageImportAvailable) { throw new DocumentNotFoundException( 'Schema', - AVAILABLE_BILLABLEMETRIC_CODE_ENUM.IMAGE_IMPORT, + BILLABLEMETRIC_CODE_ENUM.IMAGE_IMPORT, APIMessages.FEATURE_UNAVAILABLE.IMAGE_IMPORT ); } @@ -90,13 +90,13 @@ export class UpdateTemplateColumns { if (hasValidations && email) { const validationsAvailable = await this.paymentAPIService.checkEvent({ email, - billableMetricCode: AVAILABLE_BILLABLEMETRIC_CODE_ENUM.ADVANCED_VALIDATORS, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.ADVANCED_VALIDATORS, }); if (!validationsAvailable) { throw new DocumentNotFoundException( 'Schema', - AVAILABLE_BILLABLEMETRIC_CODE_ENUM.ADVANCED_VALIDATORS, + BILLABLEMETRIC_CODE_ENUM.ADVANCED_VALIDATORS, APIMessages.FEATURE_UNAVAILABLE.ADVANCED_VALIDATIONS ); } diff --git a/apps/api/src/app/upload/usecases/index.ts b/apps/api/src/app/upload/usecases/index.ts index b07f4ce80..83c201379 100644 --- a/apps/api/src/app/upload/usecases/index.ts +++ b/apps/api/src/app/upload/usecases/index.ts @@ -9,6 +9,7 @@ import { PaginateFileContent } from './paginate-file-content/paginate-file-conte import { GetOriginalFileContent } from './get-original-file-content/get-original-file-content.usecase'; import { GetUploadProcessInformation } from './get-upload-process-info/get-upload-process-info.usecase'; import { UploadCleanupSchedulerService } from './uploadcleanup-scheduler/uploadcleanup-scheduler.service'; +import { PaymentAPIService } from '@impler/services'; export const USE_CASES = [ GetPreviewRows, @@ -18,6 +19,7 @@ export const USE_CASES = [ TerminateUpload, MakeUploadEntry, GetUploadColumns, + PaymentAPIService, PaginateFileContent, GetOriginalFileContent, GetUploadProcessInformation, @@ -33,6 +35,7 @@ export { MakeUploadEntry, TerminateUpload, GetUploadColumns, + PaymentAPIService, GetOriginalFileContent, GetUploadProcessInformation, PaginateFileContent, diff --git a/apps/api/src/app/user/usecases/apply-coupon/apply-coupon.usecase.ts b/apps/api/src/app/user/usecases/apply-coupon/apply-coupon.usecase.ts deleted file mode 100644 index d0b12d35d..000000000 --- a/apps/api/src/app/user/usecases/apply-coupon/apply-coupon.usecase.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; - -@Injectable() -export class ApplyCoupon { - constructor(private paymentApiService: PaymentAPIService) {} - - async execute(couponCode: string, userEmail: string, planCode: string) { - try { - return await this.paymentApiService.checkAppliedCoupon(couponCode, userEmail, planCode); - } catch (error) { - if (error) { - throw new BadRequestException(error); - } else throw new InternalServerErrorException(); - } - } -} diff --git a/apps/api/src/app/user/usecases/cancel-subscription/cancel-subscription.usecase.ts b/apps/api/src/app/user/usecases/cancel-subscription/cancel-subscription.usecase.ts deleted file mode 100644 index 60a5e6d7c..000000000 --- a/apps/api/src/app/user/usecases/cancel-subscription/cancel-subscription.usecase.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as dayjs from 'dayjs'; -import { Injectable } from '@nestjs/common'; -import { DATE_FORMATS } from '@shared/constants'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class CancelSubscription { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - async execute(projectId: string, cancellationReasons: string[]) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - const canceledSubscription = await this.paymentApiService.cancelSubscription(teamOwner._userId.email); - - canceledSubscription.expiryDate = dayjs(canceledSubscription.expiryDate).format(DATE_FORMATS.COMMON); - - return canceledSubscription; - } -} diff --git a/apps/api/src/app/user/usecases/checkout/checkout.usecase.ts b/apps/api/src/app/user/usecases/checkout/checkout.usecase.ts deleted file mode 100644 index e5ad43bd2..000000000 --- a/apps/api/src/app/user/usecases/checkout/checkout.usecase.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class Checkout { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute({ - projectId, - paymentMethodId, - planCode, - couponCode, - }: { - projectId: string; - planCode: string; - paymentMethodId: string; - couponCode?: string; - }) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - - return this.paymentApiService.checkout({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - externalId: teamOwner._userId.email, - planCode: planCode, - paymentMethodId: paymentMethodId, - couponCode: couponCode, - }); - } -} diff --git a/apps/api/src/app/user/usecases/delete-user-payment-method/delete-user-payment-method.usecase.ts b/apps/api/src/app/user/usecases/delete-user-payment-method/delete-user-payment-method.usecase.ts deleted file mode 100644 index 4792ac391..000000000 --- a/apps/api/src/app/user/usecases/delete-user-payment-method/delete-user-payment-method.usecase.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; - -@Injectable() -export class DeleteUserPaymentMethod { - constructor(private paymentApiService: PaymentAPIService) {} - - async execute(paymentMethodId: string) { - return await this.paymentApiService.deleteUserPaymentMethod(paymentMethodId); - } -} diff --git a/apps/api/src/app/user/usecases/get-transaction-history/get-transaction-history.usecase.ts b/apps/api/src/app/user/usecases/get-transaction-history/get-transaction-history.usecase.ts deleted file mode 100644 index a0ae28e79..000000000 --- a/apps/api/src/app/user/usecases/get-transaction-history/get-transaction-history.usecase.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class GetTransactionHistory { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute(projectId: string) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - const transactions = await this.paymentApiService.getTransactionHistory(teamOwner._userId.email); - - return transactions.map((transactionItem) => ({ - transactionDate: transactionItem.transactionDate, - planName: transactionItem.planName, - transactionStatus: transactionItem.transactionStatus, - membershipDate: transactionItem.membershipDate ? transactionItem.membershipDate : undefined, - expiryDate: transactionItem.expiryDate ? transactionItem.expiryDate : undefined, - isPlanActive: transactionItem.isPlanActive, - charge: transactionItem.charge, - amount: transactionItem.amount, - currency: transactionItem.currency, - _id: transactionItem.id, - })); - } -} diff --git a/apps/api/src/app/user/usecases/index.ts b/apps/api/src/app/user/usecases/index.ts index 3044f49f8..4d6b019f7 100644 --- a/apps/api/src/app/user/usecases/index.ts +++ b/apps/api/src/app/user/usecases/index.ts @@ -1,44 +1,11 @@ import { GetImportCounts } from './get-import-count/get-import-count.usecase'; import { GetActiveSubscription } from './get-active-subscription/get-active-subscription.usecase'; -import { CancelSubscription } from './cancel-subscription/cancel-subscription.usecase'; -import { UpdatePaymentMethod } from './setup-payment-intent/setup-payment-intent.usecase'; -import { ConfirmIntentId } from './save-payment-intent-id/save-paymentintentid.usecase'; -import { RetrievePaymentMethods } from './retrive-payment-methods/retrive-payment-methods.usecase'; -import { DeleteUserPaymentMethod } from './delete-user-payment-method/delete-user-payment-method.usecase'; -import { GetTransactionHistory } from './get-transaction-history/get-transaction-history.usecase'; -import { ApplyCoupon } from './apply-coupon/apply-coupon.usecase'; -import { Checkout } from './checkout/checkout.usecase'; -import { Subscription } from './subscription/subscription.usecase'; -import { UpdateSubscriptionPaymentMethod } from './subscription/update-payment-method.usecase'; export const USE_CASES = [ GetImportCounts, - CancelSubscription, GetActiveSubscription, - UpdatePaymentMethod, - ConfirmIntentId, - RetrievePaymentMethods, - DeleteUserPaymentMethod, - GetTransactionHistory, - ApplyCoupon, - Checkout, - Subscription, - UpdatePaymentMethod, - UpdateSubscriptionPaymentMethod, + // ]; -export { - GetImportCounts, - CancelSubscription, - GetActiveSubscription, - UpdatePaymentMethod, - ConfirmIntentId, - RetrievePaymentMethods, - DeleteUserPaymentMethod, - GetTransactionHistory, - ApplyCoupon, - Checkout, - Subscription, - UpdateSubscriptionPaymentMethod, -}; +export { GetImportCounts, GetActiveSubscription }; diff --git a/apps/api/src/app/user/usecases/retrive-payment-methods/retrive-payment-methods.usecase.ts b/apps/api/src/app/user/usecases/retrive-payment-methods/retrive-payment-methods.usecase.ts deleted file mode 100644 index 6a23b527e..000000000 --- a/apps/api/src/app/user/usecases/retrive-payment-methods/retrive-payment-methods.usecase.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class RetrievePaymentMethods { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute(projectId: string) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - return await this.paymentApiService.retriveUserPaymentMethods(teamOwner._userId.email); - } -} diff --git a/apps/api/src/app/user/usecases/save-payment-intent-id/save-paymentintentid.usecase.ts b/apps/api/src/app/user/usecases/save-payment-intent-id/save-paymentintentid.usecase.ts deleted file mode 100644 index 261b6bb5b..000000000 --- a/apps/api/src/app/user/usecases/save-payment-intent-id/save-paymentintentid.usecase.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class ConfirmIntentId { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute(projectId: string, intentId: string) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - - return await this.paymentApiService.confirmPaymentIntentId(teamOwner._userId.email, intentId); - } -} diff --git a/apps/api/src/app/user/usecases/setup-payment-intent/setup-payment-intent.usecase.ts b/apps/api/src/app/user/usecases/setup-payment-intent/setup-payment-intent.usecase.ts deleted file mode 100644 index e89d856c0..000000000 --- a/apps/api/src/app/user/usecases/setup-payment-intent/setup-payment-intent.usecase.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class UpdatePaymentMethod { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute(projectId: string, paymentMethodId: string) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - - return await this.paymentApiService.updatePaymentMethod(teamOwner._userId.email, paymentMethodId); - } -} diff --git a/apps/api/src/app/user/usecases/subscription/subscription.usecase.ts b/apps/api/src/app/user/usecases/subscription/subscription.usecase.ts deleted file mode 100644 index 7d0ff6e18..000000000 --- a/apps/api/src/app/user/usecases/subscription/subscription.usecase.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PaymentAPIService } from '@impler/services'; -import { Injectable } from '@nestjs/common'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class Subscription { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute({ - projectId, - planCode, - selectedPaymentMethod, - couponCode, - }: { - projectId: string; - planCode: string; - selectedPaymentMethod: string; - couponCode?: string; - }) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - - return await this.paymentApiService.subscribe({ - planCode, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - email: teamOwner._userId.email, - selectedPaymentMethod, - couponCode, - }); - } -} diff --git a/apps/api/src/app/user/usecases/subscription/update-payment-method.usecase.ts b/apps/api/src/app/user/usecases/subscription/update-payment-method.usecase.ts deleted file mode 100644 index 2b59cc216..000000000 --- a/apps/api/src/app/user/usecases/subscription/update-payment-method.usecase.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PaymentAPIService } from '@impler/services'; -import { EnvironmentRepository } from '@impler/dal'; - -@Injectable() -export class UpdateSubscriptionPaymentMethod { - constructor( - private paymentApiService: PaymentAPIService, - private environmentRepository: EnvironmentRepository - ) {} - - async execute(projectId: string, paymentMethodId: string) { - const teamOwner = await this.environmentRepository.getTeamOwnerDetails(projectId); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - return this.paymentApiService.updatePaymentMethodId(teamOwner._userId.email, paymentMethodId); - } -} diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index a6cc000f5..10631c97e 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,24 +1,10 @@ import { ApiTags, ApiOperation, ApiSecurity } from '@nestjs/swagger'; -import { Body, Controller, Delete, Get, Param, Put, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; -import { - GetImportCounts, - CancelSubscription, - GetActiveSubscription, - UpdatePaymentMethod, - ConfirmIntentId, - DeleteUserPaymentMethod, - GetTransactionHistory, - ApplyCoupon, - Checkout, - Subscription, - RetrievePaymentMethods, - UpdateSubscriptionPaymentMethod, -} from './usecases'; +import { GetImportCounts, GetActiveSubscription } from './usecases'; import { JwtAuthGuard } from '@shared/framework/auth.gaurd'; import { IJwtPayload, ACCESS_KEY_NAME } from '@impler/shared'; import { UserSession } from '@shared/framework/user.decorator'; -import { CancelSubscriptionDto } from './dto/cancel-subscription.dto'; @ApiTags('User') @Controller('/user') @@ -26,18 +12,8 @@ import { CancelSubscriptionDto } from './dto/cancel-subscription.dto'; @ApiSecurity(ACCESS_KEY_NAME) export class UserController { constructor( - private checkout: Checkout, - private applyCoupon: ApplyCoupon, private getImportsCount: GetImportCounts, - private getActiveSubscription: GetActiveSubscription, - private cancelSubscription: CancelSubscription, - private updatePaymentMethod: UpdatePaymentMethod, - private confirmIntentId: ConfirmIntentId, - private getTransactionHistory: GetTransactionHistory, - private retrivePaymentMethods: RetrievePaymentMethods, - private deleteUserPaymentMethod: DeleteUserPaymentMethod, - private subscription: Subscription, - private updateSubscriptionPaymentMethod: UpdateSubscriptionPaymentMethod + private getActiveSubscription: GetActiveSubscription ) {} @Get('/import-count') @@ -64,123 +40,4 @@ export class UserController { async getActiveSubscriptionRoute(@UserSession() user: IJwtPayload) { return this.getActiveSubscription.execute(user._projectId); } - - @Delete('/subscription') - @ApiOperation({ - summary: 'Cancel active subscription for user', - }) - async cancelSubscriptionRoute( - @UserSession() user: IJwtPayload, - @Body() cancelSubscriptionDto: CancelSubscriptionDto - ) { - return this.cancelSubscription.execute(user._projectId, cancelSubscriptionDto.reasons); - } - - @Put('/setup-intent/:paymentMethodId') - @ApiOperation({ - summary: 'Setup User Payment Intent', - }) - async setupEmandateIntent(@UserSession() user: IJwtPayload, @Param('paymentMethodId') paymentMethodId: string) { - return this.updatePaymentMethod.execute(user._projectId, paymentMethodId); - } - - @Put('/payment-method/:paymentMethodId') - @ApiOperation({ - summary: 'Update User Payment Method', - }) - async updatePaymentMethodRoute(@UserSession() user: IJwtPayload, @Param('paymentMethodId') paymentMethodId: string) { - return this.updatePaymentMethod.execute(user._projectId, paymentMethodId); - } - - @Put('/subscription-payment-method/:paymentMethodId') - @ApiOperation({ - summary: 'Update User Payment Method', - }) - async updateSubscriptionPaymentMethodRoute( - @UserSession() user: IJwtPayload, - @Param('paymentMethodId') paymentMethodId: string - ) { - return this.updateSubscriptionPaymentMethod.execute(user._projectId, paymentMethodId); - } - - @Put('/confirm-payment-intent-id/:intentId') - @ApiOperation({ - summary: 'Pass the Payment Intent Id If user cancels the E-Mandate Authorization', - }) - async savePaymentIntentIdRoute(@UserSession() user: IJwtPayload, @Param('intentId') intentId: string) { - return this.confirmIntentId.execute(user._projectId, intentId); - } - - @Get('/payment-methods') - @ApiOperation({ - summary: 'Retrieve the cards of the User', - }) - async retriveUserPaymentMethods(@UserSession() user: IJwtPayload) { - return this.retrivePaymentMethods.execute(user._projectId); - } - - @Delete('/payment-methods/:paymentMethodId') - @ApiOperation({ - summary: 'Detach or Delete the card of the User', - }) - async deletePaymentMethodRoute(@Param('paymentMethodId') paymentMethodId: string) { - return this.deleteUserPaymentMethod.execute(paymentMethodId); - } - - @Get('/transactions/history') - @ApiOperation({ - summary: 'Get Transaction History for User', - }) - async getTransactionHistoryRoute(@UserSession() user: IJwtPayload) { - return this.getTransactionHistory.execute(user._projectId); - } - - @Get('/coupons/:couponCode/apply/:planCode') - @ApiOperation({ - summary: - 'Check if a Particular coupon is available to apply for a particular plan and if the coupon is valid or not', - }) - async applyCouponRoute( - @UserSession() user: IJwtPayload, - @Param('couponCode') couponCode: string, - @Param('planCode') planCode: string - ) { - return this.applyCoupon.execute(couponCode, user.email, planCode); - } - - @Get('/checkout') - @ApiOperation({ - summary: 'Get Tax Information and checkout details', - }) - async checkoutRoute( - @Query('planCode') planCode: string, - @UserSession() user: IJwtPayload, - @Query('paymentMethodId') paymentMethodId: string, - @Query('couponCode') couponCode?: string - ) { - return this.checkout.execute({ - planCode: planCode, - projectId: user._projectId, - paymentMethodId: paymentMethodId, - couponCode: couponCode, - }); - } - - @Get('/subscribe') - @ApiOperation({ - summary: 'Make successful Plan Purchase and begin subscription', - }) - async newSubscriptionRoute( - @Query('planCode') planCode: string, - @UserSession() user: IJwtPayload, - @Query('paymentMethodId') paymentMethodId: string, - @Query('couponCode') couponCode?: string - ) { - return await this.subscription.execute({ - projectId: user._projectId, - planCode, - selectedPaymentMethod: paymentMethodId, - couponCode, - }); - } } diff --git a/apps/queue-manager/package.json b/apps/queue-manager/package.json index da2ac7abd..7e9ea8954 100644 --- a/apps/queue-manager/package.json +++ b/apps/queue-manager/package.json @@ -1,6 +1,6 @@ { "name": "@impler/queue-manager", - "version": "1.8.1", + "version": "1.9.0", "author": "implerhq", "license": "MIT", "private": true, diff --git a/apps/queue-manager/src/consumers/get-auto-import-job-data.consumer.ts b/apps/queue-manager/src/consumers/get-auto-import-job-data.consumer.ts index 73dfbbfef..ca8b9a5d0 100644 --- a/apps/queue-manager/src/consumers/get-auto-import-job-data.consumer.ts +++ b/apps/queue-manager/src/consumers/get-auto-import-job-data.consumer.ts @@ -7,6 +7,7 @@ import { UserJobImportStatusEnum, IFilter, FilterOperationEnum, + BILLABLEMETRIC_CODE_ENUM, } from '@impler/shared'; import { SendImportJobDataConsumer } from './send-import-job-data.consumer'; @@ -394,10 +395,8 @@ export class SendAutoImportJobDataConsumer extends SendImportJobDataConsumer { await this.paymentAPIService.createEvent( { - uploadId: _jobId, - totalRecords: validationResult.totalRecords, - validRecords: validationResult.validRecords, - invalidRecords: validationResult.invalidRecords, + units: validationResult.totalRecords, + billableMetricCode: BILLABLEMETRIC_CODE_ENUM.ROWS, }, userJobInfo.externalUserId ); diff --git a/apps/web/assets/icons/Close.icon.tsx b/apps/web/assets/icons/Close.icon.tsx index 2e2f83a64..747f198ae 100644 --- a/apps/web/assets/icons/Close.icon.tsx +++ b/apps/web/assets/icons/Close.icon.tsx @@ -1,7 +1,7 @@ import { IconType } from '@types'; import { IconSizes } from 'config'; -export const CloseIcon = ({ size = 'sm', color }: IconType) => { +export const CloseIcon = ({ size = 'lg', color }: IconType) => { return ( { + return ( + + + + + + ); +}; diff --git a/apps/web/assets/icons/Forbidden.icon.tsx b/apps/web/assets/icons/Forbidden.icon.tsx new file mode 100644 index 000000000..67cd0d113 --- /dev/null +++ b/apps/web/assets/icons/Forbidden.icon.tsx @@ -0,0 +1,21 @@ +import { IconType } from '@types'; + +export function ForbiddenIcon({ size = 'xl' }: IconType) { + return ( + + Forbid Streamline Icon: https://streamlinehq.com + + + + ); +} diff --git a/apps/web/assets/icons/GiveHeart.icon.tsx b/apps/web/assets/icons/GiveHeart.icon.tsx new file mode 100644 index 000000000..8bd0cad07 --- /dev/null +++ b/apps/web/assets/icons/GiveHeart.icon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const GiveHeartIcon = () => ( + + + +); diff --git a/apps/web/assets/icons/Importer.icon.tsx b/apps/web/assets/icons/Importer.icon.tsx new file mode 100644 index 000000000..3c751fbe8 --- /dev/null +++ b/apps/web/assets/icons/Importer.icon.tsx @@ -0,0 +1,20 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const ImporterIcon = ({ size = 'xl', color = 'currentColor', className }: IconType) => { + return ( + + + + ); +}; diff --git a/apps/web/assets/icons/IntegrationStep.icon.tsx b/apps/web/assets/icons/IntegrationStep.icon.tsx new file mode 100644 index 000000000..3b5d077f3 --- /dev/null +++ b/apps/web/assets/icons/IntegrationStep.icon.tsx @@ -0,0 +1,20 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const IntegrationStepIcon = ({ size = 'xl', color = 'currentColor', className }: IconType) => { + return ( + + + + ); +}; diff --git a/apps/web/assets/icons/ListTick.icon.tsx b/apps/web/assets/icons/ListTick.icon.tsx new file mode 100644 index 000000000..92a00934c --- /dev/null +++ b/apps/web/assets/icons/ListTick.icon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const ListTickIcon = () => { + return ( + + + + ); +}; diff --git a/apps/web/assets/icons/PaymentCard.icon.tsx b/apps/web/assets/icons/PaymentCard.icon.tsx new file mode 100644 index 000000000..c3831bcd4 --- /dev/null +++ b/apps/web/assets/icons/PaymentCard.icon.tsx @@ -0,0 +1,32 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const PaymentCardIcon = ({ size = 'lg' }: IconType) => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/apps/web/assets/icons/RoundedCheck.icon.tsx b/apps/web/assets/icons/RoundedCheck.icon.tsx new file mode 100644 index 000000000..a0c929743 --- /dev/null +++ b/apps/web/assets/icons/RoundedCheck.icon.tsx @@ -0,0 +1,16 @@ +import { IconType } from '@types'; + +export function RoundedCheckIcon(props: IconType) { + return ( + + {/* eslint-disable-next-line max-len */} + + + ); +} diff --git a/apps/web/assets/icons/SetupDestination.icon.tsx b/apps/web/assets/icons/SetupDestination.icon.tsx new file mode 100644 index 000000000..20a3a0451 --- /dev/null +++ b/apps/web/assets/icons/SetupDestination.icon.tsx @@ -0,0 +1,19 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const SetupDestinationIcon = ({ size = 'xl' }: IconType) => { + return ( + + + + ); +}; diff --git a/apps/web/assets/icons/Stocks.icon.tsx b/apps/web/assets/icons/Stocks.icon.tsx new file mode 100644 index 000000000..7b7acd37f --- /dev/null +++ b/apps/web/assets/icons/Stocks.icon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const StocksIcon = () => ( + + + +); diff --git a/apps/web/assets/icons/Team.icon.tsx b/apps/web/assets/icons/Team.icon.tsx new file mode 100644 index 000000000..74cac7f3c --- /dev/null +++ b/apps/web/assets/icons/Team.icon.tsx @@ -0,0 +1,20 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const TeamIcon = ({ size = 'xl', color = 'currentColor', className }: IconType) => { + return ( + + + + ); +}; diff --git a/apps/web/assets/icons/ThreeDotsVertical.icon.tsx b/apps/web/assets/icons/ThreeDotsVertical.icon.tsx new file mode 100644 index 000000000..477af22f2 --- /dev/null +++ b/apps/web/assets/icons/ThreeDotsVertical.icon.tsx @@ -0,0 +1,20 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const ThreeDotsVerticalIcon = ({ size = 'sm', color, className }: IconType) => { + return ( + + + + + + ); +}; diff --git a/apps/web/assets/icons/TimeClock.icon.tsx b/apps/web/assets/icons/TimeClock.icon.tsx new file mode 100644 index 000000000..340be1255 --- /dev/null +++ b/apps/web/assets/icons/TimeClock.icon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const TimeClockIcon = () => ( + + + +); diff --git a/apps/web/assets/icons/ViewTransaction.icon.tsx b/apps/web/assets/icons/ViewTransaction.icon.tsx new file mode 100644 index 000000000..026f450cf --- /dev/null +++ b/apps/web/assets/icons/ViewTransaction.icon.tsx @@ -0,0 +1,44 @@ +import { IconType } from '@types'; +import { IconSizes } from 'config'; + +export const ViewTransactionIcon = ({ size = 'lg' }: IconType) => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/apps/web/assets/icons/index.ts b/apps/web/assets/icons/index.ts index fffeee8d1..80ceb1832 100644 --- a/apps/web/assets/icons/index.ts +++ b/apps/web/assets/icons/index.ts @@ -2,3 +2,4 @@ export { MoonIcon } from './Moon.icon'; export { SunIcon } from './Sun.icon'; export { ChevronDownIcon } from './ChevronDown.icon'; export { ImportIcon } from './Import.icon'; +export { ForbiddenIcon } from './Forbidden.icon'; diff --git a/apps/web/assets/images/companies/Superworks.svg b/apps/web/assets/images/companies/Superworks.svg new file mode 100644 index 000000000..121b1f06e --- /dev/null +++ b/apps/web/assets/images/companies/Superworks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/aklamio.svg b/apps/web/assets/images/companies/aklamio.svg new file mode 100644 index 000000000..f07c7d338 --- /dev/null +++ b/apps/web/assets/images/companies/aklamio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/artha.svg b/apps/web/assets/images/companies/artha.svg new file mode 100644 index 000000000..a3140c692 --- /dev/null +++ b/apps/web/assets/images/companies/artha.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/nasscom.svg b/apps/web/assets/images/companies/nasscom.svg new file mode 100644 index 000000000..6c791192f --- /dev/null +++ b/apps/web/assets/images/companies/nasscom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/nirvana.svg b/apps/web/assets/images/companies/nirvana.svg new file mode 100644 index 000000000..6d2bd08c8 --- /dev/null +++ b/apps/web/assets/images/companies/nirvana.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/omniva.svg b/apps/web/assets/images/companies/omniva.svg new file mode 100644 index 000000000..a9067d83e --- /dev/null +++ b/apps/web/assets/images/companies/omniva.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/orbit.svg b/apps/web/assets/images/companies/orbit.svg new file mode 100644 index 000000000..32e718731 --- /dev/null +++ b/apps/web/assets/images/companies/orbit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/assets/images/companies/ratio-dev.webp b/apps/web/assets/images/companies/ratio-dev.webp new file mode 100644 index 000000000..8501b41b4 Binary files /dev/null and b/apps/web/assets/images/companies/ratio-dev.webp differ diff --git a/apps/web/assets/images/companies/shipblink.webp b/apps/web/assets/images/companies/shipblink.webp new file mode 100644 index 000000000..dcd15cf0d Binary files /dev/null and b/apps/web/assets/images/companies/shipblink.webp differ diff --git a/apps/web/assets/images/companies/ubico.svg b/apps/web/assets/images/companies/ubico.svg new file mode 100644 index 000000000..794548ba2 --- /dev/null +++ b/apps/web/assets/images/companies/ubico.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/components/AddCard/ActiveSubscriptionContent.tsx b/apps/web/components/AddCard/ActiveSubscriptionContent.tsx deleted file mode 100644 index 205ecb04b..000000000 --- a/apps/web/components/AddCard/ActiveSubscriptionContent.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { Card, Stack, Title, Text, Skeleton, Box, Group, Divider, Flex } from '@mantine/core'; -import { colors } from '@config'; -import { LeftArrowIcon } from '@assets/icons/LeftArrow.icon'; -import { modals } from '@mantine/modals'; -import { getPlanType } from '@shared/utils'; -import { ISubscriptionData } from '@impler/shared'; - -interface ActiveSubscriptionContentProps { - email: string; - activePlanDetails?: ISubscriptionData; - isActivePlanDetailsLoading?: boolean; -} - -export function ActiveSubscriptionContent({ - activePlanDetails, - isActivePlanDetailsLoading, -}: ActiveSubscriptionContentProps) { - const planAmount = Number(activePlanDetails?.amount) || 0; - const outstandingCharges = Number(activePlanDetails?.plan?.charge) || 0; - const paymentMethodCurrency = activePlanDetails?.customer?.paymentMethodCurrency || 'USD'; - - const tax = Number(activePlanDetails?.tax) || 0; - - const totalAmount = planAmount + outstandingCharges + tax; - - return ( - - - modals.closeAll()} href="#"> - - - Back - - - - - As Per Your Current Card - - - {isActivePlanDetailsLoading ? ( - - - - - - - - - - - ) : activePlanDetails ? ( - <> - - {paymentMethodCurrency.toUpperCase()} {planAmount} - - {' / '} {getPlanType(activePlanDetails.plan?.interval)} - - - - - - Plan Name - {activePlanDetails?.plan.name} - - - - Plan Amount - {`${planAmount} (${activePlanDetails.customer?.paymentMethodCurrency?.toUpperCase()})`} - - {outstandingCharges > 0 && ( - - Outstanding Charges - - {outstandingCharges} - - {` (${activePlanDetails.customer?.paymentMethodCurrency?.toUpperCase()})`} - - - - )} - - {tax > 0 && ( - - Tax - {`${tax} (${activePlanDetails.customer?.paymentMethodCurrency?.toUpperCase()})`} - - )} - - - Total Amount - - {`${totalAmount.toFixed(2)} (${activePlanDetails.customer?.paymentMethodCurrency?.toUpperCase()})`} - - - - - ) : null} - - - ); -} diff --git a/apps/web/components/AddCard/AddCard.styles.tsx b/apps/web/components/AddCard/AddCard.styles.tsx deleted file mode 100644 index a6dc9c2ae..000000000 --- a/apps/web/components/AddCard/AddCard.styles.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { colors } from '@config'; -import { createStyles } from '@mantine/core'; - -export default createStyles((theme) => ({ - addCard: { - display: 'flex', - cursor: 'pointer', - color: colors.blue, - alignItems: 'center', - minHeight: 185, - justifyContent: 'center', - transition: `color 0.3s ease, background-color 0.3s ease`, - borderRadius: theme.radius.md, - border: `1px solid ${colors.blue}`, - ':hover': { - color: colors.white, - backgroundColor: colors.blue, - }, - }, -})); diff --git a/apps/web/components/AddCard/AddCard.tsx b/apps/web/components/AddCard/AddCard.tsx deleted file mode 100644 index 8ef221e15..000000000 --- a/apps/web/components/AddCard/AddCard.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Text, UnstyledButton } from '@mantine/core'; -import useStyles from './AddCard.styles'; - -interface AddCardProps { - onClick: () => void; -} - -export const AddCard = ({ onClick }: AddCardProps) => { - const { classes } = useStyles(); - - return ( - - - + Add New Card - - - ); -}; diff --git a/apps/web/components/AddCard/Card/Card.styles.tsx b/apps/web/components/AddCard/Card/Card.styles.tsx deleted file mode 100644 index 94136cb53..000000000 --- a/apps/web/components/AddCard/Card/Card.styles.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { colors } from '@config'; -import { createStyles } from '@mantine/core'; - -export default createStyles((theme) => ({ - card: { - borderRadius: theme.radius.md, - backgroundColor: colors.BGPrimaryDark, - }, -})); diff --git a/apps/web/components/AddCard/Card/Card.tsx b/apps/web/components/AddCard/Card/Card.tsx deleted file mode 100644 index e0bcf5482..000000000 --- a/apps/web/components/AddCard/Card/Card.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import Image from 'next/image'; -import { Card as MantineCard, Group, Stack, Text } from '@mantine/core'; - -import { colors } from '@config'; -import { Button } from '@ui/button'; -import useStyles from './Card.styles'; -import { ICardData } from '@impler/shared'; -import { DeleteIcon } from '@assets/icons/Delete.icon'; - -interface CardProps { - data: ICardData; - onRemoveCardClick: (paymentMethodId: string) => void; -} - -export const Card = ({ data, onRemoveCardClick }: CardProps) => { - const { classes } = useStyles(); - const src = data.brand.toLowerCase()?.replaceAll(' ', '_') || 'default'; - - return ( - - - - - - - Card Number - - - **** **** **** {data.last4Digits} - - - - - - - Expiry Date - - - {`${data?.expMonth}/${data?.expYear}`} - - - - } - onClick={() => onRemoveCardClick(data.paymentMethodId)} - > - Remove - - - - - ); -}; diff --git a/apps/web/components/AddCard/Card/index.ts b/apps/web/components/AddCard/Card/index.ts deleted file mode 100644 index ca0b06047..000000000 --- a/apps/web/components/AddCard/Card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Card'; diff --git a/apps/web/components/AddCard/CardForm.styles.ts b/apps/web/components/AddCard/CardForm.styles.ts deleted file mode 100644 index acf4861b3..000000000 --- a/apps/web/components/AddCard/CardForm.styles.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { colors } from '@config'; -import { createStyles } from '@mantine/core'; - -export const useStyles = createStyles((theme, { numOfPaymentMethods }: { numOfPaymentMethods: number }) => ({ - scrollArea: { - height: Math.min(numOfPaymentMethods * 70, 120), - overflowX: 'hidden', - overflowY: 'auto', - position: 'relative', - display: 'block', - width: '100%', - boxSizing: 'border-box', - }, - - scrollAreaContent: { - display: 'flex', - flexDirection: 'column', - width: '100%', - }, - - scrollbar: { - '&[data-orientation="vertical"]': { - width: 10, - backgroundColor: colors.StrokeLight, - opacity: 1, - height: '100%', - }, - - '&[data-orientation="vertical"] .mantine-ScrollArea-thumb': { - backgroundColor: colors.blue, - opacity: 1, - transition: 'opacity 0.3s ease', - height: 'auto', - '&:hover': { - backgroundColor: colors.blue, - }, - }, - - '&:hover': { - opacity: 1, - }, - - '&[data-orientation="vertical"]:hover .mantine-ScrollArea-thumb': { - opacity: 1, - }, - }, -})); diff --git a/apps/web/components/AddCard/CardForm.tsx b/apps/web/components/AddCard/CardForm.tsx deleted file mode 100644 index 78f97aee9..000000000 --- a/apps/web/components/AddCard/CardForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState } from 'react'; -import Link from 'next/link'; -import { Stack, Group, Title, Text, Flex, ScrollArea } from '@mantine/core'; -import { colors } from '@config'; -import { Button } from '@ui/button'; -import { ICardData } from '@impler/shared'; -import { useStyles } from './CardForm.styles'; -import { CurrentCardSection } from './CurrentCardSection'; -import { PaymentMethods } from './PaymentMethods'; -import { AddNewPaymentMethodForm } from './PaymentMethods/AddNewPaymentMethodForm'; - -interface CardFormProps { - showForm: boolean; - activeCard?: ICardData; - paymentMethods?: ICardData[]; - selectedPaymentMethod: string | undefined; - isLoading: boolean; - onToggleForm: () => void; - onPaymentMethodChange: (id: string) => void; - onSubmit: () => void; -} - -export function CardForm({ - showForm, - paymentMethods, - selectedPaymentMethod, - isLoading, - onToggleForm, - onPaymentMethodChange, - onSubmit, - activeCard, -}: CardFormProps) { - const [isFormValid, setIsFormValid] = useState(false); - const { classes } = useStyles({ numOfPaymentMethods: paymentMethods?.length || 0 }); - - const handleSubmit = () => { - if (showForm && !isFormValid) { - return; - } - onSubmit(); - }; - - return ( - - - {showForm ? ( - <> - - - Add New Card - - - - Show Added Cards - - - - - - ) : ( - - - - - Change Card - - - - - - - - )} - - - - - - - ); -} diff --git a/apps/web/components/AddCard/ChangeCard.tsx b/apps/web/components/AddCard/ChangeCard.tsx deleted file mode 100644 index 2df20952d..000000000 --- a/apps/web/components/AddCard/ChangeCard.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Card, Flex } from '@mantine/core'; -import { CardNumberElement, useElements, useStripe } from '@stripe/react-stripe-js'; -import { colors } from '@config'; -import { ICardData } from '@impler/shared'; -import { CardForm } from './CardForm'; -import { useAddCard } from '@hooks/useAddCard'; -import { usePaymentMethods } from '@hooks/usePaymentMethods'; -import { usePlanDetails } from '@hooks/usePlanDetails'; -import { useUpdatePaymentMethod } from '@hooks/useUpdatePaymentMethod'; -import { ActiveSubscriptionContent } from './ActiveSubscriptionContent'; - -export interface ChangeCardModalContentProps { - email: string; - projectId: string; - planCode: string; - onClose: () => void; -} - -export function ChangeCard({ email, projectId }: ChangeCardModalContentProps) { - const { paymentMethods, isPaymentMethodsLoading, refetchPaymentMethods } = usePaymentMethods(); - const { activePlanDetails, isActivePlanLoading } = usePlanDetails({ projectId }); - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(); - const [activeCard, setActiveCard] = useState(undefined); - const [showForm, setShowForm] = useState(false); - const [displayedPaymentMethods, setDisplayedPaymentMethods] = useState([]); - const { updatePaymentMethod, isUpdatePaymentMethodLoading } = useUpdatePaymentMethod(); - const { addPaymentMethod, isAddPaymentMethodLoading } = useAddCard({ refetchPaymentMethods }); - - const stripe = useStripe(); - const elements = useElements(); - - useEffect(() => { - if (paymentMethods && activePlanDetails) { - const activePaymentMethodId = activePlanDetails.customer?.paymentMethodId; - - const userActiveCard = paymentMethods.find( - (method: ICardData) => method.paymentMethodId === activePaymentMethodId - ); - - setActiveCard(userActiveCard); - setSelectedPaymentMethod(activePaymentMethodId); - setDisplayedPaymentMethods(paymentMethods); - } - }, [paymentMethods, activePlanDetails]); - - const toggleFormVisibility = () => { - setShowForm((prev) => { - if (!prev) { - setSelectedPaymentMethod(undefined); - } - - return !prev; - }); - }; - - const handleSubmit = async () => { - if (showForm) { - await handleAddCard(); - } else { - await handleChangeCard(); - } - }; - - const handleAddCard = async () => { - if (!stripe || !elements) return; - - const cardNumberElement = elements.getElement(CardNumberElement); - if (!cardNumberElement) return; - - const { paymentMethod } = await stripe.createPaymentMethod({ - type: 'card', - card: cardNumberElement, - }); - - if (paymentMethod) { - await addPaymentMethod(paymentMethod.id); - setShowForm(false); - await refetchPaymentMethods(); - } - }; - - const orderPaymentMethods = () => { - if (!paymentMethods) return []; - - const selected = paymentMethods.find((method) => method.paymentMethodId === selectedPaymentMethod); - const others = paymentMethods.filter((method) => method.paymentMethodId !== selectedPaymentMethod); - - return selected ? [selected, ...others] : others; - }; - - const handleChangeCard = async () => { - if (selectedPaymentMethod) { - const sortedMethods = orderPaymentMethods(); - setDisplayedPaymentMethods(sortedMethods); - - await updatePaymentMethod({ paymentMethodId: selectedPaymentMethod }); - const newActiveCard = paymentMethods?.find( - (method: ICardData) => method.paymentMethodId === selectedPaymentMethod - ); - setActiveCard(newActiveCard); - } - }; - - return ( - - - - - - - - ); -} diff --git a/apps/web/components/AddCard/CheckoutContent.tsx b/apps/web/components/AddCard/CheckoutContent.tsx deleted file mode 100644 index 00ac78e99..000000000 --- a/apps/web/components/AddCard/CheckoutContent.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { Card, Stack, Title, Text, Flex } from '@mantine/core'; - -import { colors, MODAL_KEYS } from '@config'; -import { getPlanType } from '@shared/utils'; -import { CheckoutDetails } from '@components/Checkout'; -import { LeftArrowIcon } from '@assets/icons/LeftArrow.icon'; -import { modals } from '@mantine/modals'; - -interface CheckoutContentProps { - checkoutData?: ICheckoutData; - isCheckoutDataLoading: boolean; -} - -export function CheckoutContent({ checkoutData, isCheckoutDataLoading }: CheckoutContentProps) { - return ( - - - modals.close(MODAL_KEYS.SELECT_CARD)} href="#"> - - - Back - - - - - Get{' '} - - {checkoutData?.planName} - {' '} - Benefits - - - {checkoutData?.currency.toUpperCase()} {checkoutData?.planAmount} - - {' / '} {getPlanType(checkoutData?.interval)} - - - - - - ); -} diff --git a/apps/web/components/AddCard/Coupon/Coupon.tsx b/apps/web/components/AddCard/Coupon/Coupon.tsx deleted file mode 100644 index f06a7de7a..000000000 --- a/apps/web/components/AddCard/Coupon/Coupon.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { Alert, Group, TextInput } from '@mantine/core'; -import { CheckIcon } from '@assets/icons/Check.icon'; -import { useCoupon } from '@hooks/useCoupon'; -import { Button } from '@ui/button'; - -interface CouponProps { - planCode: string; - couponCode: string | undefined; - setAppliedCouponCode: (couponCode?: string) => void; -} - -export const Coupon = ({ planCode, couponCode, setAppliedCouponCode }: CouponProps) => { - const { register, errors, applyCouponSubmit, handleSubmit, isApplyCouponLoading } = useCoupon({ - planCode, - setAppliedCouponCode, - }); - - return ( -
- {couponCode ? ( - setAppliedCouponCode(undefined)} - icon={} - > - {`Coupon ${couponCode} is applied!`} - - ) : ( -
- - - - -
- )} -
- ); -}; diff --git a/apps/web/components/AddCard/Coupon/index.ts b/apps/web/components/AddCard/Coupon/index.ts deleted file mode 100644 index 433e89fe4..000000000 --- a/apps/web/components/AddCard/Coupon/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Coupon'; diff --git a/apps/web/components/AddCard/CurrentCardSection.tsx b/apps/web/components/AddCard/CurrentCardSection.tsx deleted file mode 100644 index becd5d9ca..000000000 --- a/apps/web/components/AddCard/CurrentCardSection.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { Group, Title, Text } from '@mantine/core'; -import { colors } from '@config'; -import { ICardData } from '@impler/shared'; -import { PaymentMethodOption } from './PaymentMethods'; - -interface CurrentCardSectionProps { - activeCard: ICardData | undefined; - hasPaymentMethods: boolean; - onAddNewClick: () => void; -} - -export function CurrentCardSection({ activeCard, hasPaymentMethods, onAddNewClick }: CurrentCardSectionProps) { - return ( - <> - - - Current Card - - {hasPaymentMethods && ( - - - + Add New Card - - - )} - - - {activeCard && ( - {}} - showRadio={false} - /> - )} - - ); -} diff --git a/apps/web/components/AddCard/PaymentMethodForm.tsx b/apps/web/components/AddCard/PaymentMethodForm.tsx deleted file mode 100644 index 7408ae6fa..000000000 --- a/apps/web/components/AddCard/PaymentMethodForm.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import Link from 'next/link'; -import { Card, Group, Stack, Title, Text } from '@mantine/core'; -import { colors } from '@config'; -import { Button } from '@ui/button'; -import { ICardData } from '@impler/shared'; -import { Coupon } from './Coupon'; -import { PaymentMethods } from './PaymentMethods'; -import { AddNewPaymentMethodForm } from './PaymentMethods/AddNewPaymentMethodForm'; - -interface PaymentMethodFormProps { - email: string; - planCode: string; - handleAddCard: () => void; - isPurchaseLoading: boolean; - isPaymentMethodsLoading: boolean; - isPaymentMethodsFetching: boolean; - appliedCouponCode?: string; - paymentMethods?: ICardData[]; - isCouponFeatureEnabled: string; - isAddPaymentMethodLoading: boolean; - setAppliedCouponCode: (code?: string) => void; - handleProceed: (paymentMethodId: string) => void; - getCheckoutData: (paymentMethodId?: string) => void; -} - -export function PaymentMethodForm({ - planCode, - handleAddCard, - handleProceed, - paymentMethods, - getCheckoutData, - isPaymentMethodsFetching, - isPaymentMethodsLoading, - isPurchaseLoading, - appliedCouponCode, - setAppliedCouponCode, - isCouponFeatureEnabled, - isAddPaymentMethodLoading, -}: PaymentMethodFormProps) { - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(); - const [showAddNewCardForm, setShowAddNewCardForm] = useState(false); - const [isFormValid, setIsFormValid] = useState(false); - - const toggleFormVisibility = () => { - setShowAddNewCardForm(!showAddNewCardForm); - if (!showAddNewCardForm) { - setSelectedPaymentMethod(undefined); - } - }; - - const handleSubmit = async () => { - if (showAddNewCardForm) { - if (isFormValid) { - handleAddCard(); - } - } else if (selectedPaymentMethod) { - handleProceed(selectedPaymentMethod); - } - }; - useEffect(() => { - if (paymentMethods?.length === 0) { - setShowAddNewCardForm(true); - getCheckoutData(); - } - if (selectedPaymentMethod) { - getCheckoutData(selectedPaymentMethod); - } else if (paymentMethods?.length) { - setSelectedPaymentMethod(paymentMethods[0].paymentMethodId); - getCheckoutData(paymentMethods[0].paymentMethodId); - } - }, [selectedPaymentMethod, paymentMethods, getCheckoutData]); - - return ( - - - - - Payment Method - - {paymentMethods && paymentMethods.length > 0 ? ( - - - {showAddNewCardForm ? 'Show Added Cards' : '+ Add New Card'} - - - ) : null} - - - - {showAddNewCardForm ? ( - - ) : ( - - )} - - {isCouponFeatureEnabled === 'false' && ( - - )} - - - - - - ); -} diff --git a/apps/web/components/AddCard/PaymentMethods/AddNewPaymentMethodForm.tsx b/apps/web/components/AddCard/PaymentMethods/AddNewPaymentMethodForm.tsx deleted file mode 100644 index 77a21ed70..000000000 --- a/apps/web/components/AddCard/PaymentMethods/AddNewPaymentMethodForm.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Group, Stack } from '@mantine/core'; -import { CardNumberElement, CardExpiryElement, CardCvcElement } from '@stripe/react-stripe-js'; -import { colors } from '@config'; -import { StripeInput } from './StripeInput'; - -interface AddNewPaymentMethodFormProps { - setIsValid: (isValid: boolean) => void; -} - -export function AddNewPaymentMethodForm({ setIsValid }: AddNewPaymentMethodFormProps) { - const [fields, setFields] = useState({ - cardNumber: { - complete: false, - error: '', - touched: false, - }, - cardExpiry: { - complete: false, - error: '', - touched: false, - }, - cardCvc: { - complete: false, - error: '', - touched: false, - }, - }); - - const baseStyle = { - style: { - base: { - fontSize: '16px', - color: colors.black, - '::placeholder': { - color: colors.grey, - }, - }, - invalid: { - color: colors.danger, - }, - }, - }; - - const stripeElementOptions = { - cardNumber: { - style: baseStyle.style, - showIcon: true, - placeholder: 'Enter your Card Number', - }, - cardExpiry: { - style: baseStyle.style, - placeholder: 'MM/YY', - }, - cardCvc: { - style: baseStyle.style, - placeholder: 'CVC', - }, - }; - - const handleElementChange = (event: any, fieldName: keyof typeof fields) => { - setFields((prev) => { - const newFields = { - ...prev, - [fieldName]: { - complete: event.complete, - error: event.error?.message || '', - touched: true, - }, - }; - - return newFields; - }); - }; - - useEffect(() => { - const isFormValid = Object.values(fields).every((field) => { - const fieldValid = field.complete && !field.error; - - return fieldValid; - }); - - setIsValid(isFormValid); - }, [fields, setIsValid]); - - const getFieldError = (fieldName: keyof typeof fields) => { - const field = fields[fieldName]; - if (!field.touched) return ''; - if (field.error) return field.error; - if (!field.complete) return 'This field is required'; - - return ''; - }; - - return ( - - handleElementChange(event, 'cardNumber')} - options={stripeElementOptions.cardNumber} - /> - - handleElementChange(event, 'cardExpiry')} - options={stripeElementOptions.cardExpiry} - /> - handleElementChange(event, 'cardCvc')} - options={stripeElementOptions.cardCvc} - /> - - - ); -} diff --git a/apps/web/components/AddCard/PaymentMethods/PaymentMethodOption.tsx b/apps/web/components/AddCard/PaymentMethods/PaymentMethodOption.tsx deleted file mode 100644 index 2148b5a84..000000000 --- a/apps/web/components/AddCard/PaymentMethods/PaymentMethodOption.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import Image from 'next/image'; -import { Flex, Group, Radio, Stack, Text, useMantineTheme } from '@mantine/core'; - -import { colors } from '@config'; -import { useStyles } from './PaymentMethods.styles'; - -interface PaymentMethodOptionProps { - method: { - paymentMethodId: string; - brand: string; - last4Digits: string; - expMonth: number; - expYear: number; - }; - selected: boolean; - onChange: (methodId: string) => void; - showRadio?: boolean; -} - -export function PaymentMethodOption({ method, selected, onChange, showRadio = true }: PaymentMethodOptionProps) { - const theme = useMantineTheme(); - const { classes } = useStyles(); - - const cardBrandsSrc = method.brand.toLowerCase().replaceAll(' ', '_') || 'default'; - - const handleClick = () => { - onChange(method.paymentMethodId); - }; - - return ( - - ); -} diff --git a/apps/web/components/AddCard/PaymentMethods/PaymentMethods.styles.tsx b/apps/web/components/AddCard/PaymentMethods/PaymentMethods.styles.tsx deleted file mode 100644 index 2444058ec..000000000 --- a/apps/web/components/AddCard/PaymentMethods/PaymentMethods.styles.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { colors } from '@config'; -import { createStyles } from '@mantine/core'; - -export const useStyles = createStyles((theme) => ({ - radio: { - '& .mantine-Radio-radio': { - border: `2px solid ${colors.blue}`, - backgroundColor: 'transparent', - - '&:checked': { - backgroundColor: colors.blue, - border: theme.colors.blue, - }, - }, - }, -})); diff --git a/apps/web/components/AddCard/PaymentMethods/PaymentMethods.tsx b/apps/web/components/AddCard/PaymentMethods/PaymentMethods.tsx deleted file mode 100644 index f17ab3c69..000000000 --- a/apps/web/components/AddCard/PaymentMethods/PaymentMethods.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { ICardData } from '@impler/shared'; -import { Radio, SimpleGrid, useMantineTheme } from '@mantine/core'; - -import { PaymentMethodOption } from './PaymentMethodOption'; -import { useStyles } from './PaymentMethods.styles'; - -interface PaymentMethodsProps { - paymentMethods?: ICardData[] | undefined; - selectedPaymentMethod: string | undefined; - handlePaymentMethodChange: (methodId: string) => void; -} - -export function PaymentMethods({ - paymentMethods, - selectedPaymentMethod, - handlePaymentMethodChange, -}: PaymentMethodsProps) { - const theme = useMantineTheme(); - const { classes } = useStyles(); - - return ( - handlePaymentMethodChange(event)} - className={classes.radio} - > - - {paymentMethods?.map((method) => ( - handlePaymentMethodChange(method.paymentMethodId)} - /> - ))} - - - ); -} diff --git a/apps/web/components/AddCard/PaymentMethods/StripeInput.tsx b/apps/web/components/AddCard/PaymentMethods/StripeInput.tsx deleted file mode 100644 index 6ca90aac7..000000000 --- a/apps/web/components/AddCard/PaymentMethods/StripeInput.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useState } from 'react'; -import { Box, Text } from '@mantine/core'; -import { colors } from '@config'; - -interface StripeInputProps { - label: string; - StripeElement: React.ComponentType; - isFullWidth?: boolean; - error?: string; - required?: boolean; - onChange: (event: any) => void; - options?: any; -} - -export function StripeInput({ - label, - StripeElement, - isFullWidth = false, - error, - required = false, - onChange, - options, -}: StripeInputProps) { - const [isClicked, setIsClicked] = useState(false); - - return ( - - - {label} {required && *} - - - { - setIsClicked(true); - onChange(event); - }} - onBlur={() => setIsClicked(true)} - /> - - {error && isClicked && ( - - {error} - - )} - - ); -} diff --git a/apps/web/components/AddCard/PaymentMethods/index.ts b/apps/web/components/AddCard/PaymentMethods/index.ts deleted file mode 100644 index aeeeb5466..000000000 --- a/apps/web/components/AddCard/PaymentMethods/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './PaymentMethods'; -export * from './PaymentMethodOption'; diff --git a/apps/web/components/AddCard/PaymentModal.tsx b/apps/web/components/AddCard/PaymentModal.tsx deleted file mode 100644 index a603aa8a4..000000000 --- a/apps/web/components/AddCard/PaymentModal.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { loadStripe } from '@stripe/stripe-js'; -import { Elements } from '@stripe/react-stripe-js'; -import getConfig from 'next/config'; -import { MODAL_KEYS } from '@config'; -import { ChangeCard } from './ChangeCard'; -import { SubscribeToPlan } from './SubscribeToPlan'; - -const { publicRuntimeConfig } = getConfig(); - -const stripePromise = - publicRuntimeConfig.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY && - loadStripe(publicRuntimeConfig.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); - -export interface PaymentModalProps { - email: string; - planCode: string; - projectId: string; - onClose: () => void; - modalId: string; -} - -export function PaymentModal(props: PaymentModalProps) { - return ( - - {props.modalId === MODAL_KEYS.SELECT_CARD ? : } - - ); -} diff --git a/apps/web/components/AddCard/SubscribeToPlan.tsx b/apps/web/components/AddCard/SubscribeToPlan.tsx deleted file mode 100644 index 1821e1004..000000000 --- a/apps/web/components/AddCard/SubscribeToPlan.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { CardNumberElement, useElements, useStripe } from '@stripe/react-stripe-js'; -import { Flex } from '@mantine/core'; - -import { useCheckout } from '@hooks/useCheckout'; -import { useSubscribe } from '@hooks/useSubscribe'; - -import { useAddCardAndSubscribe } from '@hooks/useAddCardAndSubscribe'; -import { CheckoutContent } from './CheckoutContent'; -import { PaymentMethodForm } from './PaymentMethodForm'; -import { usePaymentMethods } from '@hooks/usePaymentMethods'; - -export interface SelectCardModalContentProps { - email: string; - planCode: string; - onClose: () => void; -} - -export function SubscribeToPlan({ email, planCode }: SelectCardModalContentProps) { - const { refetchPaymentMethods, paymentMethods, isPaymentMethodsLoading } = usePaymentMethods(); - - const { - handleProceed, - appliedCouponCode, - isPurchaseLoading, - setAppliedCouponCode, - isCouponFeatureEnabled, - isPaymentMethodsFetching, - } = useSubscribe({ - email, - planCode, - }); - - const { getCheckoutData, checkoutData, isCheckoutDataLoading } = useCheckout({ - couponCode: appliedCouponCode, - planCode, - email, - }); - - const stripe = useStripe(); - const elements = useElements(); - - const { addPaymentMethod, isAddPaymentMethodLoading, setIsPaymentMethodLoading } = useAddCardAndSubscribe({ - refetchPaymentMethods, - handleProceed, - }); - - const handleAddCard = async () => { - if (!stripe || !elements) return; - - const cardNumberElement = elements.getElement(CardNumberElement); - - if (!cardNumberElement) return; - - setIsPaymentMethodLoading(true); - const { paymentMethod } = await stripe.createPaymentMethod({ - type: 'card', - card: cardNumberElement, - }); - - if (paymentMethod) { - addPaymentMethod(paymentMethod.id); - } - }; - - return ( - - - - - - ); -} diff --git a/apps/web/components/AddCard/index.ts b/apps/web/components/AddCard/index.ts deleted file mode 100644 index 85a167685..000000000 --- a/apps/web/components/AddCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AddCard'; diff --git a/apps/web/components/Checkout/CheckoutDetails.tsx b/apps/web/components/Checkout/CheckoutDetails.tsx deleted file mode 100644 index 9896ee21f..000000000 --- a/apps/web/components/Checkout/CheckoutDetails.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Group, Text, Divider, Skeleton, Stack, Box, useMantineTheme } from '@mantine/core'; - -interface CheckoutDetailsProps { - checkoutData?: ICheckoutData; - isCheckoutDataLoading: boolean; -} - -export function CheckoutDetails({ checkoutData, isCheckoutDataLoading }: CheckoutDetailsProps) { - const theme = useMantineTheme(); - - if (isCheckoutDataLoading) - return ( - - - - - - - - - - - ); - - if (checkoutData) - return ( - - - Plan Name - {checkoutData.planName} - - - - Plan Amount - {`${checkoutData.planAmount} (${checkoutData.currency.toUpperCase()})`} - - - {checkoutData.proratedRefund ? ( - - Balance - -{`${checkoutData.proratedRefund} (${checkoutData.currency.toUpperCase()})`} - - ) : null} - - - Outstanding Charges - {`${checkoutData.outstandingAmount} (${checkoutData.currency.toUpperCase()})`} - - - {checkoutData.taxAmount ? ( - - Tax {`(${checkoutData.taxLabel} ${checkoutData.taxPercentage}%)`} - {`${checkoutData.taxAmount} (${checkoutData.currency.toUpperCase()})`} - - ) : null} - {checkoutData.discount ? ( - - Coupon Discount - -{`${checkoutData.discount} (${checkoutData.currency.toUpperCase()})`} - - ) : null} - - {typeof checkoutData.totalPrice !== 'undefined' ? ( - <> - - - Total Amount - {`${checkoutData.totalPrice} (${checkoutData.currency.toUpperCase()})`} - - - ) : null} - - ); - - return null; -} diff --git a/apps/web/components/Checkout/index.ts b/apps/web/components/Checkout/index.ts deleted file mode 100644 index a2e77fc65..000000000 --- a/apps/web/components/Checkout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CheckoutDetails'; diff --git a/apps/web/components/ConfirmationModal/ConfirmationModal.tsx b/apps/web/components/ConfirmationModal/ConfirmationModal.tsx index c9ee498c2..e911165f0 100644 --- a/apps/web/components/ConfirmationModal/ConfirmationModal.tsx +++ b/apps/web/components/ConfirmationModal/ConfirmationModal.tsx @@ -7,36 +7,35 @@ import { Stack, Text } from '@mantine/core'; import { Button } from '@ui/button'; import FailedAnimationData from './failed-animation-data.json'; import SuccessAnimationData from './success-animation-data.json'; +import { useRouter } from 'next/navigation'; -interface ConfirmationModalProps { - status: string; +interface PaymentConfirmationModalProps { + paymentStatus: 'success' | 'failed'; } const Lottie = dynamic(() => import('lottie-react'), { ssr: false }); -export const ConfirmationModal = ({ status }: ConfirmationModalProps) => { - const title = - status === CONSTANTS.PAYMENT_SUCCCESS_CODE - ? CONSTANTS.SUBSCRIPTION_ACTIVATED_TITLE - : CONSTANTS.SUBSCRIPTION_FAILED_TITLE; +export function PaymentStatusConfirmationModal({ paymentStatus }: PaymentConfirmationModalProps) { + const isSuccess = paymentStatus === CONSTANTS.PAYMENT_SUCCCESS_CODE; + const title = isSuccess ? CONSTANTS.SUBSCRIPTION_ACTIVATED_TITLE : CONSTANTS.SUBSCRIPTION_FAILED_TITLE; + const router = useRouter(); return ( {title} - - - {status === CONSTANTS.PAYMENT_SUCCCESS_CODE - ? CONSTANTS.PAYMENT_SUCCESS_MESSAGE - : CONSTANTS.PAYMENT_FAILED_MESSAGE} - - ); -}; +} diff --git a/apps/web/components/Stepper/Stepper.styles.tsx b/apps/web/components/Stepper/Stepper.styles.tsx index 9eba8aaf0..8ee91a2fc 100644 --- a/apps/web/components/Stepper/Stepper.styles.tsx +++ b/apps/web/components/Stepper/Stepper.styles.tsx @@ -2,20 +2,19 @@ import { createStyles } from '@mantine/core'; export const useStyles = createStyles((theme) => ({ stepperContainer: { - marginBottom: theme.spacing.xl, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.colors.dark[5], + borderRadius: theme.radius.xl, + minWidth: 50, + height: 50, }, stepText: { - color: theme.colors.gray[5], - fontSize: theme.fontSizes.sm, - fontWeight: 500, - marginBottom: theme.spacing.xs, - }, - progressBar: { - height: 4, - backgroundColor: theme.colors.dark[4], - - '& .mantine-Progress-bar': { - backgroundColor: theme.colors.blue[5], - }, + color: theme.white, + fontSize: theme.fontSizes.md, + fontWeight: 600, + lineHeight: 1, + letterSpacing: '0.5px', }, })); diff --git a/apps/web/components/Stepper/Stepper.tsx b/apps/web/components/Stepper/Stepper.tsx index 9dc357c17..3e7787912 100644 --- a/apps/web/components/Stepper/Stepper.tsx +++ b/apps/web/components/Stepper/Stepper.tsx @@ -1,4 +1,4 @@ -import { Box, Text, Progress } from '@mantine/core'; +import { Box, Text } from '@mantine/core'; import { useStyles } from './Stepper.styles'; interface StepperProps { @@ -8,14 +8,12 @@ interface StepperProps { export const Stepper = ({ currentStep, totalSteps }: StepperProps) => { const { classes } = useStyles(); - const progressValue = (currentStep / totalSteps) * 100; return ( - Step {currentStep} of {totalSteps} + {currentStep}/{totalSteps} - ); }; diff --git a/apps/web/components/UpgradePlan/Plans/PlanCard.tsx b/apps/web/components/UpgradePlan/Plans/PlanCard.tsx index 0822236f9..b3b19af41 100644 --- a/apps/web/components/UpgradePlan/Plans/PlanCard.tsx +++ b/apps/web/components/UpgradePlan/Plans/PlanCard.tsx @@ -1,65 +1,139 @@ -import React from 'react'; -import { Card, Text, Badge, Stack, Divider } from '@mantine/core'; +import React, { useEffect, useState } from 'react'; +import { plansApi, useSubscription, useCountryAndCurrency } from 'subos-frontend'; +import { Card, Text, Badge, Stack, Divider, LoadingOverlay } from '@mantine/core'; import { Button } from '@ui/button'; -import { colors, MODAL_KEYS, PLANCODEENUM } from '@config'; +import { colors, NOTIFICATION_KEYS, PLANCODEENUM, ROUTES } from '@config'; import { Plan } from './Plans'; import { PlanFeature } from './PlanFeature'; import useStyles from './Plans.styles'; -import { usePlanDetails } from '@hooks/usePlanDetails'; +import { useAppState } from 'store/app.context'; +import { notify } from '@libs/notify'; interface PlanCardProps { plan: Plan; isYearly: boolean; - activePlanCode: string; - email: string; - projectId?: string; } -export function PlanCard({ plan, isYearly, activePlanCode, projectId }: PlanCardProps) { +export function PlanCard({ plan, isYearly }: PlanCardProps) { + const { profileInfo } = useAppState(); const { classes } = useStyles(); - const { onOpenPaymentModal } = usePlanDetails({ projectId }); + const { createPaymentSession } = plansApi; + const { currency } = useCountryAndCurrency(); + + const [isLoading, setIsLoading] = useState(false); + const { subscription, fetchSubscription } = useSubscription(); + + useEffect(() => { + const loadSubscription = async () => { + if (!profileInfo?.email) return; + + try { + setIsLoading(true); + await fetchSubscription(profileInfo.email); + } catch (err: any) { + notify(NOTIFICATION_KEYS.ERROR_FETCHING_SUBSCRIPTION_DETAILS, { + title: 'Failed to fetch subscription', + message: err?.message, + }); + } finally { + setIsLoading(false); + } + }; + + loadSubscription(); + }, [profileInfo?.email]); + + const handlePaymentSession = async () => { + setIsLoading(true); + try { + const response = await createPaymentSession(plan.code, { + returnUrl: `${window.location.origin}${ROUTES.SUBSCRIPTION_STATUS}`, + externalId: profileInfo?.email, + cancelUrl: `${window.location.origin}${ROUTES.PAYMENT_CANCEL}`, + currency, + }); + if (response?.success && response?.data?.checkoutUrl) { + window.location.href = response?.data?.checkoutUrl; + + return response; + } + + return null; + } catch (err: any) { + notify(NOTIFICATION_KEYS.ERROR_CREATE_CHECKOUT_SESSION, { + title: 'Failed to create the checkout session', + message: err?.message, + }); + } finally { + setIsLoading(false); + } + }; return ( - - {(plan.code === PLANCODEENUM.GROWTH || plan.code === PLANCODEENUM.GROWTH_YEARLY) && ( - - Recommended - - )} - {plan.name} - - {plan.price === 0 ? 'Free' : `$${plan.price} / ${isYearly ? 'year' : 'month'}`} - - - - - - - {Object.entries(plan.content).map(([category, items], categoryIndex) => ( - - {category !== 'Features' && {category}} - {items.map(({ check, title, tooltipLink }, index) => ( - - ))} - {category !== 'Features' && categoryIndex < Object.keys(plan.content).length - 1 && } - - ))} - + + {(plan.code === PLANCODEENUM.GROWTH || plan.code === PLANCODEENUM.GROWTH_YEARLY) && ( + + Recommended + + )} +
+ + {plan.name} + + {plan.price === 0 ? 'Free' : `$${plan.price} / ${isYearly ? 'year' : 'month'}`} + + + + + +
+ +
+ + {Object.entries(plan.content).map(([category, items], categoryIndex) => ( + + {category !== 'Features' && {category}} + {items.map(({ check, title, tooltipLink }, index) => ( + + ))} + {category !== 'Features' && categoryIndex < Object.keys(plan.content).length - 1 && } + + ))} + +
); } diff --git a/apps/web/components/UpgradePlan/Plans/PlanFeature.tsx b/apps/web/components/UpgradePlan/Plans/PlanFeature.tsx index e005315c4..0f72bfad0 100644 --- a/apps/web/components/UpgradePlan/Plans/PlanFeature.tsx +++ b/apps/web/components/UpgradePlan/Plans/PlanFeature.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Flex, Text } from '@mantine/core'; -import { CheckCircle } from '@assets/icons/CheckCircle.icon'; -import { CrossCircle } from '@assets/icons/CrossCircle.icon'; +import CrossIcon from '@assets/icons/Cross-filled.Icon'; +import { TickIcon } from '@assets/icons/Tick.icon'; import { TooltipLink } from '@components/guide-point'; interface PlanFeatureProps { @@ -13,7 +13,7 @@ interface PlanFeatureProps { export function PlanFeature({ included, value = '', tooltipLink }: PlanFeatureProps) { return ( - {included ? : } + {included ? : } {value} {tooltipLink && } diff --git a/apps/web/components/UpgradePlan/Plans/Plans.tsx b/apps/web/components/UpgradePlan/Plans/Plans.tsx index 98f373ed4..80490f002 100644 --- a/apps/web/components/UpgradePlan/Plans/Plans.tsx +++ b/apps/web/components/UpgradePlan/Plans/Plans.tsx @@ -21,18 +21,14 @@ export interface Plan { price: number; extraChargeOverheadTenThusandRecords: number; removeBranding: boolean; + recordsImportedPerDollar: number | null; + costPerRecordImport: number | null; + costPerExtraRecordImport: number | null; + sellingPriceOf5KRecordsImport: number | null; content: PlanContent; } -interface PlansProps { - activePlanCode: string; - email: string; - projectId?: string; - canceledOn?: string; - expiryDate?: string; -} - -export function Plans({ activePlanCode, email, projectId }: PlansProps) { +export function Plans() { const [showYearly, setShowYearly] = useState(true); return ( @@ -57,14 +53,7 @@ export function Plans({ activePlanCode, email, projectId }: PlansProps) { {plans[showYearly ? 'yearly' : 'monthly'].map((plan) => ( - + ))} diff --git a/apps/web/components/UpgradePlan/Plans/PlansPricingTable.tsx b/apps/web/components/UpgradePlan/Plans/PlansPricingTable.tsx new file mode 100644 index 000000000..342de1c6c --- /dev/null +++ b/apps/web/components/UpgradePlan/Plans/PlansPricingTable.tsx @@ -0,0 +1,27 @@ +import { Flex, Title, Text } from '@mantine/core'; +import { Plans } from './Plans'; + +interface PlanPricingTableProps { + userProfile: IProfileData; +} + +export function PlanPricingTable({ userProfile }: PlanPricingTableProps) { + if (!userProfile) { + return ( + + Loading profile information... + + ); + } + + return ( + + + Choose the plan that works best for you + + + + + + ); +} diff --git a/apps/web/components/UpgradePlan/Plans/index.ts b/apps/web/components/UpgradePlan/Plans/index.ts index 9b0e035ff..ac4d4e86c 100644 --- a/apps/web/components/UpgradePlan/Plans/index.ts +++ b/apps/web/components/UpgradePlan/Plans/index.ts @@ -1 +1,2 @@ export * from './Plans'; +export * from './PlansPricingTable'; diff --git a/apps/web/components/UpgradePlan/PlansModal.tsx b/apps/web/components/UpgradePlan/PlansModal.tsx deleted file mode 100644 index a6d9353e6..000000000 --- a/apps/web/components/UpgradePlan/PlansModal.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { Flex, Title } from '@mantine/core'; -import { Plans } from './Plans'; - -interface PlanProps { - userProfile: IProfileData; - activePlanCode?: string; - canceledOn?: string; - expiryDate?: string; -} - -export const PlansModal = ({ userProfile, activePlanCode }: PlanProps) => { - return ( - - - Choose the plan that works best for you - - - - - - - ); -}; diff --git a/apps/web/components/home/ImportCount/index.ts b/apps/web/components/home/ImportCount/index.ts deleted file mode 100644 index ad18cd845..000000000 --- a/apps/web/components/home/ImportCount/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ImportCount'; diff --git a/apps/web/components/home/ImportCount/ImportCount.tsx b/apps/web/components/home/ImportStatistics/ImportStatistics.tsx similarity index 98% rename from apps/web/components/home/ImportCount/ImportCount.tsx rename to apps/web/components/home/ImportStatistics/ImportStatistics.tsx index d17086369..4c40e1f45 100644 --- a/apps/web/components/home/ImportCount/ImportCount.tsx +++ b/apps/web/components/home/ImportStatistics/ImportStatistics.tsx @@ -18,7 +18,7 @@ import { useImportCount } from '@hooks/useImportCount'; ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend); -export function ImportCount() { +export function ImportStatistics() { const { importCountData, isImportCountLoading, dates, setDates } = useImportCount(); return ( diff --git a/apps/web/components/home/ImportStatistics/index.ts b/apps/web/components/home/ImportStatistics/index.ts new file mode 100644 index 000000000..e4abc2dc3 --- /dev/null +++ b/apps/web/components/home/ImportStatistics/index.ts @@ -0,0 +1 @@ +export * from './ImportStatistics'; diff --git a/apps/web/components/home/PlanDetails/ActivePlanDetails.tsx b/apps/web/components/home/PlanDetails/ActivePlanDetails.tsx deleted file mode 100644 index d23135006..000000000 --- a/apps/web/components/home/PlanDetails/ActivePlanDetails.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import dayjs from 'dayjs'; -import { TooltipLink } from '@components/guide-point'; -import { Stack, Group, Divider, Title, Alert } from '@mantine/core'; -import { Button } from '@ui/button'; -import { InformationIcon } from '@assets/icons/Information.icon'; - -import { - ActionsEnum, - DATE_FORMATS, - DOCUMENTATION_REFERENCE_LINKS, - MODAL_KEYS, - PLANCODEENUM, - SubjectsEnum, -} from '@config'; -import { ISubscriptionData, numberFormatter } from '@impler/shared'; - -import { PlanDetailCard } from './PlanDetailsCard'; -import { useCancelPlan } from '@hooks/useCancelPlan'; -import { usePlanDetails } from '@hooks/usePlanDetails'; -import useActivePlanDetailsStyle from './ActivePlanDetails.styles'; -import { Can } from 'store/ability.context'; - -interface ActivePlanDetailsProps { - activePlanDetails: ISubscriptionData; - numberOfRecords: number; - showWarning?: boolean; - email?: string; - projectId?: string; - projectName?: string; - showPlans: () => void; -} - -export function ActivePlanDetails({ - activePlanDetails, - numberOfRecords, - showWarning, - showPlans, - projectId, -}: ActivePlanDetailsProps) { - const { classes } = useActivePlanDetailsStyle(); - const { openCancelPlanModal } = useCancelPlan(); - const { onOpenPaymentModal } = usePlanDetails({ - projectId: projectId!, - }); - - return ( - - - {activePlanDetails.plan.name} - - - - {!(activePlanDetails.plan.code === PLANCODEENUM.STARTER || activePlanDetails.plan.canceledOn) && ( - - )} - - - - - - - - - - {Number(activePlanDetails.plan.charge) > 0 && ( - - )} - - - - - - - - - - {activePlanDetails.plan.canceledOn ? ( - } variant="filled" classNames={classes}> - Your Plan cancelled on {dayjs(activePlanDetails.plan.canceledOn).format(DATE_FORMATS.LONG)} and Expire - on {dayjs(activePlanDetails.expiryDate).format(DATE_FORMATS.LONG)} - - ) : ( - activePlanDetails.plan.code !== PLANCODEENUM.STARTER && ( - - ) - )} - - - - - - - ); -} diff --git a/apps/web/components/home/PlanDetails/ActivePlanDetails.styles.ts b/apps/web/components/home/PlanDetails/ActiveSubscriptionDetails.styles.ts similarity index 91% rename from apps/web/components/home/PlanDetails/ActivePlanDetails.styles.ts rename to apps/web/components/home/PlanDetails/ActiveSubscriptionDetails.styles.ts index 66015f4db..904f7c480 100644 --- a/apps/web/components/home/PlanDetails/ActivePlanDetails.styles.ts +++ b/apps/web/components/home/PlanDetails/ActiveSubscriptionDetails.styles.ts @@ -7,6 +7,8 @@ export default createStyles(() => ({ border: `1px solid ${colors.danger}`, borderRadius: '0px', padding: '8px 16px', + maxWidth: '35%', + marginTop: '20px', }, message: { color: colors.danger, diff --git a/apps/web/components/home/PlanDetails/ActiveSubscriptionDetails.tsx b/apps/web/components/home/PlanDetails/ActiveSubscriptionDetails.tsx new file mode 100644 index 000000000..e5a5338a3 --- /dev/null +++ b/apps/web/components/home/PlanDetails/ActiveSubscriptionDetails.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import Link from 'next/link'; +import dayjs from 'dayjs'; +import { useCustomerPortal } from 'subos-frontend'; +import { Stack, Group, ActionIcon, Menu, Alert } from '@mantine/core'; + +import { ActionsEnum, colors, DATE_FORMATS, PLANCODEENUM, SubjectsEnum } from '@config'; +import { ISubscriptionData, numberFormatter } from '@impler/shared'; + +import { useCancelPlan } from '@hooks/useCancelPlan'; +import { Can } from 'store/ability.context'; +import { useAppState } from 'store/app.context'; +import { ViewTransactionIcon } from '@assets/icons/ViewTransaction.icon'; +import { ThreeDotsVerticalIcon } from '@assets/icons/ThreeDotsVertical.icon'; +import { CloseIcon } from '@assets/icons/Close.icon'; +import { PaymentCardIcon } from '@assets/icons/PaymentCard.icon'; +import { PlanDetailCard } from './PlanDetailsCard'; +import { InformationIcon } from '@assets/icons/Information.icon'; +import useActiveSubscriptionDetailsStyle from './ActiveSubscriptionDetails.styles'; + +interface ActivePlanDetailsProps { + activePlanDetails: ISubscriptionData; + numberOfAllocatedRowsInCurrentPlan: number; + showWarning?: boolean; +} + +export function ActiveSubscriptionDetails({ + activePlanDetails, + numberOfAllocatedRowsInCurrentPlan, + showWarning, +}: ActivePlanDetailsProps) { + const { classes } = useActiveSubscriptionDetailsStyle(); + const { profileInfo } = useAppState(); + const { openCustomerPortal } = useCustomerPortal(); + const { openCancelPlanModal } = useCancelPlan(); + + const teamMembersUsed = activePlanDetails?.usage?.TEAM_MEMBERS || 0; + const teamMembersAllocated = activePlanDetails?.meta?.TEAM_MEMBERS ? activePlanDetails.meta.TEAM_MEMBERS - 1 : 0; + + let currentUsedTeamMembers: string | number = Math.max(0, teamMembersUsed); + let allocatedTeamMembers: string | number = Math.max(0, teamMembersAllocated); + + if (allocatedTeamMembers === 0) { + allocatedTeamMembers = 'NA'; + currentUsedTeamMembers = '0'; + } + const isTeamMemberLimitReached = Number(currentUsedTeamMembers) >= Number(allocatedTeamMembers); + + return ( + + + + + + + + + + + + + + } + style={{ color: colors.StrokeLight }} + > + View All Transactions + + + + + openCustomerPortal(profileInfo!.email!)} + icon={} + style={{ color: colors.StrokeLight }} + > + Change Card + + + + + {!activePlanDetails?.plan?.canceledOn && activePlanDetails?.plan?.code !== PLANCODEENUM.STARTER && ( + } + onClick={openCancelPlanModal} + style={{ color: colors.danger }} + > + Cancel Subscription + + )} + + + + + + + + + + + {Number(activePlanDetails.plan.charge) > 0 && ( + + )} + + + + + {activePlanDetails.plan.canceledOn && ( + } variant="filled" classNames={classes}> + Your Plan cancelled on {dayjs(activePlanDetails.plan.canceledOn).format(DATE_FORMATS.LONG)} and Expire on{' '} + {dayjs(activePlanDetails.expiryDate).format(DATE_FORMATS.LONG)} + + )} + + ); +} diff --git a/apps/web/components/home/PlanDetails/CancelSubscriptionModal.tsx b/apps/web/components/home/PlanDetails/CancelSubscriptionModal.tsx index b770f5eed..d6f73e015 100644 --- a/apps/web/components/home/PlanDetails/CancelSubscriptionModal.tsx +++ b/apps/web/components/home/PlanDetails/CancelSubscriptionModal.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Checkbox, Flex, Text, Center, Stack } from '@mantine/core'; +import { Checkbox, Flex, Text, Center, Stack, Textarea } from '@mantine/core'; import { Button } from '@ui/button'; import { Controller } from 'react-hook-form'; import { PlanCancelIllustration } from './illustrations/PlanCancelIllustration'; import { colors, MEMBERSHIP_CANCELLATION_REASONS } from '@config'; +import { AutoHeightComponent } from '@ui/auto-height-component'; interface CancelSubscriptionModalProps { control: any; @@ -69,6 +70,44 @@ export function CancelSubscriptionModal({ )} /> + { + const isSomethingElseSelected = Array.isArray(value) && value.includes('Something else'); + + return ( + + {isSomethingElseSelected && ( + { + if (isSomethingElseSelected && (!val || val.trim() === '')) { + return 'Please specify your other reason for cancellation'; + } + + return true; + }, + }} + render={({ field, fieldState }) => ( +