From 489ac746b056277c06cab71160633cd99e27d5da Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:45:04 -0600 Subject: [PATCH 1/3] feat: Add phone number field to workspaces --- ...240722152617-add-phoneNumber-to-workspaces.js | 16 ++++++++++++++++ .../gateway/dto/initialize-workspace.dto.ts | 7 +++++++ src/modules/gateway/gateway.usecase.spec.ts | 4 ++++ src/modules/gateway/gateway.usecase.ts | 3 ++- .../attributes/workspace.attributes.ts | 1 + .../workspaces/domains/workspaces.domain.ts | 4 ++++ .../workspaces/dto/edit-workspace-details-dto.ts | 11 ++++++++++- src/modules/workspaces/models/workspace.model.ts | 3 +++ src/modules/workspaces/workspaces.usecase.ts | 7 ++++++- 9 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 migrations/20240722152617-add-phoneNumber-to-workspaces.js diff --git a/migrations/20240722152617-add-phoneNumber-to-workspaces.js b/migrations/20240722152617-add-phoneNumber-to-workspaces.js new file mode 100644 index 00000000..3492e404 --- /dev/null +++ b/migrations/20240722152617-add-phoneNumber-to-workspaces.js @@ -0,0 +1,16 @@ +'use strict'; + +const tableName = 'workspaces'; +const newColumn = 'phone_number'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn(tableName, newColumn, { + type: Sequelize.STRING, + }); + }, + + async down(queryInterface) { + await queryInterface.removeColumn(tableName, newColumn); + }, +}; diff --git a/src/modules/gateway/dto/initialize-workspace.dto.ts b/src/modules/gateway/dto/initialize-workspace.dto.ts index c8c42464..f54fbc1d 100644 --- a/src/modules/gateway/dto/initialize-workspace.dto.ts +++ b/src/modules/gateway/dto/initialize-workspace.dto.ts @@ -16,6 +16,13 @@ export class InitializeWorkspaceDto { @IsOptional() address?: string; + @ApiProperty({ + example: '+34 622 111 333', + description: 'Phone number', + }) + @IsOptional() + phoneNumber?: string; + @ApiProperty({ example: 312321312, description: 'Workspace max space in bytes', diff --git a/src/modules/gateway/gateway.usecase.spec.ts b/src/modules/gateway/gateway.usecase.spec.ts index df555234..4cc3e15d 100644 --- a/src/modules/gateway/gateway.usecase.spec.ts +++ b/src/modules/gateway/gateway.usecase.spec.ts @@ -46,6 +46,7 @@ describe('GatewayUseCases', () => { const owner = newUser(); const maxSpaceBytes = 1000000; const workspaceAddress = '123 Main St'; + const workspacePhoneNumber = '+1 (123) 456-7890'; it('When owner does not exist, then it should throw', async () => { jest @@ -56,6 +57,7 @@ describe('GatewayUseCases', () => { ownerId: owner.uuid, maxSpaceBytes, address: workspaceAddress, + phoneNumber: workspacePhoneNumber, numberOfSeats: 20, }; @@ -71,6 +73,7 @@ describe('GatewayUseCases', () => { attributes: { defaultTeamId: newDefaultTeam.id, address: workspaceAddress, + phoneNumber: workspacePhoneNumber, }, }); jest @@ -80,6 +83,7 @@ describe('GatewayUseCases', () => { ownerId: owner.uuid, maxSpaceBytes, address: workspaceAddress, + phoneNumber: workspacePhoneNumber, numberOfSeats: 20, }; await expect( diff --git a/src/modules/gateway/gateway.usecase.ts b/src/modules/gateway/gateway.usecase.ts index f0b04662..7605e32e 100644 --- a/src/modules/gateway/gateway.usecase.ts +++ b/src/modules/gateway/gateway.usecase.ts @@ -22,12 +22,13 @@ export class GatewayUseCases { Logger.log( `Initializing workspace with owner id: ${initializeWorkspaceDto.ownerId}`, ); - const { ownerId, maxSpaceBytes, address, numberOfSeats } = + const { ownerId, maxSpaceBytes, address, numberOfSeats, phoneNumber } = initializeWorkspaceDto; return this.workspaceUseCases.initiateWorkspace(ownerId, maxSpaceBytes, { address, numberOfSeats, + phoneNumber, }); } diff --git a/src/modules/workspaces/attributes/workspace.attributes.ts b/src/modules/workspaces/attributes/workspace.attributes.ts index 5d14c739..7e722654 100644 --- a/src/modules/workspaces/attributes/workspace.attributes.ts +++ b/src/modules/workspaces/attributes/workspace.attributes.ts @@ -9,6 +9,7 @@ export interface WorkspaceAttributes { workspaceUserId: string; setupCompleted: boolean; numberOfSeats: number; + phoneNumber?: string; rootFolderId?: string; createdAt: Date; updatedAt: Date; diff --git a/src/modules/workspaces/domains/workspaces.domain.ts b/src/modules/workspaces/domains/workspaces.domain.ts index a53675f9..7f48ab17 100644 --- a/src/modules/workspaces/domains/workspaces.domain.ts +++ b/src/modules/workspaces/domains/workspaces.domain.ts @@ -14,6 +14,7 @@ export class Workspace implements WorkspaceAttributes { workspaceUserId: string; setupCompleted: boolean; numberOfSeats: number; + phoneNumber?: string; createdAt: Date; updatedAt: Date; @@ -29,6 +30,7 @@ export class Workspace implements WorkspaceAttributes { setupCompleted, avatar, numberOfSeats, + phoneNumber, createdAt, updatedAt, }: WorkspaceAttributes) { @@ -43,6 +45,7 @@ export class Workspace implements WorkspaceAttributes { this.setupCompleted = setupCompleted; this.rootFolderId = rootFolderId; this.numberOfSeats = numberOfSeats; + this.phoneNumber = phoneNumber; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -79,6 +82,7 @@ export class Workspace implements WorkspaceAttributes { avatar: this.avatar, workspaceUserId: this.workspaceUserId, numberOfSeats: this.numberOfSeats, + phoneNumber: this.phoneNumber, setupCompleted: this.setupCompleted, createdAt: this.createdAt, updatedAt: this.updatedAt, diff --git a/src/modules/workspaces/dto/edit-workspace-details-dto.ts b/src/modules/workspaces/dto/edit-workspace-details-dto.ts index b56bc572..b8f5239b 100644 --- a/src/modules/workspaces/dto/edit-workspace-details-dto.ts +++ b/src/modules/workspaces/dto/edit-workspace-details-dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString, Length } from 'class-validator'; +import { IsOptional, IsPhoneNumber, IsString, Length } from 'class-validator'; import { Workspace } from '../domains/workspaces.domain'; export class EditWorkspaceDetailsDto { @@ -21,8 +21,17 @@ export class EditWorkspaceDetailsDto { @IsString() @Length(0, 150) description?: Workspace['description']; + @IsOptional() @IsString() @Length(5, 255) address?: Workspace['address']; + + @ApiProperty({ + example: '+34 622 111 333', + description: 'Phone number', + }) + @IsOptional() + @IsPhoneNumber() + phoneNumber?: Workspace['phoneNumber']; } diff --git a/src/modules/workspaces/models/workspace.model.ts b/src/modules/workspaces/models/workspace.model.ts index 51a2cec6..8f9158d8 100644 --- a/src/modules/workspaces/models/workspace.model.ts +++ b/src/modules/workspaces/models/workspace.model.ts @@ -64,6 +64,9 @@ export class WorkspaceModel extends Model implements WorkspaceAttributes { @Column(DataType.INTEGER) numberOfSeats: number; + @Column(DataType.STRING) + phoneNumber: string; + @HasOne(() => FolderModel, 'uuid') rootFolder: FolderModel; diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index bcf4a899..26afe0f0 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -84,7 +84,11 @@ export class WorkspacesUsecases { async initiateWorkspace( ownerId: UserAttributes['uuid'], maxSpaceBytes: number, - workspaceData: { address?: string; numberOfSeats: number }, + workspaceData: { + address?: string; + numberOfSeats: number; + phoneNumber?: string; + }, ) { const owner = await this.userRepository.findByUuid(ownerId); @@ -129,6 +133,7 @@ export class WorkspacesUsecases { name: 'My Workspace', address: workspaceData?.address, numberOfSeats: workspaceData.numberOfSeats, + phoneNumber: workspaceData?.phoneNumber, avatar: null, defaultTeamId: newDefaultTeam.id, workspaceUserId: workspaceUser.uuid, From 57df9a3fd202af28fea5143b121c3b30d2a65d18 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:02:46 -0600 Subject: [PATCH 2/3] feat: Add updateBillingInfo method to PaymentsService --- src/externals/payments/payments.service.ts | 26 +++++++++++++++++++ src/modules/workspaces/workspaces.module.ts | 4 +++ .../workspaces/workspaces.usecase.spec.ts | 22 ++++++++++++++++ src/modules/workspaces/workspaces.usecase.ts | 22 ++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/src/externals/payments/payments.service.ts b/src/externals/payments/payments.service.ts index 592acc2c..dca4814f 100644 --- a/src/externals/payments/payments.service.ts +++ b/src/externals/payments/payments.service.ts @@ -72,4 +72,30 @@ export class PaymentsService { ); return res.data; } + + async updateBillingInfo( + userUuid: UserAttributes['uuid'], + payload: { + phoneNumber?: string; + address?: string; + }, + ): Promise { + const jwt = Sign( + { payload: { uuid: userUuid } }, + this.configService.get('secrets.jwt'), + ); + + const params = { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + }; + + await this.httpClient.patch( + `${this.configService.get('apis.payments.url')}/billing`, + payload, + params, + ); + } } diff --git a/src/modules/workspaces/workspaces.module.ts b/src/modules/workspaces/workspaces.module.ts index feaf1ea2..7e5b8037 100644 --- a/src/modules/workspaces/workspaces.module.ts +++ b/src/modules/workspaces/workspaces.module.ts @@ -18,6 +18,8 @@ import { AvatarService } from '../../externals/avatar/avatar.service'; import { FolderModule } from '../folder/folder.module'; import { FileModule } from '../file/file.module'; import { SharingModule } from '../sharing/sharing.module'; +import { PaymentsService } from 'src/externals/payments/payments.service'; +import { HttpClientModule } from 'src/externals/http/http.module'; @Module({ imports: [ @@ -35,6 +37,7 @@ import { SharingModule } from '../sharing/sharing.module'; forwardRef(() => SharingModule), BridgeModule, MailerModule, + HttpClientModule, ], controllers: [WorkspacesController], providers: [ @@ -44,6 +47,7 @@ import { SharingModule } from '../sharing/sharing.module'; SequelizeWorkspaceRepository, WorkspaceGuard, AvatarService, + PaymentsService, ], exports: [WorkspacesUsecases, SequelizeModule], }) diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 0f4e7bd2..643c813a 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -49,6 +49,7 @@ import { import { Role } from '../sharing/sharing.domain'; import { WorkspaceAttributes } from './attributes/workspace.attributes'; import * as jwtUtils from '../../lib/jwt'; +import { PaymentsService } from '../../externals/payments/payments.service'; jest.mock('../../middlewares/passport', () => { const originalModule = jest.requireActual('../../middlewares/passport'); @@ -75,6 +76,7 @@ describe('WorkspacesUsecases', () => { let folderUseCases: FolderUseCases; let fileUseCases: FileUseCases; let sharingUseCases: SharingService; + let paymentsService: PaymentsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -101,6 +103,7 @@ describe('WorkspacesUsecases', () => { folderUseCases = module.get(FolderUseCases); fileUseCases = module.get(FileUseCases); sharingUseCases = module.get(SharingService); + paymentsService = module.get(PaymentsService); }); it('should be defined', () => { @@ -403,6 +406,25 @@ describe('WorkspacesUsecases', () => { editWorkspaceDto, ); }); + it('When address or phoneNumber are provided, then it should call payments service', async () => { + jest + .spyOn(workspaceRepository, 'findById') + .mockResolvedValueOnce(workspace); + jest.spyOn(paymentsService, 'updateBillingInfo').mockResolvedValueOnce(); + + await service.editWorkspaceDetails(workspace.id, user, { + address: 'new address', + phoneNumber: 'new phone number', + }); + + expect(paymentsService.updateBillingInfo).toHaveBeenCalledWith( + user.uuid, + { + address: 'new address', + phoneNumber: 'new phone number', + }, + ); + }); }); describe('setupWorkspace', () => { diff --git a/src/modules/workspaces/workspaces.usecase.ts b/src/modules/workspaces/workspaces.usecase.ts index 26afe0f0..82e1a387 100644 --- a/src/modules/workspaces/workspaces.usecase.ts +++ b/src/modules/workspaces/workspaces.usecase.ts @@ -64,6 +64,7 @@ import { import { WorkspaceItemUser } from './domains/workspace-item-user.domain'; import { SharingService } from '../sharing/sharing.service'; import { ChangeUserAssignedSpaceDto } from './dto/change-user-assigned-space.dto'; +import { PaymentsService } from '../../externals/payments/payments.service'; @Injectable() export class WorkspacesUsecases { @@ -71,6 +72,7 @@ export class WorkspacesUsecases { private readonly teamRepository: SequelizeWorkspaceTeamRepository, private readonly workspaceRepository: SequelizeWorkspaceRepository, private readonly sharingUseCases: SharingService, + private readonly paymentService: PaymentsService, private networkService: BridgeService, private userRepository: SequelizeUserRepository, private userUsecases: UserUseCases, @@ -2073,6 +2075,26 @@ export class WorkspacesUsecases { throw new ForbiddenException('You are not the owner of this workspace'); } + if ( + editWorkspaceDetailsDto.phoneNumber || + editWorkspaceDetailsDto.address + ) { + try { + await this.paymentService.updateBillingInfo(workspace.ownerId, { + phoneNumber: editWorkspaceDetailsDto.phoneNumber, + address: editWorkspaceDetailsDto.address, + }); + } catch (error) { + Logger.error( + `[WORKSPACE/EDIT_DETAILS]: Error while updating billing information ${ + (error as Error).message + }`, + ); + throw new InternalServerErrorException( + 'Error while updating billing information', + ); + } + } await this.workspaceRepository.updateBy( { id: workspaceId }, editWorkspaceDetailsDto, From be1f2f3d85098d2942ad23adb4348cd6aa104d59 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:24:11 -0600 Subject: [PATCH 3/3] chore: improve test coverage --- .../workspaces/workspaces.usecase.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/modules/workspaces/workspaces.usecase.spec.ts b/src/modules/workspaces/workspaces.usecase.spec.ts index 643c813a..c1f7cacd 100644 --- a/src/modules/workspaces/workspaces.usecase.spec.ts +++ b/src/modules/workspaces/workspaces.usecase.spec.ts @@ -425,6 +425,31 @@ describe('WorkspacesUsecases', () => { }, ); }); + it('When address or phoneNumber are provided and payments service for some reason fails, it should throw', async () => { + jest + .spyOn(workspaceRepository, 'findById') + .mockResolvedValueOnce(workspace); + jest + .spyOn(paymentsService, 'updateBillingInfo') + .mockRejectedValueOnce(new Error()); + + await expect( + service.editWorkspaceDetails(workspace.id, user, { + address: 'new address', + phoneNumber: 'new phone number', + }), + ).rejects.toThrow(InternalServerErrorException); + + expect(paymentsService.updateBillingInfo).toHaveBeenCalledWith( + user.uuid, + { + address: 'new address', + phoneNumber: 'new phone number', + }, + ); + + expect(workspaceRepository.updateBy).not.toHaveBeenCalled(); + }); }); describe('setupWorkspace', () => {