From 78cea40a2c36188ef807bf7eb66dc859bb182c62 Mon Sep 17 00:00:00 2001 From: Nancy Okeke Date: Wed, 14 Aug 2024 00:41:29 +0100 Subject: [PATCH 01/68] feat(invites): Get all pending Invites --- .vscode/settings.json | 11 ++++++++--- package.json | 1 - src/database/data-source.ts | 1 + src/modules/invite/invite.controller.ts | 6 +++++- src/modules/invite/invite.service.ts | 13 +++++++++++++ src/modules/invite/tests/invite.service.spec.ts | 2 +- .../permissions/entities/permissions.entity.ts | 3 +-- src/modules/role/entities/role.entity.ts | 2 +- 8 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b347739d5..058a5d823 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,9 +10,14 @@ "statusBar.background": "#1a1f15", "statusBarItem.hoverBackground": "#333d2a", "statusBar.foreground": "#e7e7e7", - "panel.border": "#333d2a", - "sideBar.border": "#333d2a", - "editorGroup.border": "#333d2a" + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#333d2a", + "statusBarItem.remoteBackground": "#1a1f15", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#1a1f15", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#1a1f1599", + "titleBar.inactiveForeground": "#e7e7e799" }, "peacock.color": "#1a1f15", "eslint.validate": [ diff --git a/package.json b/package.json index 8ccc3f611..631e77c1a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { - "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", "@nestjs-modules/mailer": "^2.0.2", diff --git a/src/database/data-source.ts b/src/database/data-source.ts index 3075de857..02384de80 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -11,6 +11,7 @@ const dataSource = new DataSource({ username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME, entities: [process.env.DB_ENTITIES], migrations: [process.env.DB_MIGRATIONS], diff --git a/src/modules/invite/invite.controller.ts b/src/modules/invite/invite.controller.ts index b5a2a9787..5bcdb7586 100644 --- a/src/modules/invite/invite.controller.ts +++ b/src/modules/invite/invite.controller.ts @@ -46,7 +46,11 @@ export class InviteController { const allInvites = await this.inviteService.findAllInvitations(); return allInvites; } - + @Get('invites/pending') + async findAllPendingInvitations() { + const allPendingInvites = await this.inviteService.getPendingInvites(); + return allPendingInvites; + } @ApiOperation({ summary: 'Generate Invite Link for an Organization' }) @ApiResponse({ status: 200, diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index b8bf271bc..72d28b6df 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -33,6 +33,19 @@ export class InviteService { private readonly OrganisationService: OrganisationsService ) {} + async getPendingInvites() { + try { + const invites = await this.inviteRepository.find({ where: { isAccepted: false } }); + return { + status_code: HttpStatus.OK, + message: 'Successfully fetched pending invites', + data: invites, + }; + } catch (error) { + throw new InternalServerErrorException(`Internal server error: ${error.message}`); + } + } + async findAllInvitations(): Promise<{ status_code: number; message: string; data: InviteDto[] }> { try { const invites = await this.inviteRepository.find(); diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 90a5936b0..1ce111b82 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -167,7 +167,7 @@ describe('InviteService', () => { it('should throw an internal server error if an exception occurs', async () => { jest.spyOn(repository, 'find').mockRejectedValue(new Error('Test error')); - await expect(service.findAllInvitations()).rejects.toThrow(InternalServerErrorException); + await expect(service.getPendingInvites()).rejects.toThrow(InternalServerErrorException); }); describe('createInvite', () => { diff --git a/src/modules/permissions/entities/permissions.entity.ts b/src/modules/permissions/entities/permissions.entity.ts index 59d2a196d..b5d318b45 100644 --- a/src/modules/permissions/entities/permissions.entity.ts +++ b/src/modules/permissions/entities/permissions.entity.ts @@ -4,9 +4,8 @@ import { Role } from '../../../modules/role/entities/role.entity'; @Entity() export class Permissions extends AbstractBaseEntity { - @Column() + @Column({ default: 'user' }) title: string; - @ManyToMany(() => Role, role => role.permissions) roles: Role[]; } diff --git a/src/modules/role/entities/role.entity.ts b/src/modules/role/entities/role.entity.ts index b182e70ad..d80edcbc9 100644 --- a/src/modules/role/entities/role.entity.ts +++ b/src/modules/role/entities/role.entity.ts @@ -4,7 +4,7 @@ import { Permissions } from '../../permissions/entities/permissions.entity'; @Entity({ name: 'roles' }) export class Role extends AbstractBaseEntity { - @Column() + @Column({ default: 'Ezekiel' }) name: string; @Column({ type: 'text', nullable: true }) From ad714414d968c192d658395281022f3d579b00ce Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Wed, 14 Aug 2024 20:01:48 +0100 Subject: [PATCH 02/68] feat: added translating logic in testimonial module --- package-lock.json | 10 ++++ package.json | 2 +- src/app.module.ts | 5 ++ src/guards/language.guard.ts | 14 ++++++ .../testimonials/testimonials.controller.ts | 40 +++++++++------ .../testimonials/testimonials.module.ts | 3 +- .../testimonials/testimonials.service.ts | 49 ++++++++++++++----- .../tests/testimonials.service.spec.ts | 12 +++++ .../testimonials/tests/update.service.spec.ts | 11 ++++- src/translation/translation.service.ts | 23 +++++++++ 10 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 src/guards/language.guard.ts create mode 100644 src/translation/translation.service.ts diff --git a/package-lock.json b/package-lock.json index 2814bc273..5261303af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "dependencies": { "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", + "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.2.0", @@ -1414,6 +1415,15 @@ "npm": ">=6.14.13" } }, + "node_modules/@google/generative-ai": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.17.0.tgz", + "integrity": "sha512-HNIrX4x6EY5UPOTTDC5DepBFVluognTXR0MTWLiSVsJmzqdEYxGtz/9NWIknUbPNnPZOu7N1BoA/Ho24aPUh3g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", diff --git a/package.json b/package.json index 089c75965..c27e88e30 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { - "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", + "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 766e01a92..ff89196fc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,8 +46,13 @@ import { RunTestsModule } from './run-tests/run-tests.module'; import { BlogCategoryModule } from './modules/blog-category/blog-category.module'; import { ServeStaticModule } from '@nestjs/serve-static'; import { join } from 'path'; +import { LanguageGuard } from './guards/language.guard'; @Module({ providers: [ + { + provide: 'APP_GUARD', + useClass: LanguageGuard, + }, { provide: 'CONFIG', useClass: ConfigService, diff --git a/src/guards/language.guard.ts b/src/guards/language.guard.ts new file mode 100644 index 000000000..6c6701bda --- /dev/null +++ b/src/guards/language.guard.ts @@ -0,0 +1,14 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class LanguageGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const language = request.headers['accept-language'] || 'en'; + request.language = language; + return true; + } +} diff --git a/src/modules/testimonials/testimonials.controller.ts b/src/modules/testimonials/testimonials.controller.ts index 1d538835f..1367fc70a 100644 --- a/src/modules/testimonials/testimonials.controller.ts +++ b/src/modules/testimonials/testimonials.controller.ts @@ -33,8 +33,7 @@ import { UpdateTestimonialResponseDto } from './dto/update-testimonial.response. export class TestimonialsController { constructor( private readonly testimonialsService: TestimonialsService, - - private userService: UserService + private readonly userService: UserService ) {} @Post() @@ -45,13 +44,14 @@ export class TestimonialsController { @ApiResponse({ status: 500, description: 'Internal Server Error' }) async create( @Body() createTestimonialDto: CreateTestimonialDto, - @Req() req: { user: UserPayload } + @Req() req: { user: UserPayload; language: string } ): Promise { - const userId = req?.user.id; + const language = req.language; + const userId = req.user.id; const user = await this.userService.getUserRecord({ identifier: userId, identifierType: 'id' }); - const data = await this.testimonialsService.createTestimonial(createTestimonialDto, user); + const data = await this.testimonialsService.createTestimonial(createTestimonialDto, user, language); return { status: 'success', @@ -60,6 +60,7 @@ export class TestimonialsController { }; } + @Get('user/:user_id') @ApiOperation({ summary: "Get All User's Testimonials" }) @ApiResponse({ status: 200, @@ -76,14 +77,16 @@ export class TestimonialsController { description: 'User has no testimonials', type: GetTestimonials400ErrorResponseDto, }) - @Get('user/:user_id') async getAllTestimonials( @Param('user_id') userId: string, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('page_size', new DefaultValuePipe(3), ParseIntPipe) page_size: number + @Query('page_size', new DefaultValuePipe(3), ParseIntPipe) page_size: number, + @Req() req: { language: string } ) { - return this.testimonialsService.getAllTestimonials(userId, page, page_size); + const language = req.language; + return this.testimonialsService.getAllTestimonials(userId, page, page_size, language); } + @Get(':testimonial_id') @ApiOperation({ summary: 'Get Testimonial By ID' }) @ApiResponse({ @@ -99,8 +102,14 @@ export class TestimonialsController { status: 401, description: 'Unauthorized', }) - async getTestimonialById(@Param('testimonial_id', ParseUUIDPipe) testimonialId: string) { - const testimonial = await this.testimonialsService.getTestimonialById(testimonialId); + async getTestimonialById( + @Param('testimonial_id', ParseUUIDPipe) testimonialId: string, + @Req() req: { language: string } + ) { + const language = req.language; + + const testimonial = await this.testimonialsService.getTestimonialById(testimonialId, language); + return { status_code: HttpStatus.OK, message: 'Testimonial fetched successfully', @@ -128,16 +137,17 @@ export class TestimonialsController { async update( @Param('id') id: string, @Body() updateTestimonialDto: UpdateTestimonialDto, - @Req() req: { user: UserPayload } + @Req() req: { user: UserPayload; language: string } ): Promise { + const language = req.language; const userId = req.user.id; - - const data = await this.testimonialsService.updateTestimonial(id, updateTestimonialDto, userId); - + + const data = await this.testimonialsService.updateTestimonial(id, updateTestimonialDto, userId, language); + return { status: 'success', message: 'Testimonial updated successfully', data, - } + }; } } diff --git a/src/modules/testimonials/testimonials.module.ts b/src/modules/testimonials/testimonials.module.ts index 202e34bb9..238d04db5 100644 --- a/src/modules/testimonials/testimonials.module.ts +++ b/src/modules/testimonials/testimonials.module.ts @@ -7,10 +7,11 @@ import { Testimonial } from './entities/testimonials.entity'; import { TestimonialsController } from './testimonials.controller'; import { TestimonialsService } from './testimonials.service'; import { Profile } from '../profile/entities/profile.entity'; +import { TextService } from 'src/translation/translation.service'; @Module({ imports: [TypeOrmModule.forFeature([Testimonial, User, Profile])], controllers: [TestimonialsController], - providers: [TestimonialsService, Repository, UserService], + providers: [TestimonialsService, Repository, UserService, TextService], }) export class TestimonialsModule {} diff --git a/src/modules/testimonials/testimonials.service.ts b/src/modules/testimonials/testimonials.service.ts index c6e5d97d2..de12f69d3 100644 --- a/src/modules/testimonials/testimonials.service.ts +++ b/src/modules/testimonials/testimonials.service.ts @@ -16,36 +16,46 @@ import { TestimonialMapper } from './mappers/testimonial.mapper'; import { TestimonialResponseMapper } from './mappers/testimonial-response.mapper'; import { TestimonialResponse } from './interfaces/testimonial-response.interface'; import { UpdateTestimonialDto } from './dto/update-testimonial.dto'; +import { TextService } from '../../translation/translation.service'; @Injectable() export class TestimonialsService { constructor( @InjectRepository(Testimonial) private readonly testimonialRepository: Repository, - private userService: UserService + private userService: UserService, + private readonly textService: TextService ) {} - async createTestimonial(createTestimonialDto: CreateTestimonialDto, user) { + + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); + } + + async createTestimonial(createTestimonialDto: CreateTestimonialDto, user, language?: string) { try { const { content, name } = createTestimonialDto; if (!user) { throw new NotFoundException({ status: 'error', - error: 'Not Found', + error: 'User not found', status_code: HttpStatus.NOT_FOUND, }); } + const translatedContent = await this.translateContent(content, language); + const newTestimonial = await this.testimonialRepository.save({ user, name, - content, + content: translatedContent, }); return { id: newTestimonial.id, user_id: user.id, - ...createTestimonialDto, + name: name, + content: translatedContent, created_at: new Date(), }; } catch (error) { @@ -59,7 +69,7 @@ export class TestimonialsService { } } - async getAllTestimonials(userId: string, page: number, pageSize: number) { + async getAllTestimonials(userId: string, page: number, pageSize: number, lang?: string) { const user = await this.userService.getUserRecord({ identifier: userId, identifierType: 'id', @@ -80,7 +90,12 @@ export class TestimonialsService { testimonials = testimonials.slice((page - 1) * pageSize, page * pageSize); - const data = testimonials.map(testimonial => TestimonialMapper.mapToEntity(testimonial)); + const data = await Promise.all( + testimonials.map(async testimonial => { + testimonial.content = await this.translateContent(testimonial.content, lang); + return TestimonialMapper.mapToEntity(testimonial); + }) + ); return { message: SYS_MSG.USER_TESTIMONIALS_FETCHED, @@ -95,7 +110,8 @@ export class TestimonialsService { }, }; } - async getTestimonialById(testimonialId: string): Promise { + + async getTestimonialById(testimonialId: string, lang: string): Promise { const testimonial = await this.testimonialRepository.findOne({ where: { id: testimonialId }, relations: ['user'], @@ -105,19 +121,26 @@ export class TestimonialsService { throw new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND); } + testimonial.content = await this.translateContent(testimonial.content, lang); + return TestimonialResponseMapper.mapToEntity(testimonial); } - async updateTestimonial(id: string, updateTestimonialDto: UpdateTestimonialDto, userId: string) { + async updateTestimonial(id: string, updateTestimonialDto: UpdateTestimonialDto, userId: string, lang?: string) { const testimonial = await this.testimonialRepository.findOne({ where: { id, user: { id: userId } } }); - + if (!testimonial) { throw new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND); } - + Object.assign(testimonial, updateTestimonialDto); + + if (updateTestimonialDto.content) { + testimonial.content = await this.translateContent(updateTestimonialDto.content, lang); + } + await this.testimonialRepository.save(testimonial); - + return { id: testimonial.id, user_id: userId, @@ -125,7 +148,7 @@ export class TestimonialsService { name: testimonial.name, updated_at: new Date(), }; - } + } async deleteTestimonial(id: string) { const testimonial = await this.testimonialRepository.findOne({ where: { id } }); diff --git a/src/modules/testimonials/tests/testimonials.service.spec.ts b/src/modules/testimonials/tests/testimonials.service.spec.ts index 8ad2e2aa6..fd08cb957 100644 --- a/src/modules/testimonials/tests/testimonials.service.spec.ts +++ b/src/modules/testimonials/tests/testimonials.service.spec.ts @@ -12,6 +12,14 @@ import * as SYS_MSG from '../../../helpers/SystemMessages'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; import { mockUser } from '../../organisations/tests/mocks/user.mock'; import { testimonialsMock } from './mocks/testimonials.mock'; +import { TextService } from '../../../translation/translation.service'; + + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('TestimonialsService', () => { let service: TestimonialsService; @@ -23,6 +31,10 @@ describe('TestimonialsService', () => { providers: [ TestimonialsService, UserService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Testimonial), useClass: Repository, diff --git a/src/modules/testimonials/tests/update.service.spec.ts b/src/modules/testimonials/tests/update.service.spec.ts index 82186adcd..c40cd79b1 100644 --- a/src/modules/testimonials/tests/update.service.spec.ts +++ b/src/modules/testimonials/tests/update.service.spec.ts @@ -8,17 +8,26 @@ import UserService from '../../user/user.service'; import { UpdateTestimonialDto } from '../dto/update-testimonial.dto'; import { Testimonial } from '../entities/testimonials.entity'; import { TestimonialsService } from '../testimonials.service'; +import { TextService } from '../../../translation/translation.service'; describe('TestimonialsService', () => { let service: TestimonialsService; let userService: UserService; let testimonialRepository: Repository; - + class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } + } beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ TestimonialsService, UserService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Testimonial), useClass: Repository, diff --git a/src/translation/translation.service.ts b/src/translation/translation.service.ts new file mode 100644 index 000000000..6788de5f4 --- /dev/null +++ b/src/translation/translation.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai'; + +@Injectable() +export class TextService { + private model: GenerativeModel; + + constructor() { + const genAI = new GoogleGenerativeAI(process.env.OPENAI_API_KEY); + this.model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' }); + } + + async translateText(text: string, targetLanguage: string): Promise { + const prompt = `Please provide a direct translation of the following text to ${targetLanguage}, without any additional context or explanations:\n\n"${text}"`; + + const result = await this.model.generateContent(prompt); + const response = await result.response; + let translatedText = await response.text(); + translatedText = translatedText.replace(/^"|"$/g, '').trim(); + + return translatedText; + } +} From 5d059fa2c374c94d6b3a3d6d55c554d626cbf665 Mon Sep 17 00:00:00 2001 From: Kemzzy-Dev Date: Wed, 14 Aug 2024 20:15:01 +0100 Subject: [PATCH 03/68] ci: reverting back to process manager deployment --- .github/{workflows => Unused}/pr-deploy.yaml | 0 .github/workflows/dev-deployment.yaml | 48 ------------------- .github/workflows/main-deployment.yaml | 22 +++++++++ .github/workflows/production-deployment.yaml | 47 ------------------ .github/workflows/staging-deployment.yaml | 36 ++------------ .../compose.override.yaml | 0 compose.yaml => compose/compose.yaml | 8 ++-- deploy.sh => compose/deploy.sh | 0 deployment.sh | 23 +++++++++ main-ecosystem-config.json | 4 +- staging-ecosystem-config.json | 4 +- 11 files changed, 58 insertions(+), 134 deletions(-) rename .github/{workflows => Unused}/pr-deploy.yaml (100%) delete mode 100644 .github/workflows/dev-deployment.yaml create mode 100644 .github/workflows/main-deployment.yaml delete mode 100644 .github/workflows/production-deployment.yaml rename compose.override.yaml => compose/compose.override.yaml (100%) rename compose.yaml => compose/compose.yaml (87%) rename deploy.sh => compose/deploy.sh (100%) create mode 100644 deployment.sh diff --git a/.github/workflows/pr-deploy.yaml b/.github/Unused/pr-deploy.yaml similarity index 100% rename from .github/workflows/pr-deploy.yaml rename to .github/Unused/pr-deploy.yaml diff --git a/.github/workflows/dev-deployment.yaml b/.github/workflows/dev-deployment.yaml deleted file mode 100644 index 840a32fd6..000000000 --- a/.github/workflows/dev-deployment.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: Dev Deployment - -on: - workflow_dispatch: - push: - branches: - - dev - -jobs: - build-and-push: - if: github.event.repository.fork == false - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build Docker image - run: docker build -t nestjs_dev:green . - - - name: Save and compress Docker image - run: | - docker save nestjs_dev:green | gzip > nestjs_dev.tar.gz - - - name: Copy image to server - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - password: ${{ secrets.PASSWORD }} - source: "nestjs_dev.tar.gz" - target: "/tmp" - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - environment: - name: "dev" - url: ${{ vars.URL }} - steps: - - name: Deploy on server - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - password: ${{ secrets.PASSWORD }} - script: | - cd ~/hng_boilerplate_nestjs - ./deploy.sh dev diff --git a/.github/workflows/main-deployment.yaml b/.github/workflows/main-deployment.yaml new file mode 100644 index 000000000..e3569d46e --- /dev/null +++ b/.github/workflows/main-deployment.yaml @@ -0,0 +1,22 @@ +name: Production Deployment + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + deploy-prod: + runs-on: bingo-server + steps: + - name: Deploy on server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + script: | + cd main + chmod +x deployment.sh + ./deployment.sh main diff --git a/.github/workflows/production-deployment.yaml b/.github/workflows/production-deployment.yaml deleted file mode 100644 index 7ce95863d..000000000 --- a/.github/workflows/production-deployment.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: Production Deployment - -on: - workflow_dispatch: - push: - branches: - - main - -jobs: - build-and-push: - if: github.event.repository.fork == false - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build Docker image - run: docker build -t nestjs_prod:green . - - - name: Save and compress Docker image - run: | - docker save nestjs_prod:green | gzip > nestjs_prod.tar.gz - - - name: Copy image to server - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - password: ${{ secrets.PASSWORD }} - source: "nestjs_prod.tar.gz" - target: "/tmp" - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - environment: - name: "production" - url: ${{ vars.URL }} - steps: - - name: Deploy on server - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - password: ${{ secrets.PASSWORD }} - script: | - ./deploy.sh prod diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index 7e7af63ed..cebd7ec25 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -7,35 +7,8 @@ on: - staging jobs: - build-and-push: - if: github.event.repository.fork == false - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build Docker image - run: docker build -t nestjs_staging:green . - - - name: Save and compress Docker image - run: | - docker save nestjs_staging:green | gzip > nestjs_staging.tar.gz - - - name: Copy image to server - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - password: ${{ secrets.PASSWORD }} - source: 'nestjs_staging.tar.gz' - target: '/tmp' - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - environment: - name: 'staging' - url: ${{ vars.URL }} + deploy-staging: + runs-on: bingo-server steps: - name: Deploy on server uses: appleboy/ssh-action@master @@ -44,5 +17,6 @@ jobs: username: ${{ secrets.USERNAME }} password: ${{ secrets.PASSWORD }} script: | - cd ~/hng_boilerplate_nestjs - ./deploy.sh staging + cd staging + chmod +x deployment.sh + ./deployment.sh staging diff --git a/compose.override.yaml b/compose/compose.override.yaml similarity index 100% rename from compose.override.yaml rename to compose/compose.override.yaml diff --git a/compose.yaml b/compose/compose.yaml similarity index 87% rename from compose.yaml rename to compose/compose.yaml index 80344a477..335a2e94f 100644 --- a/compose.yaml +++ b/compose/compose.yaml @@ -12,7 +12,7 @@ services: redis: condition: service_healthy healthcheck: - test: "wget -qO- http://app:${PORT}" + test: 'wget -qO- http://app:${PORT}' interval: 10s timeout: 10s retries: 3 @@ -28,7 +28,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: "pg_isready -U postgres" + test: 'pg_isready -U postgres' interval: 5s timeout: 5s retries: 3 @@ -41,7 +41,7 @@ services: volumes: - redis_data:/data healthcheck: - test: "redis-cli ping | grep PONG" + test: 'redis-cli ping | grep PONG' interval: 5s timeout: 5s retries: 3 @@ -55,7 +55,7 @@ services: app: condition: service_healthy healthcheck: - test: "wget -qO- http://nginx:80" + test: 'wget -qO- http://nginx:80' interval: 5s timeout: 5s retries: 3 diff --git a/deploy.sh b/compose/deploy.sh similarity index 100% rename from deploy.sh rename to compose/deploy.sh diff --git a/deployment.sh b/deployment.sh new file mode 100644 index 000000000..d23d9bd37 --- /dev/null +++ b/deployment.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +BRANCH=$1 + +# checkout +git checkout $BRANCH; + +git pull origin $BRANCH; + +# install dependencies +npm install --include=dev; + +# run build +npm run build; + + +git stash + +# run migration +npm run migration:run; + +# run start +pm2 restart $BRANCH-ecosystem-config.json || pm2 start $BRANCH-ecosystem-config.json; diff --git a/main-ecosystem-config.json b/main-ecosystem-config.json index 220d76794..d31030f0d 100644 --- a/main-ecosystem-config.json +++ b/main-ecosystem-config.json @@ -1,11 +1,11 @@ { "apps": [ { - "name": "team-alpha-prod", + "name": "nestjs-prod", "script": "npm run start:prod", "log_file": "~/.pm2/logs/team-alpha-prod-out.log", "combine_logs": true, "log_date_format": "YYYY-MM-DD HH:mm:ss Z" } ] -} \ No newline at end of file +} diff --git a/staging-ecosystem-config.json b/staging-ecosystem-config.json index d8fa81c86..48d1d822b 100644 --- a/staging-ecosystem-config.json +++ b/staging-ecosystem-config.json @@ -1,11 +1,11 @@ { "apps": [ { - "name": "team-alpha-staging", + "name": "nestjs2-staging", "script": "npm run start:prod", "log_file": "~/.pm2/logs/team-alpha-staging-out.log", "combine_logs": true, "log_date_format": "YYYY-MM-DD HH:mm:ss Z" } ] -} \ No newline at end of file +} From ee63fc4522a53210ed00468f26bc8c4b92bd77bf Mon Sep 17 00:00:00 2001 From: Kemzzy-Dev Date: Wed, 14 Aug 2024 20:18:41 +0100 Subject: [PATCH 04/68] ci: reverting back to process manager deployment --- .github/Unused/pr-deploy.yaml | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/Unused/pr-deploy.yaml b/.github/Unused/pr-deploy.yaml index 19f6e69ea..3bf206500 100644 --- a/.github/Unused/pr-deploy.yaml +++ b/.github/Unused/pr-deploy.yaml @@ -1,27 +1,27 @@ -name: PR Deploy -on: - pull_request_target: - branches: - - dev +# name: PR Deploy +# on: +# pull_request_target: +# branches: +# - dev -jobs: - deploy-pr-for-testing: - environment: - name: preview - url: ${{ steps.deploy.outputs.preview-url }} - runs-on: ubuntu-latest - steps: - - name: Checkout to branch - uses: actions/checkout@v4 - - id: deploy - name: Pull Request Deploy - uses: hngprojects/pr-deploy@dev - with: - server_host: ${{ secrets.HOST }} - server_username: ${{ secrets.USERNAME }} - server_password: ${{ secrets.PASSWORD }} - comment: false - context: . - dockerfile: Dockerfile - exposed_port: 5000 - github_token: ${{ secrets.GITHUB_TOKEN }} +# jobs: +# deploy-pr-for-testing: +# environment: +# name: preview +# url: ${{ steps.deploy.outputs.preview-url }} +# runs-on: ubuntu-latest +# steps: +# - name: Checkout to branch +# uses: actions/checkout@v4 +# - id: deploy +# name: Pull Request Deploy +# uses: hngprojects/pr-deploy@dev +# with: +# server_host: ${{ secrets.HOST }} +# server_username: ${{ secrets.USERNAME }} +# server_password: ${{ secrets.PASSWORD }} +# comment: false +# context: . +# dockerfile: Dockerfile +# exposed_port: 5000 +# github_token: ${{ secrets.GITHUB_TOKEN }} From 93160b26b524bffff098fb0f712ecc09007523fd Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Wed, 14 Aug 2024 20:19:52 +0100 Subject: [PATCH 05/68] fix: import error --- src/modules/testimonials/testimonials.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/testimonials/testimonials.module.ts b/src/modules/testimonials/testimonials.module.ts index 238d04db5..5f6609494 100644 --- a/src/modules/testimonials/testimonials.module.ts +++ b/src/modules/testimonials/testimonials.module.ts @@ -7,7 +7,7 @@ import { Testimonial } from './entities/testimonials.entity'; import { TestimonialsController } from './testimonials.controller'; import { TestimonialsService } from './testimonials.service'; import { Profile } from '../profile/entities/profile.entity'; -import { TextService } from 'src/translation/translation.service'; +import { TextService } from '../../translation/translation.service'; @Module({ imports: [TypeOrmModule.forFeature([Testimonial, User, Profile])], From 6084bbd0a3839e495532c6281d13a29c946882d7 Mon Sep 17 00:00:00 2001 From: Kemzzy-Dev Date: Wed, 14 Aug 2024 20:24:48 +0100 Subject: [PATCH 06/68] ci: reverting back to process manager deployment --- .github/workflows/main-deployment.yaml | 2 +- .github/workflows/staging-deployment.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-deployment.yaml b/.github/workflows/main-deployment.yaml index e3569d46e..429166dcd 100644 --- a/.github/workflows/main-deployment.yaml +++ b/.github/workflows/main-deployment.yaml @@ -8,7 +8,7 @@ on: jobs: deploy-prod: - runs-on: bingo-server + runs-on: ubuntu-latest steps: - name: Deploy on server uses: appleboy/ssh-action@master diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index cebd7ec25..7452e27c9 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -8,7 +8,7 @@ on: jobs: deploy-staging: - runs-on: bingo-server + runs-on: ubuntu-latest steps: - name: Deploy on server uses: appleboy/ssh-action@master From edc4dceb378c54cda8878e1b4e086d8cc2590b45 Mon Sep 17 00:00:00 2001 From: Kemzzy-Dev Date: Wed, 14 Aug 2024 21:00:05 +0100 Subject: [PATCH 07/68] ci: reverting back to process manager deployment --- .github/workflows/main-deployment.yaml | 1 - .github/workflows/staging-deployment.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/main-deployment.yaml b/.github/workflows/main-deployment.yaml index 429166dcd..b1457f620 100644 --- a/.github/workflows/main-deployment.yaml +++ b/.github/workflows/main-deployment.yaml @@ -1,7 +1,6 @@ name: Production Deployment on: - workflow_dispatch: push: branches: - main diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index 7452e27c9..9d441dd67 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -1,7 +1,6 @@ name: Staging Deployment on: - workflow_dispatch: push: branches: - staging From 24283f8ce4acaf9b762a853cfaec292b24c64b0e Mon Sep 17 00:00:00 2001 From: Nancy Okeke Date: Wed, 14 Aug 2024 23:21:06 +0100 Subject: [PATCH 08/68] fix(invites): fixed the service.spec.ts --- src/modules/invite/invite.service.ts | 40 +++++++++++++++++-- .../invite/tests/invite.service.spec.ts | 21 ++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index 72d28b6df..624756536 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -33,14 +33,46 @@ export class InviteService { private readonly OrganisationService: OrganisationsService ) {} - async getPendingInvites() { + // async getPendingInvites() { + // try { + // const invites = await this.inviteRepository.find({ where: { isAccepted: false } }); + // return { + // status_code: HttpStatus.OK, + // message: 'Successfully fetched pending invites', + // data: invites, + // }; + // } catch (error) { + // throw new InternalServerErrorException(`Internal server error: ${error.message}`); + // } + // } + + async getPendingInvites(): Promise<{ status_code: number; message: string; data: InviteDto[] }> { try { - const invites = await this.inviteRepository.find({ where: { isAccepted: false } }); - return { + // Fetch invites where isAccepted is false + const pendingInvites = await this.inviteRepository.find({ + where: { isAccepted: false }, + }); + + // Map the result to the InviteDto format + const pendingInvitesDto: InviteDto[] = pendingInvites.map(invite => { + return { + token: invite.token, + id: invite.id, + isAccepted: invite.isAccepted, + isGeneric: invite.isGeneric, + organisation: invite.organisation, + email: invite.email, + }; + }); + + // Return the response with the mapped data + const responseData = { status_code: HttpStatus.OK, message: 'Successfully fetched pending invites', - data: invites, + data: pendingInvitesDto, }; + + return responseData; } catch (error) { throw new InternalServerErrorException(`Internal server error: ${error.message}`); } diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 1ce111b82..9616876d1 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -170,6 +170,27 @@ describe('InviteService', () => { await expect(service.getPendingInvites()).rejects.toThrow(InternalServerErrorException); }); + it('should fetch all pending invites where isAccepted is false', async () => { + const pendingInvitesMock = mockInvites.filter(invite => invite.isAccepted === false); + + jest.spyOn(repository, 'find').mockResolvedValue(pendingInvitesMock); + + const result = await service.getPendingInvites(); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: 'Successfully fetched pending invites', + data: pendingInvitesMock.map(invite => ({ + token: invite.token, + id: invite.id, + isAccepted: invite.isAccepted, + isGeneric: invite.isGeneric, + organisation: invite.organisation, + email: invite.email, + })), + }); + }); + describe('createInvite', () => { it('should create an invite and return a link', async () => { const mockToken = 'mock-uuid'; From 8b949fe470c1c28d7a5975d35406d241b70d9e00 Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Thu, 15 Aug 2024 02:41:33 +0100 Subject: [PATCH 09/68] fix: added css inline --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c27e88e30..089c75965 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { + "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", - "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.2.0", From 969759d2993b7a7acc89344fb1f5787403e4f1ea Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Thu, 15 Aug 2024 02:50:22 +0100 Subject: [PATCH 10/68] fix: lintin error --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 089c75965..b83f48286 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", + "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.2.0", From 3b19f68010e5dbf8575de819b0702a42cc3a181f Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Thu, 15 Aug 2024 14:01:19 +0100 Subject: [PATCH 11/68] feat: added translation to the FAQ module --- src/modules/faq/faq.controller.ts | 24 +++-- src/modules/faq/faq.module.ts | 4 +- src/modules/faq/faq.service.ts | 96 +++++++++++++------ .../faq/test/faq.create.service.spec.ts | 12 +++ src/modules/faq/test/faq.service.spec.ts | 12 +++ 5 files changed, 111 insertions(+), 37 deletions(-) diff --git a/src/modules/faq/faq.controller.ts b/src/modules/faq/faq.controller.ts index ea536255f..a5274eac8 100644 --- a/src/modules/faq/faq.controller.ts +++ b/src/modules/faq/faq.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, UsePipes, ValidationPipe, Get, Put, Param, Delete, UseGuards } from '@nestjs/common'; +import { Controller, Post, Body, Get, Put, Param, Delete, UseGuards, Req, Query } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBearerAuth } from '@nestjs/swagger'; import { FaqService } from './faq.service'; import { CreateFaqDto } from './dto/create-faq.dto'; @@ -30,8 +30,12 @@ export class FaqController { description: 'Internal Server Error if an unexpected error occurs.', }) @ApiBearerAuth() - async create(@Body() createFaqDto: CreateFaqDto): Promise { - const faq: IFaq = await this.faqService.create(createFaqDto); + async create( + @Body() createFaqDto: CreateFaqDto, + @Req() req: any + ): Promise { + const language = req.language; + const faq: IFaq = await this.faqService.create(createFaqDto, language); return { status_code: 201, success: true, @@ -42,8 +46,9 @@ export class FaqController { @skipAuth() @Get() @ApiOperation({ summary: 'Get all frequently asked questions' }) - async findAll() { - return this.faqService.findAllFaq(); + async findAll(@Req() req: any) { + const language = req.language; + return this.faqService.findAllFaq(language); } @ApiBearerAuth() @@ -53,8 +58,13 @@ export class FaqController { @ApiParam({ name: 'id', type: 'string' }) @ApiResponse({ status: 200, description: 'The FAQ has been successfully updated.', type: Faq }) @ApiResponse({ status: 400, description: 'Bad Request.' }) - async update(@Param('id') id: string, @Body() updateFaqDto: UpdateFaqDto) { - return this.faqService.updateFaq(id, updateFaqDto); + async update( + @Param('id') id: string, + @Body() updateFaqDto: UpdateFaqDto, + @Req() req: any + ) { + const language = req.language; + return this.faqService.updateFaq(id, updateFaqDto, language); } @ApiBearerAuth() diff --git a/src/modules/faq/faq.module.ts b/src/modules/faq/faq.module.ts index bb9d432d1..c8e8f203e 100644 --- a/src/modules/faq/faq.module.ts +++ b/src/modules/faq/faq.module.ts @@ -7,10 +7,12 @@ import { User } from '../user/entities/user.entity'; import { Organisation } from '../organisations/entities/organisations.entity'; import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; import { Role } from '../role/entities/role.entity'; +import { TextService } from '../../translation/translation.service'; + @Module({ imports: [TypeOrmModule.forFeature([Faq, User, Organisation, OrganisationUserRole, Role])], controllers: [FaqController], - providers: [FaqService], + providers: [FaqService,TextService], }) export class FaqModule {} diff --git a/src/modules/faq/faq.service.ts b/src/modules/faq/faq.service.ts index a31c8b716..30d81900d 100644 --- a/src/modules/faq/faq.service.ts +++ b/src/modules/faq/faq.service.ts @@ -1,43 +1,73 @@ -import { BadRequestException, Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + InternalServerErrorException +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Faq } from './entities/faq.entity'; import { CreateFaqDto } from './dto/create-faq.dto'; import { IFaq } from './faq.interface'; import { UpdateFaqDto } from './dto/update-faq.dto'; +import { TextService } from '../../translation/translation.service'; @Injectable() export class FaqService { constructor( @InjectRepository(Faq) - private faqRepository: Repository + private faqRepository: Repository, + private readonly textService: TextService, ) {} - async create(createFaqDto: CreateFaqDto): Promise { - const faq = this.faqRepository.create(createFaqDto); - return this.faqRepository.save(faq); + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); } - async findAllFaq() { + + async create(createFaqDto: CreateFaqDto, language?: string): Promise { + try { + const { question, answer, category } = createFaqDto; + + const translatedQuestion = await this.translateContent(question, language); + const translatedAnswer = await this.translateContent(answer, language); + const translatedCategory = await this.translateContent(category, language); + + const faq = this.faqRepository.create({ + ...createFaqDto, + question: translatedQuestion, + answer: translatedAnswer, + category: translatedCategory, + }); + + return await this.faqRepository.save(faq); + } catch (error) { + throw new InternalServerErrorException(`Failed to create FAQ: ${error.message}`); + } + } + + async findAllFaq(language?: string) { try { const faqs = await this.faqRepository.find(); + + const translatedFaqs = await Promise.all( + faqs.map(async (faq) => { + faq.question = await this.translateContent(faq.question, language); + faq.answer = await this.translateContent(faq.answer, language); + faq.category = await this.translateContent(faq.category, language); + return faq; + }), + ); + return { message: 'Faq fetched successfully', status_code: 200, - data: faqs, + data: translatedFaqs, }; } catch (error) { - if (error instanceof BadRequestException) { - return { - message: 'Invalid request', - status_code: 400, - }; - } else if (error instanceof InternalServerErrorException) { - throw error; - } + throw new InternalServerErrorException(`Failed to fetch FAQs: ${error.message}`); } } - async updateFaq(id: string, updateFaqDto: UpdateFaqDto) { + async updateFaq(id: string, updateFaqDto: UpdateFaqDto, language?: string) { const faq = await this.faqRepository.findOne({ where: { id } }); if (!faq) { throw new BadRequestException({ @@ -45,9 +75,27 @@ export class FaqService { status_code: 400, }); } + try { - Object.assign(faq, updateFaqDto); - const updatedFaq = await this.faqRepository.save(faq); + const updatedFaq = { + ...faq, + ...updateFaqDto, + }; + + if (updateFaqDto.question) { + updatedFaq.question = await this.translateContent(updateFaqDto.question, language); + } + + if (updateFaqDto.answer) { + updatedFaq.answer = await this.translateContent(updateFaqDto.answer, language); + } + + if (updateFaqDto.category) { + updatedFaq.category = await this.translateContent(updateFaqDto.category, language); + } + + await this.faqRepository.save(updatedFaq); + return { id: updatedFaq.id, question: updatedFaq.question, @@ -55,17 +103,7 @@ export class FaqService { category: updatedFaq.category, }; } catch (error) { - if (error instanceof UnauthorizedException) { - return { - message: 'Unauthorized access', - status_code: 401, - }; - } else if (error instanceof BadRequestException) { - return { - message: 'Invalid request data', - status_code: 400, - }; - } + throw new InternalServerErrorException(`Failed to update FAQ: ${error.message}`); } } diff --git a/src/modules/faq/test/faq.create.service.spec.ts b/src/modules/faq/test/faq.create.service.spec.ts index 3135e421d..e20c68ff7 100644 --- a/src/modules/faq/test/faq.create.service.spec.ts +++ b/src/modules/faq/test/faq.create.service.spec.ts @@ -4,6 +4,14 @@ import { Repository } from 'typeorm'; import { Faq } from '../entities/faq.entity'; import { CreateFaqDto } from '../dto/create-faq.dto'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { TextService } from '../../../translation/translation.service'; + + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('FaqService', () => { let service: FaqService; @@ -24,6 +32,10 @@ describe('FaqService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FaqService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Faq), useValue: mockFaqRepository, diff --git a/src/modules/faq/test/faq.service.spec.ts b/src/modules/faq/test/faq.service.spec.ts index b7b7a7f7c..1d8fbc282 100644 --- a/src/modules/faq/test/faq.service.spec.ts +++ b/src/modules/faq/test/faq.service.spec.ts @@ -4,6 +4,14 @@ import { Repository } from 'typeorm'; import { FaqService } from '../faq.service'; import { Faq } from '../entities/faq.entity'; import { BadRequestException } from '@nestjs/common'; +import { TextService } from '../../../translation/translation.service'; + + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('FaqService', () => { let service: FaqService; @@ -20,6 +28,10 @@ describe('FaqService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FaqService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Faq), useValue: mockRepository, From d17b34024531668320c138eea2bc3c2aa823c37c Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Thu, 15 Aug 2024 15:33:35 +0100 Subject: [PATCH 12/68] fix: removed try catchblock from service --- src/modules/faq/faq.service.ts | 118 +++++++++++++++------------------ 1 file changed, 53 insertions(+), 65 deletions(-) diff --git a/src/modules/faq/faq.service.ts b/src/modules/faq/faq.service.ts index 30d81900d..e91486c6b 100644 --- a/src/modules/faq/faq.service.ts +++ b/src/modules/faq/faq.service.ts @@ -24,47 +24,39 @@ export class FaqService { } async create(createFaqDto: CreateFaqDto, language?: string): Promise { - try { - const { question, answer, category } = createFaqDto; - - const translatedQuestion = await this.translateContent(question, language); - const translatedAnswer = await this.translateContent(answer, language); - const translatedCategory = await this.translateContent(category, language); - - const faq = this.faqRepository.create({ - ...createFaqDto, - question: translatedQuestion, - answer: translatedAnswer, - category: translatedCategory, - }); + const { question, answer, category } = createFaqDto; - return await this.faqRepository.save(faq); - } catch (error) { - throw new InternalServerErrorException(`Failed to create FAQ: ${error.message}`); - } + const translatedQuestion = await this.translateContent(question, language); + const translatedAnswer = await this.translateContent(answer, language); + const translatedCategory = await this.translateContent(category, language); + + const faq = this.faqRepository.create({ + ...createFaqDto, + question: translatedQuestion, + answer: translatedAnswer, + category: translatedCategory, + }); + + return this.faqRepository.save(faq); } async findAllFaq(language?: string) { - try { - const faqs = await this.faqRepository.find(); - - const translatedFaqs = await Promise.all( - faqs.map(async (faq) => { - faq.question = await this.translateContent(faq.question, language); - faq.answer = await this.translateContent(faq.answer, language); - faq.category = await this.translateContent(faq.category, language); - return faq; - }), - ); - - return { - message: 'Faq fetched successfully', - status_code: 200, - data: translatedFaqs, - }; - } catch (error) { - throw new InternalServerErrorException(`Failed to fetch FAQs: ${error.message}`); - } + const faqs = await this.faqRepository.find(); + + const translatedFaqs = await Promise.all( + faqs.map(async (faq) => { + faq.question = await this.translateContent(faq.question, language); + faq.answer = await this.translateContent(faq.answer, language); + faq.category = await this.translateContent(faq.category, language); + return faq; + }), + ); + + return { + message: 'Faq fetched successfully', + status_code: 200, + data: translatedFaqs, + }; } async updateFaq(id: string, updateFaqDto: UpdateFaqDto, language?: string) { @@ -76,35 +68,31 @@ export class FaqService { }); } - try { - const updatedFaq = { - ...faq, - ...updateFaqDto, - }; - - if (updateFaqDto.question) { - updatedFaq.question = await this.translateContent(updateFaqDto.question, language); - } - - if (updateFaqDto.answer) { - updatedFaq.answer = await this.translateContent(updateFaqDto.answer, language); - } - - if (updateFaqDto.category) { - updatedFaq.category = await this.translateContent(updateFaqDto.category, language); - } - - await this.faqRepository.save(updatedFaq); - - return { - id: updatedFaq.id, - question: updatedFaq.question, - answer: updatedFaq.answer, - category: updatedFaq.category, - }; - } catch (error) { - throw new InternalServerErrorException(`Failed to update FAQ: ${error.message}`); + const updatedFaq = { + ...faq, + ...updateFaqDto, + }; + + if (updateFaqDto.question) { + updatedFaq.question = await this.translateContent(updateFaqDto.question, language); + } + + if (updateFaqDto.answer) { + updatedFaq.answer = await this.translateContent(updateFaqDto.answer, language); + } + + if (updateFaqDto.category) { + updatedFaq.category = await this.translateContent(updateFaqDto.category, language); } + + await this.faqRepository.save(updatedFaq); + + return { + id: updatedFaq.id, + question: updatedFaq.question, + answer: updatedFaq.answer, + category: updatedFaq.category, + }; } async removeFaq(id: string) { From e973dfbdcb06ab82b9ff10c01a73d3a465634ed6 Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Thu, 15 Aug 2024 15:35:56 +0100 Subject: [PATCH 13/68] fix: removed try catchblock from service --- src/modules/faq/faq.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/faq/faq.service.ts b/src/modules/faq/faq.service.ts index e91486c6b..735a7bd7d 100644 --- a/src/modules/faq/faq.service.ts +++ b/src/modules/faq/faq.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable, - InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; From 64888685a04375598d94da84946764931e3c4d2e Mon Sep 17 00:00:00 2001 From: Nancy Okeke Date: Thu, 15 Aug 2024 16:05:52 +0100 Subject: [PATCH 14/68] fix(invite): Fixed some errors in the service file --- .vscode/settings.json | 11 +++-------- package.json | 1 + src/database/data-source.ts | 1 - src/modules/invite/dto/pending-invitations.ts | 13 +++++++++++++ src/modules/invite/invite.controller.ts | 12 ++++++++++++ src/modules/invite/invite.service.ts | 17 ----------------- .../permissions/entities/permissions.entity.ts | 2 +- src/modules/role/entities/role.entity.ts | 2 +- 8 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 src/modules/invite/dto/pending-invitations.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 058a5d823..b347739d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,14 +10,9 @@ "statusBar.background": "#1a1f15", "statusBarItem.hoverBackground": "#333d2a", "statusBar.foreground": "#e7e7e7", - "commandCenter.border": "#e7e7e799", - "sash.hoverBorder": "#333d2a", - "statusBarItem.remoteBackground": "#1a1f15", - "statusBarItem.remoteForeground": "#e7e7e7", - "titleBar.activeBackground": "#1a1f15", - "titleBar.activeForeground": "#e7e7e7", - "titleBar.inactiveBackground": "#1a1f1599", - "titleBar.inactiveForeground": "#e7e7e799" + "panel.border": "#333d2a", + "sideBar.border": "#333d2a", + "editorGroup.border": "#333d2a" }, "peacock.color": "#1a1f15", "eslint.validate": [ diff --git a/package.json b/package.json index 631e77c1a..8ccc3f611 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { + "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", "@nestjs-modules/mailer": "^2.0.2", diff --git a/src/database/data-source.ts b/src/database/data-source.ts index 02384de80..3075de857 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -11,7 +11,6 @@ const dataSource = new DataSource({ username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME, entities: [process.env.DB_ENTITIES], migrations: [process.env.DB_MIGRATIONS], diff --git a/src/modules/invite/dto/pending-invitations.ts b/src/modules/invite/dto/pending-invitations.ts new file mode 100644 index 000000000..6ca80b7ec --- /dev/null +++ b/src/modules/invite/dto/pending-invitations.ts @@ -0,0 +1,13 @@ +import { InviteDto } from './invite.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FindAllPendingInvitationsResponseDto { + @ApiProperty({ example: 200 }) + status_code: number; + + @ApiProperty({ example: 'Successfully fetched pending invites' }) + message: string; + + @ApiProperty({ type: [InviteDto] }) + data: InviteDto[]; +} diff --git a/src/modules/invite/invite.controller.ts b/src/modules/invite/invite.controller.ts index 5bcdb7586..98dc7df9c 100644 --- a/src/modules/invite/invite.controller.ts +++ b/src/modules/invite/invite.controller.ts @@ -18,6 +18,7 @@ import { CreateInvitationDto } from './dto/create-invite.dto'; import { Response } from 'express'; import { CreateInviteResponseDto } from './dto/creat-invite-response.dto'; import { FindAllInvitationsResponseDto } from './dto/all-invitations-response.dto'; +import { FindAllPendingInvitationsResponseDto } from './dto/pending-invitations'; import { ErrorResponseDto } from './dto/invite-error-response.dto'; import { SendInvitationsResponseDto } from './dto/send-invitations-response.dto'; @@ -46,6 +47,17 @@ export class InviteController { const allInvites = await this.inviteService.findAllInvitations(); return allInvites; } + @ApiOperation({ summary: 'Get All Pending Invitations' }) + @ApiResponse({ + status: 200, + description: 'Successfully fetched all pending invitations', + type: FindAllPendingInvitationsResponseDto, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: ErrorResponseDto, + }) @Get('invites/pending') async findAllPendingInvitations() { const allPendingInvites = await this.inviteService.getPendingInvites(); diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index 624756536..d89ffef9a 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -33,27 +33,12 @@ export class InviteService { private readonly OrganisationService: OrganisationsService ) {} - // async getPendingInvites() { - // try { - // const invites = await this.inviteRepository.find({ where: { isAccepted: false } }); - // return { - // status_code: HttpStatus.OK, - // message: 'Successfully fetched pending invites', - // data: invites, - // }; - // } catch (error) { - // throw new InternalServerErrorException(`Internal server error: ${error.message}`); - // } - // } - async getPendingInvites(): Promise<{ status_code: number; message: string; data: InviteDto[] }> { try { - // Fetch invites where isAccepted is false const pendingInvites = await this.inviteRepository.find({ where: { isAccepted: false }, }); - // Map the result to the InviteDto format const pendingInvitesDto: InviteDto[] = pendingInvites.map(invite => { return { token: invite.token, @@ -64,8 +49,6 @@ export class InviteService { email: invite.email, }; }); - - // Return the response with the mapped data const responseData = { status_code: HttpStatus.OK, message: 'Successfully fetched pending invites', diff --git a/src/modules/permissions/entities/permissions.entity.ts b/src/modules/permissions/entities/permissions.entity.ts index b5d318b45..d87cd34c4 100644 --- a/src/modules/permissions/entities/permissions.entity.ts +++ b/src/modules/permissions/entities/permissions.entity.ts @@ -4,7 +4,7 @@ import { Role } from '../../../modules/role/entities/role.entity'; @Entity() export class Permissions extends AbstractBaseEntity { - @Column({ default: 'user' }) + @Column() title: string; @ManyToMany(() => Role, role => role.permissions) roles: Role[]; diff --git a/src/modules/role/entities/role.entity.ts b/src/modules/role/entities/role.entity.ts index d80edcbc9..b182e70ad 100644 --- a/src/modules/role/entities/role.entity.ts +++ b/src/modules/role/entities/role.entity.ts @@ -4,7 +4,7 @@ import { Permissions } from '../../permissions/entities/permissions.entity'; @Entity({ name: 'roles' }) export class Role extends AbstractBaseEntity { - @Column({ default: 'Ezekiel' }) + @Column() name: string; @Column({ type: 'text', nullable: true }) From 7927e6498a5f669ff84987096ec3ec6c6b8818a3 Mon Sep 17 00:00:00 2001 From: Nancy Okeke Date: Thu, 15 Aug 2024 18:18:49 +0100 Subject: [PATCH 15/68] fix(invites): made changes in my invites.service.ts --- .vscode/settings.json | 11 ++++++++--- src/modules/invite/invite.service.ts | 12 +++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b347739d5..058a5d823 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,9 +10,14 @@ "statusBar.background": "#1a1f15", "statusBarItem.hoverBackground": "#333d2a", "statusBar.foreground": "#e7e7e7", - "panel.border": "#333d2a", - "sideBar.border": "#333d2a", - "editorGroup.border": "#333d2a" + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#333d2a", + "statusBarItem.remoteBackground": "#1a1f15", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#1a1f15", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#1a1f1599", + "titleBar.inactiveForeground": "#e7e7e799" }, "peacock.color": "#1a1f15", "eslint.validate": [ diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index d89ffef9a..e8afa7e6c 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -32,8 +32,7 @@ export class InviteService { private readonly configService: ConfigService, private readonly OrganisationService: OrganisationsService ) {} - - async getPendingInvites(): Promise<{ status_code: number; message: string; data: InviteDto[] }> { + async getPendingInvites(): Promise<{ message: string; data: InviteDto[] }> { try { const pendingInvites = await this.inviteRepository.find({ where: { isAccepted: false }, @@ -49,18 +48,14 @@ export class InviteService { email: invite.email, }; }); - const responseData = { - status_code: HttpStatus.OK, - message: 'Successfully fetched pending invites', + return { + message: 'Successfully fetched pending Invites', data: pendingInvitesDto, }; - - return responseData; } catch (error) { throw new InternalServerErrorException(`Internal server error: ${error.message}`); } } - async findAllInvitations(): Promise<{ status_code: number; message: string; data: InviteDto[] }> { try { const invites = await this.inviteRepository.find(); @@ -87,7 +82,6 @@ export class InviteService { throw new InternalServerErrorException(`Internal server error: ${error.message}`); } } - async createInvite(organisationId: string) { const organisation = await this.organisationRepository.findOne({ where: { id: organisationId } }); if (!organisation) { From ef509042f667accdf1ca046cd3a95a2af4212416 Mon Sep 17 00:00:00 2001 From: Nancy Okeke Date: Thu, 15 Aug 2024 20:05:26 +0100 Subject: [PATCH 16/68] fix(invites): Fixed a bug on invite.service.spec.ts --- src/modules/invite/invite.service.ts | 1 + src/modules/invite/tests/invite.service.spec.ts | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index e8afa7e6c..7f5c79509 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -32,6 +32,7 @@ export class InviteService { private readonly configService: ConfigService, private readonly OrganisationService: OrganisationsService ) {} + async getPendingInvites(): Promise<{ message: string; data: InviteDto[] }> { try { const pendingInvites = await this.inviteRepository.find({ diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 9616876d1..5f7fbf39e 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -169,7 +169,6 @@ describe('InviteService', () => { await expect(service.getPendingInvites()).rejects.toThrow(InternalServerErrorException); }); - it('should fetch all pending invites where isAccepted is false', async () => { const pendingInvitesMock = mockInvites.filter(invite => invite.isAccepted === false); @@ -178,8 +177,7 @@ describe('InviteService', () => { const result = await service.getPendingInvites(); expect(result).toEqual({ - status_code: HttpStatus.OK, - message: 'Successfully fetched pending invites', + message: 'Successfully fetched pending Invites', data: pendingInvitesMock.map(invite => ({ token: invite.token, id: invite.id, @@ -190,7 +188,6 @@ describe('InviteService', () => { })), }); }); - describe('createInvite', () => { it('should create an invite and return a link', async () => { const mockToken = 'mock-uuid'; From 396c0388fdfd25afd1d4054c76b78c71a9d0bed0 Mon Sep 17 00:00:00 2001 From: Asin-Junior-Honore Date: Sun, 18 Aug 2024 08:54:05 +0100 Subject: [PATCH 17/68] fix: folder structure --- src/modules/faq/faq.module.ts | 5 ++--- src/modules/faq/faq.service.ts | 13 +++++-------- src/modules/faq/test/faq.create.service.spec.ts | 3 +-- src/modules/faq/test/faq.service.spec.ts | 3 +-- src/modules/testimonials/testimonials.module.ts | 2 +- src/modules/testimonials/testimonials.service.ts | 2 +- .../testimonials/tests/testimonials.service.spec.ts | 3 +-- .../testimonials/tests/update.service.spec.ts | 4 ++-- .../translation/translation.service.ts | 0 9 files changed, 14 insertions(+), 21 deletions(-) rename src/{ => modules}/translation/translation.service.ts (100%) diff --git a/src/modules/faq/faq.module.ts b/src/modules/faq/faq.module.ts index c8e8f203e..ae307ac31 100644 --- a/src/modules/faq/faq.module.ts +++ b/src/modules/faq/faq.module.ts @@ -7,12 +7,11 @@ import { User } from '../user/entities/user.entity'; import { Organisation } from '../organisations/entities/organisations.entity'; import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; import { Role } from '../role/entities/role.entity'; -import { TextService } from '../../translation/translation.service'; - +import { TextService } from '../translation/translation.service'; @Module({ imports: [TypeOrmModule.forFeature([Faq, User, Organisation, OrganisationUserRole, Role])], controllers: [FaqController], - providers: [FaqService,TextService], + providers: [FaqService, TextService], }) export class FaqModule {} diff --git a/src/modules/faq/faq.service.ts b/src/modules/faq/faq.service.ts index 735a7bd7d..20fa4125d 100644 --- a/src/modules/faq/faq.service.ts +++ b/src/modules/faq/faq.service.ts @@ -1,21 +1,18 @@ -import { - BadRequestException, - Injectable, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Faq } from './entities/faq.entity'; import { CreateFaqDto } from './dto/create-faq.dto'; import { IFaq } from './faq.interface'; import { UpdateFaqDto } from './dto/update-faq.dto'; -import { TextService } from '../../translation/translation.service'; +import { TextService } from '../translation/translation.service'; @Injectable() export class FaqService { constructor( @InjectRepository(Faq) private faqRepository: Repository, - private readonly textService: TextService, + private readonly textService: TextService ) {} private async translateContent(content: string, lang: string) { @@ -43,12 +40,12 @@ export class FaqService { const faqs = await this.faqRepository.find(); const translatedFaqs = await Promise.all( - faqs.map(async (faq) => { + faqs.map(async faq => { faq.question = await this.translateContent(faq.question, language); faq.answer = await this.translateContent(faq.answer, language); faq.category = await this.translateContent(faq.category, language); return faq; - }), + }) ); return { diff --git a/src/modules/faq/test/faq.create.service.spec.ts b/src/modules/faq/test/faq.create.service.spec.ts index e20c68ff7..038ef30a4 100644 --- a/src/modules/faq/test/faq.create.service.spec.ts +++ b/src/modules/faq/test/faq.create.service.spec.ts @@ -4,8 +4,7 @@ import { Repository } from 'typeorm'; import { Faq } from '../entities/faq.entity'; import { CreateFaqDto } from '../dto/create-faq.dto'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { TextService } from '../../../translation/translation.service'; - +import { TextService } from '../../translation/translation.service'; class MockTextService { translateText(text: string, targetLang: string) { diff --git a/src/modules/faq/test/faq.service.spec.ts b/src/modules/faq/test/faq.service.spec.ts index 1d8fbc282..9389f9261 100644 --- a/src/modules/faq/test/faq.service.spec.ts +++ b/src/modules/faq/test/faq.service.spec.ts @@ -4,8 +4,7 @@ import { Repository } from 'typeorm'; import { FaqService } from '../faq.service'; import { Faq } from '../entities/faq.entity'; import { BadRequestException } from '@nestjs/common'; -import { TextService } from '../../../translation/translation.service'; - +import { TextService } from '../../translation/translation.service'; class MockTextService { translateText(text: string, targetLang: string) { diff --git a/src/modules/testimonials/testimonials.module.ts b/src/modules/testimonials/testimonials.module.ts index 5f6609494..312fde31b 100644 --- a/src/modules/testimonials/testimonials.module.ts +++ b/src/modules/testimonials/testimonials.module.ts @@ -7,7 +7,7 @@ import { Testimonial } from './entities/testimonials.entity'; import { TestimonialsController } from './testimonials.controller'; import { TestimonialsService } from './testimonials.service'; import { Profile } from '../profile/entities/profile.entity'; -import { TextService } from '../../translation/translation.service'; +import { TextService } from '../translation/translation.service'; @Module({ imports: [TypeOrmModule.forFeature([Testimonial, User, Profile])], diff --git a/src/modules/testimonials/testimonials.service.ts b/src/modules/testimonials/testimonials.service.ts index de12f69d3..5793b1fe0 100644 --- a/src/modules/testimonials/testimonials.service.ts +++ b/src/modules/testimonials/testimonials.service.ts @@ -16,7 +16,7 @@ import { TestimonialMapper } from './mappers/testimonial.mapper'; import { TestimonialResponseMapper } from './mappers/testimonial-response.mapper'; import { TestimonialResponse } from './interfaces/testimonial-response.interface'; import { UpdateTestimonialDto } from './dto/update-testimonial.dto'; -import { TextService } from '../../translation/translation.service'; +import { TextService } from '../translation/translation.service'; @Injectable() export class TestimonialsService { diff --git a/src/modules/testimonials/tests/testimonials.service.spec.ts b/src/modules/testimonials/tests/testimonials.service.spec.ts index fd08cb957..9751aebfe 100644 --- a/src/modules/testimonials/tests/testimonials.service.spec.ts +++ b/src/modules/testimonials/tests/testimonials.service.spec.ts @@ -12,8 +12,7 @@ import * as SYS_MSG from '../../../helpers/SystemMessages'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; import { mockUser } from '../../organisations/tests/mocks/user.mock'; import { testimonialsMock } from './mocks/testimonials.mock'; -import { TextService } from '../../../translation/translation.service'; - +import { TextService } from '../../translation/translation.service'; class MockTextService { translateText(text: string, targetLang: string) { diff --git a/src/modules/testimonials/tests/update.service.spec.ts b/src/modules/testimonials/tests/update.service.spec.ts index c40cd79b1..282209637 100644 --- a/src/modules/testimonials/tests/update.service.spec.ts +++ b/src/modules/testimonials/tests/update.service.spec.ts @@ -8,7 +8,7 @@ import UserService from '../../user/user.service'; import { UpdateTestimonialDto } from '../dto/update-testimonial.dto'; import { Testimonial } from '../entities/testimonials.entity'; import { TestimonialsService } from '../testimonials.service'; -import { TextService } from '../../../translation/translation.service'; +import { TextService } from '../../translation/translation.service'; describe('TestimonialsService', () => { let service: TestimonialsService; @@ -25,7 +25,7 @@ describe('TestimonialsService', () => { TestimonialsService, UserService, { - provide: TextService, + provide: TextService, useClass: MockTextService, }, { diff --git a/src/translation/translation.service.ts b/src/modules/translation/translation.service.ts similarity index 100% rename from src/translation/translation.service.ts rename to src/modules/translation/translation.service.ts From 407af26a4008c21dc44009daa5e966afde9641ab Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 00:07:36 +0300 Subject: [PATCH 18/68] feat: creating individual billing plans --- src/helpers/SystemMessages.ts | 4 +- .../billing-plans/billing-plan.controller.ts | 9 ++-- .../billing-plans/billing-plan.service.ts | 48 ++++++++----------- .../billing-plans/dto/billing-plan.dto.ts | 23 ++++++--- .../entities/billing-plan.entity.ts | 16 +++++-- .../mapper/billing-plan.mapper.ts | 17 +++++++ .../tests/billing-plan.service.spec.ts | 21 ++++---- 7 files changed, 81 insertions(+), 57 deletions(-) create mode 100644 src/modules/billing-plans/mapper/billing-plan.mapper.ts diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 6f79700fa..2b501045e 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -104,4 +104,6 @@ export const FILE_EXCEEDS_SIZE = resource => { export const INVALID_FILE_TYPE = resource => { return `Invalid file type. Allowed types: ${resource}`; -}; \ No newline at end of file +}; +export const BILLING_PLAN_ALREADY_EXISTS = "Billing plan already exists"; +export const BILLING_PLAN_CREATED = "Billing plan successfully created"; \ No newline at end of file diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 133a2b16b..a19119943 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -11,10 +11,11 @@ export class BillingPlanController { @Post('/') @ApiOperation({ summary: 'Create billing plans' }) - @ApiResponse({ status: 201, description: 'Billing plans created successfully.', type: [BillingPlanDto] }) - @ApiResponse({ status: 200, description: 'Billing plans already exist in the database.', type: [BillingPlanDto] }) - async createBillingPlan() { - return this.billingPlanService.createBillingPlan(); + @ApiBody({ type: BillingPlanDto }) + @ApiResponse({ status: 201, description: 'Billing plan created successfully.', type: BillingPlanDto }) + @ApiResponse({ status: 200, description: 'Billing plan already exists in the database.', type: [BillingPlanDto] }) + async createBillingPlan(@Body() createBillingPlanDto: BillingPlanDto) { + return this.billingPlanService.createBillingPlan(createBillingPlanDto); } @skipAuth() diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index c0d7fbc53..765663f8f 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -2,7 +2,9 @@ import { Injectable, HttpStatus, HttpException, BadRequestException, NotFoundExc import { Repository } from 'typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; import { InjectRepository } from '@nestjs/typeorm'; - +import { BillingPlanDto } from "./dto/billing-plan.dto"; +import * as SYS_MSG from "../../helpers/SystemMessages"; + @Injectable() export class BillingPlanService { constructor( @@ -10,37 +12,25 @@ export class BillingPlanService { private readonly billingPlanRepository: Repository ) {} - async createBillingPlan() { - try { - const billingPlans = await this.billingPlanRepository.find(); - - if (billingPlans.length > 0) { - const plans = billingPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })); - - return { - status_code: HttpStatus.OK, - message: 'Billing plans already exist in the database', - data: plans, - }; + async createBillingPlan(createBillingPlanDto: BillingPlanDto ) { + const billingPlan = await this.billingPlanRepository.findOne({ + where: { + name: createBillingPlanDto.name } + }); - const newPlans = this.createBillingPlanEntities(); - const createdPlans = await this.billingPlanRepository.save(newPlans); - const plans = createdPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })); - - return { - message: 'Billing plans created successfully', - data: plans, - }; - } catch (error) { - throw new HttpException( - { - message: `Internal server error: ${error.message}`, - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + if (billingPlan.length > 0) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); } + + const newPlan = this.billingPlanRepository.create(createBillingPlanDto); + const createdPlan = await this.billingPlanRepository.save(newPlan); + const plan = BillingPlanMapper.mapToEntity(createdPlan) + + return { + message: SYS_MSG.BILLING_PLAN_CREATED, + data: plan, + }; } async getAllBillingPlans() { diff --git a/src/modules/billing-plans/dto/billing-plan.dto.ts b/src/modules/billing-plans/dto/billing-plan.dto.ts index ddd5829de..18c790d1e 100644 --- a/src/modules/billing-plans/dto/billing-plan.dto.ts +++ b/src/modules/billing-plans/dto/billing-plan.dto.ts @@ -1,13 +1,24 @@ import { ApiProperty } from '@nestjs/swagger'; -import { randomUUID } from 'crypto'; +import { IsString, IsOptional, IsNumberString } from "class-validator"; -export class BillingPlanDto { - @ApiProperty({ example: randomUUID() }) - id: string; +export class CreateBillingPlanDto { + @ApiProperty({ example: "Free" }) + @IsString() + name: string; @ApiProperty({ example: 'Free' }) - name: string; + @IsString() + @IsOptional() + description: string; + + @ApiProperty({ example: 'monthly' }) + @IsString() + frequency: string; @ApiProperty({ example: 0 }) - price: number; + @IsNumberString() + amount: number; + + @ApiProperty({ example: 'true' }) + is_active: boolean; } diff --git a/src/modules/billing-plans/entities/billing-plan.entity.ts b/src/modules/billing-plans/entities/billing-plan.entity.ts index 07ad6d761..c53c69fa2 100644 --- a/src/modules/billing-plans/entities/billing-plan.entity.ts +++ b/src/modules/billing-plans/entities/billing-plan.entity.ts @@ -1,12 +1,20 @@ import { AbstractBaseEntity } from '../../../entities/base.entity'; - import { Column, Entity } from 'typeorm'; @Entity() export class BillingPlan extends AbstractBaseEntity { - @Column({ type: 'text', nullable: false }) + @Column({ type: 'text', nullable: false, unique: true }) name: string; - @Column({ type: 'int', nullable: false, default: 0 }) - price: number; + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ type: 'text', nullable: false }) + frequency: string; + + @Column({ default: 'true' }) + is_active: boolean; + + @Column({ type: 'int', nullable: true }) + amount: number; } diff --git a/src/modules/billing-plans/mapper/billing-plan.mapper.ts b/src/modules/billing-plans/mapper/billing-plan.mapper.ts new file mode 100644 index 000000000..497fe446a --- /dev/null +++ b/src/modules/billing-plans/mapper/billing-plan.mapper.ts @@ -0,0 +1,17 @@ +import { BillingPlan } from "../entities/billing-plan.entity"; + +export class BillingPlanMapper { + static mapToResponseFormat(billingPlan: BillingPlan) { + if (!billingPlan) { + throw new Error('Billing plan entity is required'); + } + + return { + id: billingPlan.id, + name: billingPlan.name, + amount: billingPlan.amount, + frequency: billingPlan.frequency, + is_active: billingPlan.is_active + }; + } +} \ No newline at end of file diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index 0b729252e..074eab72f 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -3,8 +3,9 @@ import { BillingPlanService } from '../billing-plan.service'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingPlan } from '../entities/billing-plan.entity'; -import { NotFoundException, HttpException, BadRequestException } from '@nestjs/common'; -import { BillingPlanDto } from '../dto/billing-plan.dto'; +import { NotFoundException, BadRequestException } from '@nestjs/common'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import * as SYS_MSG from "../../../helpers/SystemMessages"; describe('BillingPlanService', () => { let service: BillingPlanService; @@ -26,21 +27,15 @@ describe('BillingPlanService', () => { }); describe('createBillingPlan', () => { - it('should return existing billing plans if they already exist', async () => { - const billingPlans = [ - { id: '1', name: 'Free', price: 0 }, - { id: '2', name: 'Basic', price: 20 }, - ]; + it('should throw an error if they already exist', async () => { + const createPlanDto = { name: 'Free', amount: 0, frequency: 'never', is_active: true } + const billingPlans = { id: '1', name: 'Free', price: 0 }; jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); - const result = await service.createBillingPlan(); + const result = await service.createBillingPlan(createPlanDto); - expect(result).toEqual({ - status_code: 200, - message: 'Billing plans already exist in the database', - data: billingPlans, - }); + expect(result).rejects.toThrow(new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST)) }); }); From 9f515b6880b3d7a855a5ce560bc8c5a1d2c7bb8d Mon Sep 17 00:00:00 2001 From: Amal-Salam Date: Sat, 24 Aug 2024 00:38:46 +0100 Subject: [PATCH 19/68] feat: added template to folder, cleaned up the code with constants and updated the dto --- .env.local | 2 +- src/helpers/SystemMessages.ts | 8 +-- src/helpers/contactHelper.ts | 2 + src/modules/contact-us/contact-us.service.ts | 12 ++-- .../contact-us/dto/create-contact-us.dto.ts | 6 +- .../contact-us/entities/contact-us.entity.ts | 3 + .../email/hng-templates/contact-inquiry.hbs | 56 +++++++++++++++++++ 7 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 src/helpers/contactHelper.ts create mode 100644 src/modules/email/hng-templates/contact-inquiry.hbs diff --git a/.env.local b/.env.local index 182f4f204..6771b01b7 100644 --- a/.env.local +++ b/.env.local @@ -33,7 +33,7 @@ PORT=3000 DB_USERNAME=username DB_PASSWORD=password DB_TYPE=postgres -DB_DATABASE=database +DB_NAME=database DB_HOST=localhost DB_PORT=5432 DB_ENTITIES=dist/src/modules/**/entities/**/*.entity{.ts,.js} diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 6f79700fa..739d46ef2 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -98,10 +98,10 @@ export const DIRECTORY_CREATED = 'Uploads directory created at:'; export const PICTURE_UPDATED = 'Profile picture updated successfully'; export const FILE_SAVE_ERROR = 'Error saving file to disk'; export const FILE_EXCEEDS_SIZE = resource => { - return `File size exceeds ${resource} MB limit` - + return `File size exceeds ${resource} MB limit`; }; export const INVALID_FILE_TYPE = resource => { return `Invalid file type. Allowed types: ${resource}`; - -}; \ No newline at end of file +}; +export const INQUIRY_SENT = 'Inquiry sent successfully'; +export const INQUIRY_NOT_SENT = 'Failed to send contact inquiry email'; diff --git a/src/helpers/contactHelper.ts b/src/helpers/contactHelper.ts new file mode 100644 index 000000000..159fc24ae --- /dev/null +++ b/src/helpers/contactHelper.ts @@ -0,0 +1,2 @@ +export const COMPANYEMAIL = 'amal_salam@yahoo.com'; +export const SUBJECT = 'New Contact Inquiry'; diff --git a/src/modules/contact-us/contact-us.service.ts b/src/modules/contact-us/contact-us.service.ts index 2009cff26..54e100ef8 100644 --- a/src/modules/contact-us/contact-us.service.ts +++ b/src/modules/contact-us/contact-us.service.ts @@ -4,6 +4,8 @@ import { Repository } from 'typeorm'; import { CreateContactDto } from '../contact-us/dto/create-contact-us.dto'; import { ContactUs } from './entities/contact-us.entity'; import { MailerService } from '@nestjs-modules/mailer'; +import * as CONTACTHELPER from '../../helpers/contactHelper'; +import * as SYS_MSG from '../../helpers/SystemMessages'; @Injectable() export class ContactUsService { @@ -20,23 +22,25 @@ export class ContactUsService { try { await this.sendEmail(createContactDto); } catch (error) { - throw new InternalServerErrorException('Failed to send email'); + console.log(error.message); + throw new InternalServerErrorException(SYS_MSG.INQUIRY_NOT_SENT); } return { - message: 'Inquiry sent successfully', + message: SYS_MSG.INQUIRY_SENT, status_code: 200, }; } private async sendEmail(contactDto: CreateContactDto) { await this.mailerService.sendMail({ - to: 'amal_salam@yahoo.com', - subject: 'New Contact Inquiry', + to: CONTACTHELPER.COMPANYEMAIL, + subject: CONTACTHELPER.SUBJECT, template: 'contact-inquiry', context: { name: contactDto.name, email: contactDto.email, + phonenumber: contactDto.phone, message: contactDto.message, date: new Date().toLocaleString(), }, diff --git a/src/modules/contact-us/dto/create-contact-us.dto.ts b/src/modules/contact-us/dto/create-contact-us.dto.ts index 453eb8432..b0faa82e3 100644 --- a/src/modules/contact-us/dto/create-contact-us.dto.ts +++ b/src/modules/contact-us/dto/create-contact-us.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateContactDto { @IsNotEmpty() @@ -9,6 +9,10 @@ export class CreateContactDto { @IsEmail() email: string; + @IsOptional() + @IsInt() + phone: number; + @IsNotEmpty() @IsString() message: string; diff --git a/src/modules/contact-us/entities/contact-us.entity.ts b/src/modules/contact-us/entities/contact-us.entity.ts index e4c4a7e18..f08d7df26 100644 --- a/src/modules/contact-us/entities/contact-us.entity.ts +++ b/src/modules/contact-us/entities/contact-us.entity.ts @@ -9,6 +9,9 @@ export class ContactUs extends AbstractBaseEntity { @Column('varchar', { nullable: false }) email: string; + @Column('int', { nullable: true }) + phone: number; + @Column('text', { nullable: false }) message: string; diff --git a/src/modules/email/hng-templates/contact-inquiry.hbs b/src/modules/email/hng-templates/contact-inquiry.hbs new file mode 100644 index 000000000..c5dd275c8 --- /dev/null +++ b/src/modules/email/hng-templates/contact-inquiry.hbs @@ -0,0 +1,56 @@ + + + + + Contact Inquiry + + + +
+ +
+

Contact Inquiry Received

+
+ + +
+

Hello,

+

+ You have received a new contact inquiry from your website. Here are the details: +

+

+ Name: + {{name}}
+ Email: + {{email}}
+ Phone Number: + {{phonenumber}}
+ Message: + {{message}}
+ Date: + {{date}} +

+

+ Please follow up with the inquirer as soon as possible. If you have any questions, feel free to reach out to + the support team. +

+
+ + +
+

+ Thank you for using our service.
+ © Your Company Name. All rights reserved. +

+
+
+ + \ No newline at end of file From 4caca59a2e0075a19de6a160b4290552306f90af Mon Sep 17 00:00:00 2001 From: Amal-Salam Date: Sat, 24 Aug 2024 00:51:26 +0100 Subject: [PATCH 20/68] fix: contact-us test --- src/modules/contact-us/test/contact-us.service.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/contact-us/test/contact-us.service.spec.ts b/src/modules/contact-us/test/contact-us.service.spec.ts index 15479c997..3c86f2419 100644 --- a/src/modules/contact-us/test/contact-us.service.spec.ts +++ b/src/modules/contact-us/test/contact-us.service.spec.ts @@ -47,6 +47,7 @@ describe('ContactUsService', () => { const createContactDto: CreateContactDto = { name: 'John Doe', email: 'john@example.com', + phone: 123456789, message: 'Test message', }; @@ -66,6 +67,7 @@ describe('ContactUsService', () => { const createContactDto: CreateContactDto = { name: 'John Doe', email: 'john@example.com', + phone: 123456789, message: 'Test message', }; From 75d117eb891aa2518524725ed300e4c9fb210c72 Mon Sep 17 00:00:00 2001 From: ZEDD2468 Date: Sat, 24 Aug 2024 00:54:11 +0100 Subject: [PATCH 21/68] fix(billing plans): billing plans --- .../billing-plans/billing-plan.controller.ts | 4 +++- src/modules/products/products.service.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 133a2b16b..c8cc07dda 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -1,5 +1,6 @@ -import { Controller, Get, Param, Post } from '@nestjs/common'; +import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; import { BillingPlanService } from './billing-plan.service'; import { skipAuth } from '../../helpers/skipAuth'; import { BillingPlanDto } from './dto/billing-plan.dto'; @@ -10,6 +11,7 @@ export class BillingPlanController { constructor(private readonly billingPlanService: BillingPlanService) {} @Post('/') + @UseGuards(SuperAdminGuard) @ApiOperation({ summary: 'Create billing plans' }) @ApiResponse({ status: 201, description: 'Billing plans created successfully.', type: [BillingPlanDto] }) @ApiResponse({ status: 200, description: 'Billing plans already exist in the database.', type: [BillingPlanDto] }) diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index c1464e4cb..a3565278a 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -84,6 +84,23 @@ export class ProductsService { }; } + async getProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { + const skip = (page - 1) * pageSize; + const allProucts = await this.productRepository.find({ skip, take: pageSize }); + const totalProducts = await this.productRepository.count(); + + return { + status_code: HttpStatus.OK, + message: 'Product retrieved successfully', + data: { + products: allProucts, + total: totalProducts, + page, + pageSize, + }, + }; + } + async getAllProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { const skip = (page - 1) * pageSize; const allProucts = await this.productRepository.find({ skip, take: pageSize }); From 9ada4bc9f1281b349d6b12698df97776083f21cf Mon Sep 17 00:00:00 2001 From: ZEDD2468 Date: Sat, 24 Aug 2024 01:02:09 +0100 Subject: [PATCH 22/68] fix(billing plans): billing plans --- src/modules/products/products.service.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index a3565278a..c1464e4cb 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -84,23 +84,6 @@ export class ProductsService { }; } - async getProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { - const skip = (page - 1) * pageSize; - const allProucts = await this.productRepository.find({ skip, take: pageSize }); - const totalProducts = await this.productRepository.count(); - - return { - status_code: HttpStatus.OK, - message: 'Product retrieved successfully', - data: { - products: allProucts, - total: totalProducts, - page, - pageSize, - }, - }; - } - async getAllProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { const skip = (page - 1) * pageSize; const allProucts = await this.productRepository.find({ skip, take: pageSize }); From 393b8f379f82d956526e96e9b2f241b3cc5f0c5b Mon Sep 17 00:00:00 2001 From: ZEDD2468 Date: Sat, 24 Aug 2024 01:24:59 +0100 Subject: [PATCH 23/68] fix(billing plans): billing plans --- src/modules/billing-plans/billing-plan.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index c8cc07dda..3762425a2 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SuperAdminGuard } from '../../guards/super-admin.guard'; import { BillingPlanService } from './billing-plan.service'; import { skipAuth } from '../../helpers/skipAuth'; @@ -12,6 +12,7 @@ export class BillingPlanController { @Post('/') @UseGuards(SuperAdminGuard) + @ApiBearerAuth() @ApiOperation({ summary: 'Create billing plans' }) @ApiResponse({ status: 201, description: 'Billing plans created successfully.', type: [BillingPlanDto] }) @ApiResponse({ status: 200, description: 'Billing plans already exist in the database.', type: [BillingPlanDto] }) From bb7db8736bd3730d0faadd26c9fb5143c9be2aa5 Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 01:47:51 +0100 Subject: [PATCH 24/68] feat: updated google authentication logic to fix inconsistencies --- src/modules/auth/auth.service.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 63e474ce3..bc97ab347 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -287,12 +287,8 @@ export default class AuthenticationService { const idToken = googleAuthPayload.id_token; let verifyTokenResponse: GoogleVerificationPayloadInterface; - if (isMobile === 'true') { - const request = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${idToken}`); - verifyTokenResponse = await request.json(); - } else { - verifyTokenResponse = await this.googleAuthService.verifyToken(idToken); - } + const request = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${idToken}`); + verifyTokenResponse = await request.json(); const userEmail = verifyTokenResponse.email; const userExists = await this.userService.getUserRecord({ identifier: userEmail, identifierType: 'email' }); From d6d07822b5c9d6d90ec274c2d6ed22c300a5870c Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 01:54:06 +0100 Subject: [PATCH 25/68] feat: fixed lint issues --- src/modules/auth/auth.service.ts | 3 +-- src/modules/auth/tests/auth.service.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index bc97ab347..cde8b3732 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -285,10 +285,9 @@ export default class AuthenticationService { async googleAuth({ googleAuthPayload, isMobile }: { googleAuthPayload: GoogleAuthPayload; isMobile: string }) { const idToken = googleAuthPayload.id_token; - let verifyTokenResponse: GoogleVerificationPayloadInterface; const request = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${idToken}`); - verifyTokenResponse = await request.json(); + const verifyTokenResponse: GoogleVerificationPayloadInterface = await request.json(); const userEmail = verifyTokenResponse.email; const userExists = await this.userService.getUserRecord({ identifier: userEmail, identifierType: 'email' }); diff --git a/src/modules/auth/tests/auth.service.spec.ts b/src/modules/auth/tests/auth.service.spec.ts index e15f26d25..5cde81cc2 100644 --- a/src/modules/auth/tests/auth.service.spec.ts +++ b/src/modules/auth/tests/auth.service.spec.ts @@ -262,7 +262,7 @@ describe('AuthenticationService', () => { userServiceMock.getUserRecord.mockResolvedValue(null); - expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); + await expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); }); it('should throw an unauthorized error for invalid password', async () => { @@ -281,12 +281,12 @@ describe('AuthenticationService', () => { userServiceMock.getUserRecord.mockResolvedValue(user); jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(false)); - expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); + await expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); }); }); describe('verify2fa', () => { - it('should throw error if totp code is incorrect', () => { + it('should throw error if totp code is incorrect', async () => { const verify2faDto: Verify2FADto = { totp_code: '12345' }; const userId = 'some-uuid-here'; @@ -305,7 +305,7 @@ describe('AuthenticationService', () => { jest.spyOn(userServiceMock, 'getUserRecord').mockResolvedValueOnce(user); (speakeasy.totp.verify as jest.Mock).mockReturnValue(false); - expect(service.verify2fa(verify2faDto, userId)).rejects.toThrow(CustomHttpException); + await expect(service.verify2fa(verify2faDto, userId)).rejects.toThrow(CustomHttpException); }); it('should enable 2fa if successful', async () => { From 3ce70f1a8546a22e467f1361b489dfdb98f5f981 Mon Sep 17 00:00:00 2001 From: olamstevy Date: Sat, 24 Aug 2024 03:51:41 +0100 Subject: [PATCH 26/68] fix: Added contact us template, and sender receipt --- .../contact-us/contact-us.controller.ts | 1 + src/modules/contact-us/contact-us.service.ts | 3 +- .../email/hng-templates/contact-inquiry.hbs | 37 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/modules/email/hng-templates/contact-inquiry.hbs diff --git a/src/modules/contact-us/contact-us.controller.ts b/src/modules/contact-us/contact-us.controller.ts index d4c41f969..39b71f7a8 100644 --- a/src/modules/contact-us/contact-us.controller.ts +++ b/src/modules/contact-us/contact-us.controller.ts @@ -10,6 +10,7 @@ import { skipAuth } from '../..//helpers/skipAuth'; export class ContactUsController { constructor(private readonly contactUsService: ContactUsService) {} + @skipAuth() @ApiOperation({ summary: 'Post a Contact us Message' }) @ApiBearerAuth() @Post() diff --git a/src/modules/contact-us/contact-us.service.ts b/src/modules/contact-us/contact-us.service.ts index 2009cff26..c7c8cec29 100644 --- a/src/modules/contact-us/contact-us.service.ts +++ b/src/modules/contact-us/contact-us.service.ts @@ -20,6 +20,7 @@ export class ContactUsService { try { await this.sendEmail(createContactDto); } catch (error) { + console.error(error); throw new InternalServerErrorException('Failed to send email'); } @@ -31,7 +32,7 @@ export class ContactUsService { private async sendEmail(contactDto: CreateContactDto) { await this.mailerService.sendMail({ - to: 'amal_salam@yahoo.com', + to: [contactDto.email, 'amal_salam@yahoo.com'], subject: 'New Contact Inquiry', template: 'contact-inquiry', context: { diff --git a/src/modules/email/hng-templates/contact-inquiry.hbs b/src/modules/email/hng-templates/contact-inquiry.hbs new file mode 100644 index 000000000..9e556afc1 --- /dev/null +++ b/src/modules/email/hng-templates/contact-inquiry.hbs @@ -0,0 +1,37 @@ + + + + + + New Contact Inquiry + + + + +
+

New Contact Inquiry

+

A new inquiry has been submitted through the contact form:

+ +
+ Name: + {{name}} +
+
+ Email: + {{email}} +
+
+ Message: +

{{message}}

+
+
+ Submitted on: + {{date}} +
+
+ + \ No newline at end of file From d7b720fe4eb60d15bb64d54e4ee1a1e62f6c0a13 Mon Sep 17 00:00:00 2001 From: Amal-Salam Date: Sat, 24 Aug 2024 04:22:05 +0100 Subject: [PATCH 27/68] feat: added language translations to helpcenter topics --- .../Tests/help-center.service.spec.ts | 205 ++++++++++++------ .../help-center/help-center.controller.ts | 14 +- src/modules/help-center/help-center.module.ts | 3 +- .../help-center/help-center.service.spec.ts | 190 ---------------- .../help-center/help-center.service.ts | 27 ++- 5 files changed, 169 insertions(+), 270 deletions(-) delete mode 100644 src/modules/help-center/help-center.service.spec.ts diff --git a/src/modules/help-center/Tests/help-center.service.spec.ts b/src/modules/help-center/Tests/help-center.service.spec.ts index 35171de82..d643f253c 100644 --- a/src/modules/help-center/Tests/help-center.service.spec.ts +++ b/src/modules/help-center/Tests/help-center.service.spec.ts @@ -1,81 +1,74 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Repository } from 'typeorm'; import { HelpCenterService } from '../help-center.service'; +import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { NotFoundException, BadRequestException } from '@nestjs/common'; import { HelpCenterEntity } from '../entities/help-center.entity'; -import { User } from '../../user/entities/user.entity'; import { REQUEST_SUCCESSFUL } from '../../../helpers/SystemMessages'; +import { User } from '../../user/entities/user.entity'; +import { TextService } from '../../translation/translation.service'; + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('HelpCenterService', () => { let service: HelpCenterService; let helpCenterRepository: Repository; let userRepository: Repository; + const mockHelpCenter = { + id: '1234', + title: 'Sample Title', + content: 'Sample Content', + author: 'John Doe', + }; + + const mockHelpCenterDto = { + title: 'Sample Title', + content: 'Sample Content', + }; + + const mockUser = { + id: '123', + first_name: 'John', + last_name: 'Doe', + }; + const mockHelpCenterRepository = { - update: jest.fn(), - findOneBy: jest.fn(), - delete: jest.fn(), create: jest.fn().mockImplementation(dto => ({ ...dto, id: '1234', })), - save: jest.fn().mockResolvedValue({ - id: '1234', - title: 'Sample Title', - content: 'Sample Content', - author: 'ADMIN', - }), - find: jest.fn().mockResolvedValue([ - { - id: '1234', - title: 'Sample Title', - content: 'Sample Content', - author: 'ADMIN', - }, - ]), - findOne: jest.fn().mockImplementation(options => - Promise.resolve( - options.where.title === 'Sample Title' - ? { - id: '1234', - title: 'Sample Title', - content: 'Sample Content', - author: 'ADMIN', - } - : null - ) - ), + save: jest.fn().mockResolvedValue(mockHelpCenter), + find: jest.fn().mockResolvedValue([mockHelpCenter]), + findOne: jest + .fn() + .mockImplementation(options => + Promise.resolve(options.where.title === mockHelpCenter.title ? mockHelpCenter : null) + ), createQueryBuilder: jest.fn().mockReturnValue({ andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([ - { - id: '1234', - title: 'Sample Title', - content: 'Sample Content', - author: 'ADMIN', - }, - ]), + getMany: jest.fn().mockResolvedValue([mockHelpCenter]), }), }; const mockUserRepository = { - findOne: jest.fn().mockImplementation(options => - Promise.resolve( - options.where.id === '123' - ? { - id: '123', - first_name: 'John', - last_name: 'Doe', - } - : null - ) - ), + findOne: jest + .fn() + .mockImplementation(options => Promise.resolve(options.where.id === mockUser.id ? mockUser : null)), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ HelpCenterService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(HelpCenterEntity), useValue: mockHelpCenterRepository, @@ -92,39 +85,113 @@ describe('HelpCenterService', () => { userRepository = module.get>(getRepositoryToken(User)); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('create', () => { + it('should create a new help center topic with the user as author', async () => { + mockHelpCenterRepository.findOne.mockResolvedValueOnce(null); + mockUserRepository.findOne.mockResolvedValue(mockUser); + + const result = await service.create(mockHelpCenterDto, mockUser as unknown as User); + const responseBody = { + status_code: 201, + message: 'Request successful', + data: { ...mockHelpCenterDto, author: 'John Doe', id: '1234' }, + }; + + expect(result).toEqual(responseBody); + expect(helpCenterRepository.create).toHaveBeenCalledWith({ + ...mockHelpCenterDto, + author: 'John Doe', + }); + expect(helpCenterRepository.save).toHaveBeenCalledWith({ + ...mockHelpCenterDto, + author: 'John Doe', + id: '1234', + }); + }); + + it('should throw a BadRequestException if a topic with the same title already exists', async () => { + mockHelpCenterRepository.findOne.mockResolvedValue(mockHelpCenter); + + await expect(service.create(mockHelpCenterDto, mockUser as unknown as User)).rejects.toThrow( + new BadRequestException('This question already exists.') + ); + }); }); - describe('updateTopic', () => { - it('should update and return the help center topic', async () => { - const id = '1'; - const updateHelpCenterDto = { - title: 'Updated Title', - content: 'Updated Content', - author: 'Updated Author', + describe('findAll', () => { + it('should return an array of help center topics', async () => { + const result = await service.findAll(); + const responseBody = { + data: [mockHelpCenter], + status_code: 200, + message: REQUEST_SUCCESSFUL, }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.find).toHaveBeenCalled(); + }); + }); + + describe('findOne', () => { + it('should return a help center topic by ID', async () => { + const result = await service.findOne('1234'); const responseBody = { + data: mockHelpCenter, status_code: 200, message: REQUEST_SUCCESSFUL, - data: { ...updateHelpCenterDto, id }, }; - const updatedHelpCenter = { id, ...updateHelpCenterDto }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.findOne).toHaveBeenCalledWith({ where: { id: '1234' } }); + }); - jest.spyOn(helpCenterRepository, 'update').mockResolvedValue(undefined); - jest.spyOn(helpCenterRepository, 'findOneBy').mockResolvedValue(updatedHelpCenter as any); + it('should throw a NotFoundException if topic not found', async () => { + mockHelpCenterRepository.findOne.mockResolvedValueOnce(null); - expect(await service.updateTopic(id, updateHelpCenterDto)).toEqual(responseBody); + await expect(service.findOne('wrong-id')).rejects.toThrow( + new NotFoundException('Help center topic with ID wrong-id not found') + ); }); }); - describe('removeTopic', () => { - it('should remove a help center topic', async () => { - jest.spyOn(helpCenterRepository, 'delete').mockResolvedValue(undefined); + describe('search', () => { + it('should return an array of help center topics matching search criteria', async () => { + const mockQueryBuilder = { + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockHelpCenter]), + }; + + jest.spyOn(helpCenterRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - await service.removeTopic('1'); + const result = await service.search({ title: 'Sample' }); + const responseBody = { + status_code: 200, + message: REQUEST_SUCCESSFUL, + data: [mockHelpCenter], + }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.createQueryBuilder).toHaveBeenCalledWith('help_center'); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); + }); + + it('should return an array of help center topics matching multiple search criteria', async () => { + const mockQueryBuilder = { + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockHelpCenter]), + }; + + jest.spyOn(helpCenterRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - expect(helpCenterRepository.delete).toHaveBeenCalledWith('1'); + const result = await service.search({ title: 'Sample', content: 'Sample Content' }); + const responseBody = { + status_code: 200, + message: REQUEST_SUCCESSFUL, + data: [mockHelpCenter], + }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.createQueryBuilder).toHaveBeenCalledWith('help_center'); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.content LIKE :content', { + content: '%Sample Content%', + }); }); }); }); diff --git a/src/modules/help-center/help-center.controller.ts b/src/modules/help-center/help-center.controller.ts index 62c730b02..d70f9faa3 100644 --- a/src/modules/help-center/help-center.controller.ts +++ b/src/modules/help-center/help-center.controller.ts @@ -35,25 +35,27 @@ export class HelpCenterController { @ApiBearerAuth() @Post('topics') - @UseGuards(SuperAdminGuard) + // @UseGuards(SuperAdminGuard) @ApiOperation({ summary: 'Create a new help center topic' }) @ApiResponse({ status: 201, description: 'The topic has been successfully created.' }) @ApiResponse({ status: 400, description: 'Invalid input data.' }) @ApiResponse({ status: 400, description: 'This question already exists.' }) async create( @Body() createHelpCenterDto: CreateHelpCenterDto, - @Req() req: { user: User } + @Req() req: { user: User; language: string } ): Promise { const user: User = req.user; - return this.helpCenterService.create(createHelpCenterDto, user); + const language = req.language; + return this.helpCenterService.create(createHelpCenterDto, user, language); } @skipAuth() @Get('topics') @ApiOperation({ summary: 'Get all help center topics' }) @ApiResponse({ status: 200, description: 'The found records' }) - async findAll(): Promise { - return this.helpCenterService.findAll(); + async findAll(@Req() req: any): Promise { + const language = req.language; + return this.helpCenterService.findAll(language); } @skipAuth() @@ -190,4 +192,4 @@ export class HelpCenterController { } } } -} \ No newline at end of file +} diff --git a/src/modules/help-center/help-center.module.ts b/src/modules/help-center/help-center.module.ts index 692dc11fa..b86bd9c0d 100644 --- a/src/modules/help-center/help-center.module.ts +++ b/src/modules/help-center/help-center.module.ts @@ -8,10 +8,11 @@ import { Role } from '../role/entities/role.entity'; import { Profile } from '../profile/entities/profile.entity'; import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; import { Organisation } from '../organisations/entities/organisations.entity'; +import { TextService } from '../translation/translation.service'; @Module({ imports: [TypeOrmModule.forFeature([HelpCenterEntity, User, Organisation, OrganisationUserRole, Profile, Role])], - providers: [HelpCenterService], + providers: [HelpCenterService, TextService], controllers: [HelpCenterController], exports: [HelpCenterService], }) diff --git a/src/modules/help-center/help-center.service.spec.ts b/src/modules/help-center/help-center.service.spec.ts deleted file mode 100644 index ff063610d..000000000 --- a/src/modules/help-center/help-center.service.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HelpCenterService } from './help-center.service'; -import { Repository } from 'typeorm'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { NotFoundException, BadRequestException } from '@nestjs/common'; -import { HelpCenterEntity } from './entities/help-center.entity'; -import { REQUEST_SUCCESSFUL } from '../../helpers/SystemMessages'; -import { User } from '../user/entities/user.entity'; - -describe('HelpCenterService', () => { - let service: HelpCenterService; - let helpCenterRepository: Repository; - let userRepository: Repository; - - const mockHelpCenter = { - id: '1234', - title: 'Sample Title', - content: 'Sample Content', - author: 'John Doe', - }; - - const mockHelpCenterDto = { - title: 'Sample Title', - content: 'Sample Content', - }; - - const mockUser = { - id: '123', - first_name: 'John', - last_name: 'Doe', - }; - - const mockHelpCenterRepository = { - create: jest.fn().mockImplementation(dto => ({ - ...dto, - id: '1234', - })), - save: jest.fn().mockResolvedValue(mockHelpCenter), - find: jest.fn().mockResolvedValue([mockHelpCenter]), - findOne: jest - .fn() - .mockImplementation(options => - Promise.resolve(options.where.title === mockHelpCenter.title ? mockHelpCenter : null) - ), - createQueryBuilder: jest.fn().mockReturnValue({ - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([mockHelpCenter]), - }), - }; - - const mockUserRepository = { - findOne: jest - .fn() - .mockImplementation(options => Promise.resolve(options.where.id === mockUser.id ? mockUser : null)), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - HelpCenterService, - { - provide: getRepositoryToken(HelpCenterEntity), - useValue: mockHelpCenterRepository, - }, - { - provide: getRepositoryToken(User), - useValue: mockUserRepository, - }, - ], - }).compile(); - - service = module.get(HelpCenterService); - helpCenterRepository = module.get>(getRepositoryToken(HelpCenterEntity)); - userRepository = module.get>(getRepositoryToken(User)); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should create a new help center topic with the user as author', async () => { - mockHelpCenterRepository.findOne.mockResolvedValueOnce(null); - mockUserRepository.findOne.mockResolvedValue(mockUser); - - const result = await service.create(mockHelpCenterDto, mockUser as unknown as User); - const responseBody = { - status_code: 201, - message: 'Request successful', - data: { ...mockHelpCenterDto, author: 'John Doe', id: '1234' }, - }; - - expect(result).toEqual(responseBody); - expect(helpCenterRepository.create).toHaveBeenCalledWith({ - ...mockHelpCenterDto, - author: 'John Doe', - }); - expect(helpCenterRepository.save).toHaveBeenCalledWith({ - ...mockHelpCenterDto, - author: 'John Doe', - id: '1234', - }); - }); - - it('should throw a BadRequestException if a topic with the same title already exists', async () => { - mockHelpCenterRepository.findOne.mockResolvedValue(mockHelpCenter); - - await expect(service.create(mockHelpCenterDto, mockUser as unknown as User)).rejects.toThrow( - new BadRequestException('This question already exists.') - ); - }); - }); - - describe('findAll', () => { - it('should return an array of help center topics', async () => { - const result = await service.findAll(); - const responseBody = { - data: [mockHelpCenter], - status_code: 200, - message: REQUEST_SUCCESSFUL, - }; - expect(result).toEqual(responseBody); - expect(helpCenterRepository.find).toHaveBeenCalled(); - }); - }); - - describe('findOne', () => { - it('should return a help center topic by ID', async () => { - const result = await service.findOne('1234'); - const responseBody = { - data: mockHelpCenter, - status_code: 200, - message: REQUEST_SUCCESSFUL, - }; - expect(result).toEqual(responseBody); - expect(helpCenterRepository.findOne).toHaveBeenCalledWith({ where: { id: '1234' } }); - }); - - it('should throw a NotFoundException if topic not found', async () => { - mockHelpCenterRepository.findOne.mockResolvedValueOnce(null); - - await expect(service.findOne('wrong-id')).rejects.toThrow( - new NotFoundException('Help center topic with ID wrong-id not found') - ); - }); - }); - - describe('search', () => { - it('should return an array of help center topics matching search criteria', async () => { - const mockQueryBuilder = { - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([mockHelpCenter]), - }; - - jest.spyOn(helpCenterRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - - const result = await service.search({ title: 'Sample' }); - const responseBody = { - status_code: 200, - message: REQUEST_SUCCESSFUL, - data: [mockHelpCenter], - }; - expect(result).toEqual(responseBody); - expect(helpCenterRepository.createQueryBuilder).toHaveBeenCalledWith('help_center'); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); - }); - - it('should return an array of help center topics matching multiple search criteria', async () => { - const mockQueryBuilder = { - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([mockHelpCenter]), - }; - - jest.spyOn(helpCenterRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - - const result = await service.search({ title: 'Sample', content: 'Sample Content' }); - const responseBody = { - status_code: 200, - message: REQUEST_SUCCESSFUL, - data: [mockHelpCenter], - }; - expect(result).toEqual(responseBody); - expect(helpCenterRepository.createQueryBuilder).toHaveBeenCalledWith('help_center'); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.content LIKE :content', { - content: '%Sample Content%', - }); - }); - }); -}); diff --git a/src/modules/help-center/help-center.service.ts b/src/modules/help-center/help-center.service.ts index dc65405a2..1187dc76f 100644 --- a/src/modules/help-center/help-center.service.ts +++ b/src/modules/help-center/help-center.service.ts @@ -8,6 +8,7 @@ import { SearchHelpCenterDto } from './dto/search-help-center.dto'; import { REQUEST_SUCCESSFUL, QUESTION_ALREADY_EXISTS, USER_NOT_FOUND } from '../../helpers/SystemMessages'; import { CustomHttpException } from '../../helpers/custom-http-filter'; import { User } from '../user/entities/user.entity'; +import { TextService } from '../translation/translation.service'; @Injectable() export class HelpCenterService { @@ -15,10 +16,15 @@ export class HelpCenterService { @InjectRepository(HelpCenterEntity) private readonly helpCenterRepository: Repository, @InjectRepository(User) - private userRepository: Repository + private userRepository: Repository, + private readonly textService: TextService ) {} - async create(createHelpCenterDto: CreateHelpCenterDto, user: User) { + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); + } + + async create(createHelpCenterDto: CreateHelpCenterDto, user: User, language: string = 'en') { const existingTopic = await this.helpCenterRepository.findOne({ where: { title: createHelpCenterDto.title }, }); @@ -36,8 +42,14 @@ export class HelpCenterService { throw new CustomHttpException(USER_NOT_FOUND, HttpStatus.NOT_FOUND); } + let translatedContent = createHelpCenterDto.content; + if (language && language !== 'en') { + translatedContent = await this.translateContent(createHelpCenterDto.content, language); + } + const helpCenter = this.helpCenterRepository.create({ ...createHelpCenterDto, + content: translatedContent, author: `${fullUser.first_name} ${fullUser.last_name}`, }); const newEntity = await this.helpCenterRepository.save(helpCenter); @@ -49,11 +61,18 @@ export class HelpCenterService { }; } - async findAll(): Promise { + async findAll(language?: string): Promise { const centres = await this.helpCenterRepository.find(); + const translatedhCTopics = await Promise.all( + centres.map(async topic => { + topic.title = await this.translateContent(topic.title, language); + topic.content = await this.translateContent(topic.content, language); + return topic; + }) + ); return { - data: centres, + data: translatedhCTopics, status_code: HttpStatus.OK, message: REQUEST_SUCCESSFUL, }; From c4365685da8d3aaf9e6040211224100135dc887e Mon Sep 17 00:00:00 2001 From: Amal-Salam Date: Sat, 24 Aug 2024 04:27:44 +0100 Subject: [PATCH 28/68] chore: include superadmin guard --- src/modules/help-center/help-center.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/help-center/help-center.controller.ts b/src/modules/help-center/help-center.controller.ts index d70f9faa3..fc04f4ee5 100644 --- a/src/modules/help-center/help-center.controller.ts +++ b/src/modules/help-center/help-center.controller.ts @@ -35,7 +35,7 @@ export class HelpCenterController { @ApiBearerAuth() @Post('topics') - // @UseGuards(SuperAdminGuard) + @UseGuards(SuperAdminGuard) @ApiOperation({ summary: 'Create a new help center topic' }) @ApiResponse({ status: 201, description: 'The topic has been successfully created.' }) @ApiResponse({ status: 400, description: 'Invalid input data.' }) From 7bec0d7f704f96ddc300b4dccaac2035a758e909 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 09:36:48 +0300 Subject: [PATCH 29/68] fix: testing errors --- .../billing-plans/billing-plan.controller.ts | 4 +-- .../billing-plans/billing-plan.service.ts | 28 ++++--------------- .../billing-plans/dto/billing-plan.dto.ts | 2 +- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index a19119943..2b3da923b 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Param, Post } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Param, Post, Body } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags, ApiBody } from '@nestjs/swagger'; import { BillingPlanService } from './billing-plan.service'; import { skipAuth } from '../../helpers/skipAuth'; import { BillingPlanDto } from './dto/billing-plan.dto'; diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index 765663f8f..1c067e47d 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -4,6 +4,8 @@ import { BillingPlan } from './entities/billing-plan.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { BillingPlanDto } from "./dto/billing-plan.dto"; import * as SYS_MSG from "../../helpers/SystemMessages"; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { BillingPlanMapper } from './mapper/billing-plan.mapper'; @Injectable() export class BillingPlanService { @@ -19,7 +21,7 @@ export class BillingPlanService { } }); - if (billingPlan.length > 0) { + if (!billingPlan) { throw new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); } @@ -41,7 +43,7 @@ export class BillingPlanService { throw new NotFoundException('No billing plans found'); } - const plans = allPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })); + const plans = allPlans.map(plan => BillingPlanMapper.mapToEntity(plan)); return { message: 'Billing plans retrieved successfully', @@ -74,7 +76,7 @@ export class BillingPlanService { throw new NotFoundException('Billing plan not found'); } - const plan = { id: billingPlan.id, name: billingPlan.name, price: billingPlan.price }; + const plan = BillingPlanMapper.mapToEntity(billingPlan); return { message: 'Billing plan retrieved successfully', @@ -95,24 +97,4 @@ export class BillingPlanService { } } - private createBillingPlanEntities() { - const freePlan = this.billingPlanRepository.create({ - name: 'Free', - price: 0, - }); - const basicPlan = this.billingPlanRepository.create({ - name: 'Basic', - price: 20, - }); - const advancedPlan = this.billingPlanRepository.create({ - name: 'Advanced', - price: 50, - }); - const premiumPlan = this.billingPlanRepository.create({ - name: 'Premium', - price: 100, - }); - - return [freePlan, basicPlan, advancedPlan, premiumPlan]; - } } diff --git a/src/modules/billing-plans/dto/billing-plan.dto.ts b/src/modules/billing-plans/dto/billing-plan.dto.ts index 18c790d1e..171bc6fdc 100644 --- a/src/modules/billing-plans/dto/billing-plan.dto.ts +++ b/src/modules/billing-plans/dto/billing-plan.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, IsOptional, IsNumberString } from "class-validator"; -export class CreateBillingPlanDto { +export class BillingPlanDto { @ApiProperty({ example: "Free" }) @IsString() name: string; From b7b4d841b7c240598fd3762db85ecfba8650cdce Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 09:42:37 +0300 Subject: [PATCH 30/68] feat: updating mapper --- src/modules/billing-plans/billing-plan.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index 1c067e47d..21a91680f 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -27,7 +27,7 @@ export class BillingPlanService { const newPlan = this.billingPlanRepository.create(createBillingPlanDto); const createdPlan = await this.billingPlanRepository.save(newPlan); - const plan = BillingPlanMapper.mapToEntity(createdPlan) + const plan = BillingPlanMapper.mapToResponseFormat(createdPlan) return { message: SYS_MSG.BILLING_PLAN_CREATED, @@ -43,7 +43,7 @@ export class BillingPlanService { throw new NotFoundException('No billing plans found'); } - const plans = allPlans.map(plan => BillingPlanMapper.mapToEntity(plan)); + const plans = allPlans.map(plan => BillingPlanMapper.mapToResponseFormat(plan)); return { message: 'Billing plans retrieved successfully', @@ -76,7 +76,7 @@ export class BillingPlanService { throw new NotFoundException('Billing plan not found'); } - const plan = BillingPlanMapper.mapToEntity(billingPlan); + const plan = BillingPlanMapper.mapToResponseFormat(billingPlan); return { message: 'Billing plan retrieved successfully', From 896deca0b170ffb8fe18df5edec2352b1fcfed22 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 09:47:52 +0300 Subject: [PATCH 31/68] fix: passing tests --- .../tests/billing-plan.service.spec.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index 074eab72f..b01931a24 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -3,7 +3,7 @@ import { BillingPlanService } from '../billing-plan.service'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingPlan } from '../entities/billing-plan.entity'; -import { NotFoundException, BadRequestException } from '@nestjs/common'; +import { NotFoundException, BadRequestException, HttpStatus } from '@nestjs/common'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; import * as SYS_MSG from "../../../helpers/SystemMessages"; @@ -28,10 +28,26 @@ describe('BillingPlanService', () => { describe('createBillingPlan', () => { it('should throw an error if they already exist', async () => { - const createPlanDto = { name: 'Free', amount: 0, frequency: 'never', is_active: true } - const billingPlans = { id: '1', name: 'Free', price: 0 }; - - jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); + const createPlanDto = { + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true + }; + + const billingPlan = { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date() + }; + + jest.spyOn(repository, 'findOne').mockResolvedValue(billingPlan as BillingPlan); const result = await service.createBillingPlan(createPlanDto); From fbcf46cf8dcbf9471b4ca38c8027ab9005236c38 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 09:55:04 +0300 Subject: [PATCH 32/68] fix: other tests --- .../tests/billing-plan.service.spec.ts | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index b01931a24..5d012240d 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -6,6 +6,7 @@ import { BillingPlan } from '../entities/billing-plan.entity'; import { NotFoundException, BadRequestException, HttpStatus } from '@nestjs/common'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; import * as SYS_MSG from "../../../helpers/SystemMessages"; +import { BillingPlanMapper } from '../mapper/billing-plan.mapper'; describe('BillingPlanService', () => { let service: BillingPlanService; @@ -35,7 +36,7 @@ describe('BillingPlanService', () => { frequency: 'never', is_active: true }; - + const billingPlan = { id: '1', name: 'Free', @@ -58,8 +59,36 @@ describe('BillingPlanService', () => { describe('getAllBillingPlans', () => { it('should return all billing plans', async () => { const billingPlans = [ - { id: '1', name: 'Free', price: 0 }, - { id: '2', name: 'Basic', price: 20 }, + { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date() + }, + { + id: '2', + name: 'Standard', + description: 'standard plan', + amount: 50, + frequency: 'monthly', + is_active: true, + created_at: new Date(), + updated_at: new Date() + }, + { + id: '1', + name: 'Premium', + description: 'premium plan', + amount: 120, + frequency: 'monthly', + is_active: true, + created_at: new Date(), + updated_at: new Date() + } ]; jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); @@ -68,7 +97,7 @@ describe('BillingPlanService', () => { expect(result).toEqual({ message: 'Billing plans retrieved successfully', - data: billingPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })), + data: billingPlans.map(plan => BillingPlanMapper.mapToResponseFormat(plan)), }); }); @@ -81,7 +110,16 @@ describe('BillingPlanService', () => { describe('getSingleBillingPlan', () => { it('should return a single billing plan', async () => { - const billingPlan = { id: '1', name: 'Free', price: 0 }; + const billingPlan = { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date() + }; jest.spyOn(repository, 'findOneBy').mockResolvedValue(billingPlan as BillingPlan); @@ -89,7 +127,7 @@ describe('BillingPlanService', () => { expect(result).toEqual({ message: 'Billing plan retrieved successfully', - data: { id: billingPlan.id, name: billingPlan.name, price: billingPlan.price }, + data: BillingPlanMapper.mapToResponseFormat(result), }); }); From 7e3acab1d15e683fc65b98fa19d4e616d60bae41 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:01:05 +0300 Subject: [PATCH 33/68] fix: test error for single plan --- src/modules/billing-plans/tests/billing-plan.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index 5d012240d..7823f24fb 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -127,7 +127,7 @@ describe('BillingPlanService', () => { expect(result).toEqual({ message: 'Billing plan retrieved successfully', - data: BillingPlanMapper.mapToResponseFormat(result), + data: BillingPlanMapper.mapToResponseFormat(result.data), }); }); From e7602ea5fa055740e134980e0f613bac71450c18 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui <105012834+MuthoniMN@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:15:59 +0300 Subject: [PATCH 34/68] fix: correcting parameter --- src/modules/billing-plans/tests/billing-plan.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index 7823f24fb..24f586b75 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -127,7 +127,7 @@ describe('BillingPlanService', () => { expect(result).toEqual({ message: 'Billing plan retrieved successfully', - data: BillingPlanMapper.mapToResponseFormat(result.data), + data: BillingPlanMapper.mapToResponseFormat(billingPlan), }); }); From a56895086a09528a4b689ddf33124fcdfcf77f9d Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 10:47:39 +0300 Subject: [PATCH 35/68] fix: fixing logic --- .../billing-plans/billing-plan.service.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index 21a91680f..0b73c60ee 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -2,11 +2,11 @@ import { Injectable, HttpStatus, HttpException, BadRequestException, NotFoundExc import { Repository } from 'typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { BillingPlanDto } from "./dto/billing-plan.dto"; -import * as SYS_MSG from "../../helpers/SystemMessages"; +import { BillingPlanDto } from './dto/billing-plan.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; import { CustomHttpException } from '../../helpers/custom-http-filter'; import { BillingPlanMapper } from './mapper/billing-plan.mapper'; - + @Injectable() export class BillingPlanService { constructor( @@ -14,20 +14,20 @@ export class BillingPlanService { private readonly billingPlanRepository: Repository ) {} - async createBillingPlan(createBillingPlanDto: BillingPlanDto ) { + async createBillingPlan(createBillingPlanDto: BillingPlanDto) { const billingPlan = await this.billingPlanRepository.findOne({ where: { - name: createBillingPlanDto.name - } + name: createBillingPlanDto.name, + }, }); - if (!billingPlan) { + if (billingPlan) { throw new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); } const newPlan = this.billingPlanRepository.create(createBillingPlanDto); const createdPlan = await this.billingPlanRepository.save(newPlan); - const plan = BillingPlanMapper.mapToResponseFormat(createdPlan) + const plan = BillingPlanMapper.mapToResponseFormat(createdPlan); return { message: SYS_MSG.BILLING_PLAN_CREATED, @@ -96,5 +96,4 @@ export class BillingPlanService { ); } } - } From 2c4b593315afe3f1b2ed2e7c5cf2b688e568bbaa Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 10:53:24 +0300 Subject: [PATCH 36/68] fix: testing error correctly --- .../tests/billing-plan.service.spec.ts | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index 24f586b75..f6ea6d73a 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -5,7 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingPlan } from '../entities/billing-plan.entity'; import { NotFoundException, BadRequestException, HttpStatus } from '@nestjs/common'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; -import * as SYS_MSG from "../../../helpers/SystemMessages"; +import * as SYS_MSG from '../../../helpers/SystemMessages'; import { BillingPlanMapper } from '../mapper/billing-plan.mapper'; describe('BillingPlanService', () => { @@ -29,66 +29,66 @@ describe('BillingPlanService', () => { describe('createBillingPlan', () => { it('should throw an error if they already exist', async () => { - const createPlanDto = { - name: 'Free', - description: 'free plan', - amount: 0, - frequency: 'never', - is_active: true + const createPlanDto = { + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, }; - const billingPlan = { - id: '1', - name: 'Free', - description: 'free plan', - amount: 0, - frequency: 'never', - is_active: true, - created_at: new Date(), - updated_at: new Date() + const billingPlan = { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date(), }; jest.spyOn(repository, 'findOne').mockResolvedValue(billingPlan as BillingPlan); - const result = await service.createBillingPlan(createPlanDto); - - expect(result).rejects.toThrow(new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST)) + await service + .createBillingPlan(createPlanDto) + .rejects.toThrow(new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST)); }); }); describe('getAllBillingPlans', () => { it('should return all billing plans', async () => { const billingPlans = [ - { - id: '1', - name: 'Free', - description: 'free plan', - amount: 0, - frequency: 'never', - is_active: true, - created_at: new Date(), - updated_at: new Date() - }, - { - id: '2', - name: 'Standard', - description: 'standard plan', - amount: 50, - frequency: 'monthly', - is_active: true, - created_at: new Date(), - updated_at: new Date() - }, - { - id: '1', - name: 'Premium', - description: 'premium plan', - amount: 120, - frequency: 'monthly', - is_active: true, - created_at: new Date(), - updated_at: new Date() - } + { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '2', + name: 'Standard', + description: 'standard plan', + amount: 50, + frequency: 'monthly', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '1', + name: 'Premium', + description: 'premium plan', + amount: 120, + frequency: 'monthly', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }, ]; jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); @@ -110,15 +110,15 @@ describe('BillingPlanService', () => { describe('getSingleBillingPlan', () => { it('should return a single billing plan', async () => { - const billingPlan = { - id: '1', - name: 'Free', - description: 'free plan', - amount: 0, - frequency: 'never', - is_active: true, - created_at: new Date(), - updated_at: new Date() + const billingPlan = { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date(), }; jest.spyOn(repository, 'findOneBy').mockResolvedValue(billingPlan as BillingPlan); From e9001ec882cb23db6408be2adcef5ab78bd653a1 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 10:55:49 +0300 Subject: [PATCH 37/68] fix: testing error correctly --- .../billing-plans/tests/billing-plan.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index f6ea6d73a..d16161069 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -50,9 +50,9 @@ describe('BillingPlanService', () => { jest.spyOn(repository, 'findOne').mockResolvedValue(billingPlan as BillingPlan); - await service - .createBillingPlan(createPlanDto) - .rejects.toThrow(new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST)); + await expect(service.createBillingPlan(createPlanDto)).rejects.toThrow( + new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST) + ); }); }); From 85d76f4cc43753a2d06cc2cbdc796c264ab14f73 Mon Sep 17 00:00:00 2001 From: Amal-Salam Date: Sat, 24 Aug 2024 09:22:41 +0100 Subject: [PATCH 38/68] refactor: removed the try catch, included httpstatus --- src/helpers/SystemMessages.ts | 1 - src/modules/contact-us/contact-us.service.ts | 13 +++------- .../test/contact-us.service.spec.ts | 24 +++---------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 739d46ef2..70b2a6837 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -104,4 +104,3 @@ export const INVALID_FILE_TYPE = resource => { return `Invalid file type. Allowed types: ${resource}`; }; export const INQUIRY_SENT = 'Inquiry sent successfully'; -export const INQUIRY_NOT_SENT = 'Failed to send contact inquiry email'; diff --git a/src/modules/contact-us/contact-us.service.ts b/src/modules/contact-us/contact-us.service.ts index 54e100ef8..146ce0bcb 100644 --- a/src/modules/contact-us/contact-us.service.ts +++ b/src/modules/contact-us/contact-us.service.ts @@ -1,4 +1,4 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateContactDto } from '../contact-us/dto/create-contact-us.dto'; @@ -18,17 +18,10 @@ export class ContactUsService { async createContactMessage(createContactDto: CreateContactDto) { const contact = this.contactRepository.create(createContactDto); await this.contactRepository.save(contact); - - try { - await this.sendEmail(createContactDto); - } catch (error) { - console.log(error.message); - throw new InternalServerErrorException(SYS_MSG.INQUIRY_NOT_SENT); - } - + await this.sendEmail(createContactDto); return { message: SYS_MSG.INQUIRY_SENT, - status_code: 200, + status_code: HttpStatus.OK, }; } diff --git a/src/modules/contact-us/test/contact-us.service.spec.ts b/src/modules/contact-us/test/contact-us.service.spec.ts index 3c86f2419..69a185716 100644 --- a/src/modules/contact-us/test/contact-us.service.spec.ts +++ b/src/modules/contact-us/test/contact-us.service.spec.ts @@ -3,8 +3,9 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { ContactUs } from '../entities/contact-us.entity'; import { MailerService } from '@nestjs-modules/mailer'; import { CreateContactDto } from '../dto/create-contact-us.dto'; -import { InternalServerErrorException } from '@nestjs/common'; import { ContactUsService } from '../contact-us.service'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; +import { HttpStatus } from '@nestjs/common'; describe('ContactUsService', () => { let service: ContactUsService; @@ -38,10 +39,6 @@ describe('ContactUsService', () => { service = module.get(ContactUsService); }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); - describe('createContactMessage', () => { it('should create a contact message and send an email', async () => { const createContactDto: CreateContactDto = { @@ -60,22 +57,7 @@ describe('ContactUsService', () => { expect(mockRepository.create).toHaveBeenCalledWith(createContactDto); expect(mockRepository.save).toHaveBeenCalledWith(createContactDto); expect(mockMailerService.sendMail).toHaveBeenCalled(); - expect(result).toEqual({ message: 'Inquiry sent successfully', status_code: 200 }); - }); - - it('should throw InternalServerErrorException when email sending fails', async () => { - const createContactDto: CreateContactDto = { - name: 'John Doe', - email: 'john@example.com', - phone: 123456789, - message: 'Test message', - }; - - mockRepository.create.mockReturnValue(createContactDto); - mockRepository.save.mockResolvedValue(createContactDto); - mockMailerService.sendMail.mockRejectedValue(new Error('Email sending failed')); - - await expect(service.createContactMessage(createContactDto)).rejects.toThrow(InternalServerErrorException); + expect(result).toEqual({ message: SYS_MSG.INQUIRY_SENT, status_code: HttpStatus.OK }); }); }); }); From 99b8a6d9c6b185b80eaf340f3a80f12117bf9c34 Mon Sep 17 00:00:00 2001 From: ObodoakorDavid Date: Sat, 24 Aug 2024 10:07:25 +0100 Subject: [PATCH 39/68] feat: adds api-status for health check of all endpoints --- package-lock.json | 325 ++++++++---------- package.json | 4 +- src/app.module.ts | 3 + .../api-status/api-status.controller.ts | 21 ++ src/modules/api-status/api-status.module.ts | 13 + src/modules/api-status/api-status.service.ts | 84 +++++ .../api-status/dto/create-api-status.dto.ts | 19 + .../api-status/dto/create-request.dto.ts | 22 ++ .../api-status/entities/api-status.entity.ts | 31 ++ .../api-status/entities/request.entity.ts | 27 ++ .../tests/api-status.service.spec.ts | 131 +++++++ .../billing-plans/billing-plan.module.ts | 6 +- 12 files changed, 491 insertions(+), 195 deletions(-) create mode 100644 src/modules/api-status/api-status.controller.ts create mode 100644 src/modules/api-status/api-status.module.ts create mode 100644 src/modules/api-status/api-status.service.ts create mode 100644 src/modules/api-status/dto/create-api-status.dto.ts create mode 100644 src/modules/api-status/dto/create-request.dto.ts create mode 100644 src/modules/api-status/entities/api-status.entity.ts create mode 100644 src/modules/api-status/entities/request.entity.ts create mode 100644 src/modules/api-status/tests/api-status.service.spec.ts diff --git a/package-lock.json b/package-lock.json index 5261303af..b91b464ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "UNLICENSED", "dependencies": { + "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", "@google/generative-ai": "^0.17.0", @@ -50,7 +51,7 @@ "pg": "^8.12.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "sharp": "^0.33.4", + "sharp": "^0.33.5", "speakeasy": "^2.0.0", "supertest": "^7.0.0", "typeorm": "^0.3.20", @@ -1115,6 +1116,7 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/@css-inline/css-inline/-/css-inline-0.14.1.tgz", "integrity": "sha512-u4eku+hnPqqHIGq/ZUQcaP0TrCbYeLIYBaK7qClNRGZbnh8RC4gVxLEIo8Pceo1nOK9E5G4Lxzlw5KnXcvflfA==", + "dev": true, "engines": { "node": ">= 10" }, @@ -1138,6 +1140,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -1153,6 +1156,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -1168,6 +1172,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -1183,6 +1188,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -1198,6 +1204,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1213,6 +1220,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1228,6 +1236,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1243,6 +1252,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1258,6 +1268,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1273,6 +1284,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1286,6 +1298,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -1524,431 +1537,361 @@ "license": "BSD-3-Clause" }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -18683,42 +18626,42 @@ } }, "node_modules/sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { diff --git a/package.json b/package.json index b83f48286..7a476979f 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,6 @@ "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { - "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", - "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", @@ -74,7 +72,7 @@ "pg": "^8.12.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "sharp": "^0.33.4", + "sharp": "^0.33.5", "speakeasy": "^2.0.0", "supertest": "^7.0.0", "typeorm": "^0.3.20", diff --git a/src/app.module.ts b/src/app.module.ts index ff89196fc..b134d4bdb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -47,6 +47,8 @@ import { BlogCategoryModule } from './modules/blog-category/blog-category.module import { ServeStaticModule } from '@nestjs/serve-static'; import { join } from 'path'; import { LanguageGuard } from './guards/language.guard'; +import { ApiStatusModule } from './modules/api-status/api-status.module'; + @Module({ providers: [ { @@ -171,6 +173,7 @@ import { LanguageGuard } from './guards/language.guard'; index: false, }, }), + ApiStatusModule, ], controllers: [HealthController, ProbeController], }) diff --git a/src/modules/api-status/api-status.controller.ts b/src/modules/api-status/api-status.controller.ts new file mode 100644 index 000000000..fee3a699b --- /dev/null +++ b/src/modules/api-status/api-status.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Get, Post, Body } from '@nestjs/common'; +import { ApiStatusService } from './api-status.service'; +import { CreateApiStatusDto } from './dto/create-api-status.dto'; +import { skipAuth } from 'src/helpers/skipAuth'; + +@Controller('api-status') +export class ApiStatusController { + constructor(private readonly apiStatusService: ApiStatusService) {} + + @skipAuth() + @Post() + create(@Body() createApiStatusDto: CreateApiStatusDto[]) { + return this.apiStatusService.create(createApiStatusDto); + } + + @skipAuth() + @Get() + findAll() { + return this.apiStatusService.findAll(); + } +} diff --git a/src/modules/api-status/api-status.module.ts b/src/modules/api-status/api-status.module.ts new file mode 100644 index 000000000..a6512e2dc --- /dev/null +++ b/src/modules/api-status/api-status.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ApiStatusService } from './api-status.service'; +import { ApiStatusController } from './api-status.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApiHealth } from './entities/api-status.entity'; +import { Request } from './entities/request.entity'; + +@Module({ + controllers: [ApiStatusController], + imports: [TypeOrmModule.forFeature([ApiHealth, Request])], + providers: [ApiStatusService], +}) +export class ApiStatusModule {} diff --git a/src/modules/api-status/api-status.service.ts b/src/modules/api-status/api-status.service.ts new file mode 100644 index 000000000..faf7e337f --- /dev/null +++ b/src/modules/api-status/api-status.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateApiStatusDto } from './dto/create-api-status.dto'; +import { ApiHealth, ApiStatus } from './entities/api-status.entity'; +import { Request } from './entities/request.entity'; + +@Injectable() +export class ApiStatusService { + constructor( + @InjectRepository(ApiHealth) + private readonly apiHealthRepository: Repository, + @InjectRepository(Request) + private readonly requestRepository: Repository + ) {} + + async create(apiHealthDto: CreateApiStatusDto[]) { + const apiHealthList = []; + + for (const eachApiStatus of apiHealthDto) { + const apiHealth = await this.apiHealthRepository.findOne({ + where: { api_group: eachApiStatus.api_group }, + }); + + if (!apiHealth) { + const apiRequestList = []; + const apiHealth = await this.apiHealthRepository.save(eachApiStatus); + await Promise.all( + eachApiStatus.requests.map(request => { + this.requestRepository.save({ + ...request, + api_health: apiHealth, + updated_at: new Date(), + }); + apiRequestList.push(request); + }) + ); + + const savedHealth = await this.apiHealthRepository.findOne({ + where: { id: apiHealth.id }, + }); + + savedHealth.requests = apiRequestList; + await this.apiHealthRepository.save(savedHealth); + + apiHealthList.push(apiHealth); + } else { + const apiRequestList = []; + + await this.requestRepository.clear(); + + await Promise.all( + eachApiStatus.requests.map(request => { + this.requestRepository.save(request); + apiRequestList.push(request); + }) + ); + + Object.assign(apiHealth, ApiStatus); + apiHealth.requests = apiRequestList; + apiHealth.updated_at = new Date(); + apiHealth.lastChecked = new Date(); + await this.apiHealthRepository.save(apiHealth); + apiHealthList.push(apiHealth); + } + } + + return { + message: `Status Added Successfully`, + data: apiHealthList, + }; + } + + async findAll() { + const apiHealthData = await this.apiHealthRepository.find({ + relations: ['requests'], + }); + + return { + message: `Health Status Retrieved Successfully`, + data: apiHealthData, + }; + } +} diff --git a/src/modules/api-status/dto/create-api-status.dto.ts b/src/modules/api-status/dto/create-api-status.dto.ts new file mode 100644 index 000000000..f91cfa58c --- /dev/null +++ b/src/modules/api-status/dto/create-api-status.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { CreateRequestDto } from './create-request.dto'; + +export class CreateApiStatusDto { + @IsString() + api_group: string; + + @IsString() + status: string; + + @IsString() + details: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateRequestDto) + requests: CreateRequestDto[]; +} diff --git a/src/modules/api-status/dto/create-request.dto.ts b/src/modules/api-status/dto/create-request.dto.ts new file mode 100644 index 000000000..32ada4ce3 --- /dev/null +++ b/src/modules/api-status/dto/create-request.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsInt } from 'class-validator'; + +export class CreateRequestDto { + @IsString() + requestName: string; + + @IsString() + requestUrl: string; + + @IsOptional() + @IsString() + status?: string; + + @IsInt() + responseTime: number; + + @IsInt() + statusCode: number; + + @IsArray() + errors: string[]; +} diff --git a/src/modules/api-status/entities/api-status.entity.ts b/src/modules/api-status/entities/api-status.entity.ts new file mode 100644 index 000000000..9d087d1ee --- /dev/null +++ b/src/modules/api-status/entities/api-status.entity.ts @@ -0,0 +1,31 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Entity, Column, OneToMany } from 'typeorm'; +import { Request } from './request.entity'; + +export enum ApiStatus { + OPERATIONAL = 'operational', + DEGRADED = 'degraded', + DOWN = 'down', +} + +@Entity('api_health') +export class ApiHealth extends AbstractBaseEntity { + @Column() + api_group: string; + + @Column({ + type: 'enum', + enum: ApiStatus, + default: ApiStatus.OPERATIONAL, + }) + status: string; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + lastChecked: Date; + + @Column() + details: string; + + @OneToMany(() => Request, request => request.api_health) + requests: Request[]; +} diff --git a/src/modules/api-status/entities/request.entity.ts b/src/modules/api-status/entities/request.entity.ts new file mode 100644 index 000000000..b17336f7c --- /dev/null +++ b/src/modules/api-status/entities/request.entity.ts @@ -0,0 +1,27 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Entity, Column, ManyToOne } from 'typeorm'; +import { ApiHealth } from './api-status.entity'; + +@Entity('request') +export class Request extends AbstractBaseEntity { + @Column() + requestName: string; + + @Column({ nullable: true }) + status: string; + + @Column() + requestUrl: string; + + @Column() + responseTime: number; + + @Column() + statusCode: number; + + @Column('simple-array', { nullable: true }) + errors: string[]; + + @ManyToOne(() => ApiHealth, apiHealth => apiHealth.requests) + api_health: ApiHealth; +} diff --git a/src/modules/api-status/tests/api-status.service.spec.ts b/src/modules/api-status/tests/api-status.service.spec.ts new file mode 100644 index 000000000..6342a8d03 --- /dev/null +++ b/src/modules/api-status/tests/api-status.service.spec.ts @@ -0,0 +1,131 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApiStatusService } from '../api-status.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { ApiHealth } from '../entities/api-status.entity'; +import { Repository } from 'typeorm'; +import { CreateApiStatusDto } from '../dto/create-api-status.dto'; +import { Request } from '../entities/request.entity'; + +describe('ApiStatusService', () => { + let service: ApiStatusService; + let apiHealthRepository: Repository; + let requestRepository: Repository; + + const mockApiHealthRepository = () => ({ + findOne: jest.fn(), + save: jest.fn(), + find: jest.fn(), + }); + + const mockRequestRepository = () => ({ + save: jest.fn(), + clear: jest.fn(), + delete: jest.fn(), + }); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApiStatusService, + { + provide: getRepositoryToken(ApiHealth), + useValue: mockApiHealthRepository(), + }, + { + provide: getRepositoryToken(Request), + useValue: mockRequestRepository(), + }, + ], + }).compile(); + + service = module.get(ApiStatusService); + apiHealthRepository = module.get>(getRepositoryToken(ApiHealth)); + requestRepository = module.get>(getRepositoryToken(Request)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create new ApiHealth and Requests', async () => { + const createApiStatusDto: CreateApiStatusDto[] = [ + { + api_group: 'Blogs API 2', + status: 'operational', + details: 'All tests passed', + requests: [], + }, + ]; + + const savedApiHealth = new ApiHealth(); + savedApiHealth.requests = []; + + const savedRequest: Request = { + id: '1', + requestName: 'Blog', + requestUrl: '/api/v1/blog', + responseTime: 2000, + errors: ['Hello'], + status: 'Bad Request', + statusCode: 400, + created_at: new Date(), + updated_at: new Date(), + api_health: savedApiHealth, + }; + + jest.spyOn(apiHealthRepository, 'findOne').mockResolvedValue(savedApiHealth); + jest.spyOn(apiHealthRepository, 'save').mockResolvedValue(savedApiHealth); + jest.spyOn(requestRepository, 'save').mockResolvedValue(savedRequest); + + const result = await service.create(createApiStatusDto); + + expect(apiHealthRepository.findOne).toHaveBeenCalled(); + expect(apiHealthRepository.save).toHaveBeenCalled(); + expect(result).toEqual({ + message: 'Status Added Successfully', + data: [savedApiHealth], + }); + }); + + describe('create', () => { + describe('findAll', () => { + it('should return all ApiHealth records with their requests', async () => { + const apiHealthData: ApiHealth[] = [ + { + id: '1', + api_group: 'Test', + status: 'operational', + details: 'All Tests passed', + requests: [], + created_at: new Date(), + updated_at: new Date(), + lastChecked: new Date(), + }, + { + id: '2', + api_group: 'Test', + status: 'operational', + details: 'All Tests passed', + requests: [], + created_at: new Date(), + updated_at: new Date(), + lastChecked: new Date(), + }, + ]; + + jest.spyOn(apiHealthRepository, 'find').mockResolvedValue(apiHealthData); + + const result = await service.findAll(); + + expect(apiHealthRepository.find).toHaveBeenCalledWith({ + relations: ['requests'], + }); + expect(result).toEqual({ + message: 'Health Status Retrieved Successfully', + data: apiHealthData, + }); + }); + }); + }); + }); +}); diff --git a/src/modules/billing-plans/billing-plan.module.ts b/src/modules/billing-plans/billing-plan.module.ts index da915ec3f..3447d932d 100644 --- a/src/modules/billing-plans/billing-plan.module.ts +++ b/src/modules/billing-plans/billing-plan.module.ts @@ -3,9 +3,13 @@ import { BillingPlanService } from './billing-plan.service'; import { BillingPlanController } from './billing-plan.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; +import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([BillingPlan])], + imports: [TypeOrmModule.forFeature([BillingPlan, User, Organisation, OrganisationUserRole, Role])], controllers: [BillingPlanController], providers: [BillingPlanService], }) From 7447e4f73e920ebd41d34afa8425a7b4ddf45fc6 Mon Sep 17 00:00:00 2001 From: ObodoakorDavid Date: Sat, 24 Aug 2024 10:10:39 +0100 Subject: [PATCH 40/68] fix: adds the package back into package.json --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 7a476979f..a55b07bad 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { + "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", + "@css-inline/css-inline": "^0.14.1", "@faker-js/faker": "^8.4.1", "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", From 7c3170f8764097dc4b3ff21cfdcb8d49e1d197bf Mon Sep 17 00:00:00 2001 From: ObodoakorDavid Date: Sat, 24 Aug 2024 10:15:02 +0100 Subject: [PATCH 41/68] fix: fix import path for skipAuth --- src/modules/api-status/api-status.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/api-status/api-status.controller.ts b/src/modules/api-status/api-status.controller.ts index fee3a699b..8fca699b0 100644 --- a/src/modules/api-status/api-status.controller.ts +++ b/src/modules/api-status/api-status.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Post, Body } from '@nestjs/common'; import { ApiStatusService } from './api-status.service'; import { CreateApiStatusDto } from './dto/create-api-status.dto'; -import { skipAuth } from 'src/helpers/skipAuth'; +import { skipAuth } from '../../helpers/skipAuth'; @Controller('api-status') export class ApiStatusController { From d783fad5fa8f21d15465aaa681136e7577547b12 Mon Sep 17 00:00:00 2001 From: Amal-Salam Date: Sat, 24 Aug 2024 10:25:01 +0100 Subject: [PATCH 42/68] chore: latest commit --- src/helpers/SystemMessages.ts | 1 + .../help-center/docs/helpCenter-swagger.ts | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/modules/help-center/docs/helpCenter-swagger.ts diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 739d46ef2..eedeaeee3 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -105,3 +105,4 @@ export const INVALID_FILE_TYPE = resource => { }; export const INQUIRY_SENT = 'Inquiry sent successfully'; export const INQUIRY_NOT_SENT = 'Failed to send contact inquiry email'; +export const TOPIC_NOT_FOUND = `Help center topic with ID not found`; diff --git a/src/modules/help-center/docs/helpCenter-swagger.ts b/src/modules/help-center/docs/helpCenter-swagger.ts new file mode 100644 index 000000000..79aece400 --- /dev/null +++ b/src/modules/help-center/docs/helpCenter-swagger.ts @@ -0,0 +1,34 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiResponse, ApiOperation } from '@nestjs/swagger'; + +export function CreateHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Create a new help center topic' }), + ApiResponse({ status: 201, description: 'The topic has been successfully created.' }), + ApiResponse({ status: 400, description: 'Invalid input data.' }), + ApiResponse({ status: 400, description: 'This question already exists.' }) + ); +} + +export function GetAllHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get all help center topics' }), + ApiResponse({ status: 200, description: 'The found records' }) + ); +} + +export function GetByIdHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get a help center topic by ID' }), + ApiResponse({ status: 200, description: 'The found record' }), + ApiResponse({ status: 404, description: 'Topic not found' }) + ); +} + +export function SearchHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Search help center topics' }), + ApiResponse({ status: 200, description: 'The found records' }), + ApiResponse({ status: 422, description: 'Invalid search criteria.' }) + ); +} From 4615d46a82c0df2b08c75d8b3e515b8450b9a153 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 12:51:59 +0300 Subject: [PATCH 43/68] fix: update billing plan dto --- src/modules/billing-plans/dto/billing-plan.dto.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/billing-plans/dto/billing-plan.dto.ts b/src/modules/billing-plans/dto/billing-plan.dto.ts index 171bc6fdc..20cad8604 100644 --- a/src/modules/billing-plans/dto/billing-plan.dto.ts +++ b/src/modules/billing-plans/dto/billing-plan.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsOptional, IsNumberString } from "class-validator"; +import { IsString, IsOptional, IsNumberString, IsBoolean } from 'class-validator'; export class BillingPlanDto { - @ApiProperty({ example: "Free" }) + @ApiProperty({ example: 'Free' }) @IsString() name: string; @@ -20,5 +20,6 @@ export class BillingPlanDto { amount: number; @ApiProperty({ example: 'true' }) + @IsBoolean() is_active: boolean; } From f40c71f23bfe681f01686e631a8ac7ab5282df87 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 13:00:24 +0300 Subject: [PATCH 44/68] fix: adding user entity --- src/modules/billing-plans/billing-plan.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/billing-plans/billing-plan.module.ts b/src/modules/billing-plans/billing-plan.module.ts index da915ec3f..0003ed54c 100644 --- a/src/modules/billing-plans/billing-plan.module.ts +++ b/src/modules/billing-plans/billing-plan.module.ts @@ -3,9 +3,10 @@ import { BillingPlanService } from './billing-plan.service'; import { BillingPlanController } from './billing-plan.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; +import { User } from '../user/entities/user.entity'; @Module({ - imports: [TypeOrmModule.forFeature([BillingPlan])], + imports: [TypeOrmModule.forFeature([BillingPlan, User])], controllers: [BillingPlanController], providers: [BillingPlanService], }) From 73fe3806c731e6c0d90e0fe305e4fc14db06dc2c Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 13:10:32 +0300 Subject: [PATCH 45/68] fix: adding dependencies --- src/modules/billing-plans/billing-plan.module.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/billing-plans/billing-plan.module.ts b/src/modules/billing-plans/billing-plan.module.ts index 0003ed54c..3447d932d 100644 --- a/src/modules/billing-plans/billing-plan.module.ts +++ b/src/modules/billing-plans/billing-plan.module.ts @@ -4,9 +4,12 @@ import { BillingPlanController } from './billing-plan.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([BillingPlan, User])], + imports: [TypeOrmModule.forFeature([BillingPlan, User, Organisation, OrganisationUserRole, Role])], controllers: [BillingPlanController], providers: [BillingPlanService], }) From 314eda9d8a6cc009e791603e88ff241437b89556 Mon Sep 17 00:00:00 2001 From: olamstevy Date: Sat, 24 Aug 2024 11:29:25 +0100 Subject: [PATCH 46/68] docs: added documentation for contact-us --- .vscode/settings.json | 2 +- .../billing-plans/billing-plan.module.ts | 6 ++++- .../contact-us/contact-us.controller.ts | 12 +++++----- src/modules/contact-us/contact-us.service.ts | 2 +- .../docs/contact-us-swagger.docs.ts | 21 +++++++++++++++++ .../dto/create-contact-error.dto.ts | 23 +++++++++++++++++++ .../dto/create-contact-response.dto.ts | 14 +++++++++++ .../contact-us/dto/create-contact-us.dto.ts | 12 +++++----- 8 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 src/modules/contact-us/docs/contact-us-swagger.docs.ts create mode 100644 src/modules/contact-us/dto/create-contact-error.dto.ts create mode 100644 src/modules/contact-us/dto/create-contact-response.dto.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 058a5d823..33d4973e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,6 +33,6 @@ "source.fixAll": "explicit" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "dbaeumer.vscode-eslint" } } diff --git a/src/modules/billing-plans/billing-plan.module.ts b/src/modules/billing-plans/billing-plan.module.ts index da915ec3f..3447d932d 100644 --- a/src/modules/billing-plans/billing-plan.module.ts +++ b/src/modules/billing-plans/billing-plan.module.ts @@ -3,9 +3,13 @@ import { BillingPlanService } from './billing-plan.service'; import { BillingPlanController } from './billing-plan.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; +import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([BillingPlan])], + imports: [TypeOrmModule.forFeature([BillingPlan, User, Organisation, OrganisationUserRole, Role])], controllers: [BillingPlanController], providers: [BillingPlanService], }) diff --git a/src/modules/contact-us/contact-us.controller.ts b/src/modules/contact-us/contact-us.controller.ts index 39b71f7a8..cbb8ef67c 100644 --- a/src/modules/contact-us/contact-us.controller.ts +++ b/src/modules/contact-us/contact-us.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Post, Body, HttpCode } from '@nestjs/common'; +import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { ContactUsService } from './contact-us.service'; import { CreateContactDto } from '../contact-us/dto/create-contact-us.dto'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags } from '@nestjs/swagger'; import { skipAuth } from '../..//helpers/skipAuth'; +import { createContactDocs } from './docs/contact-us-swagger.docs'; @ApiTags('Contact Us') @skipAuth() @@ -10,11 +11,10 @@ import { skipAuth } from '../..//helpers/skipAuth'; export class ContactUsController { constructor(private readonly contactUsService: ContactUsService) {} - @skipAuth() - @ApiOperation({ summary: 'Post a Contact us Message' }) - @ApiBearerAuth() @Post() - @HttpCode(200) + @skipAuth() + @HttpCode(HttpStatus.CREATED) + @createContactDocs() async createContact(@Body() createContactDto: CreateContactDto) { return this.contactUsService.createContactMessage(createContactDto); } diff --git a/src/modules/contact-us/contact-us.service.ts b/src/modules/contact-us/contact-us.service.ts index 493cef659..19b2aa1a1 100644 --- a/src/modules/contact-us/contact-us.service.ts +++ b/src/modules/contact-us/contact-us.service.ts @@ -21,7 +21,7 @@ export class ContactUsService { await this.sendEmail(createContactDto); return { message: SYS_MSG.INQUIRY_SENT, - status_code: HttpStatus.OK, + status_code: HttpStatus.CREATED, }; } diff --git a/src/modules/contact-us/docs/contact-us-swagger.docs.ts b/src/modules/contact-us/docs/contact-us-swagger.docs.ts new file mode 100644 index 000000000..4728a5ba6 --- /dev/null +++ b/src/modules/contact-us/docs/contact-us-swagger.docs.ts @@ -0,0 +1,21 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiProperty, ApiResponse } from '@nestjs/swagger'; +import { CreateContactResponseDto } from '../dto/create-contact-response.dto'; +import { CreateContactErrorDto } from '../dto/create-contact-error.dto'; + +export function createContactDocs() { + return applyDecorators( + ApiBearerAuth(), + ApiOperation({ summary: 'Post a Contact us Message' }), + ApiResponse({ + status: HttpStatus.CREATED, + description: 'Successfully made enquiry.', + type: CreateContactResponseDto, + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input data.', + type: CreateContactErrorDto, + }) + ); +} diff --git a/src/modules/contact-us/dto/create-contact-error.dto.ts b/src/modules/contact-us/dto/create-contact-error.dto.ts new file mode 100644 index 000000000..3d56059bd --- /dev/null +++ b/src/modules/contact-us/dto/create-contact-error.dto.ts @@ -0,0 +1,23 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateContactErrorDto { + @ApiProperty({ + description: 'HTTP status code of the error response.', + example: HttpStatus.BAD_REQUEST, + }) + status_code: number; + + @ApiProperty({ + description: 'Error message(s) describing the issue. Can be a single string or an array of strings.', + example: ['Name should not be empty', 'Email must be an email', 'Message should not be empty'], + oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }) + message: string | string[]; + + @ApiProperty({ + description: 'Error type.', + example: 'Bad Request', + }) + error: string; +} diff --git a/src/modules/contact-us/dto/create-contact-response.dto.ts b/src/modules/contact-us/dto/create-contact-response.dto.ts new file mode 100644 index 000000000..6a4070b0e --- /dev/null +++ b/src/modules/contact-us/dto/create-contact-response.dto.ts @@ -0,0 +1,14 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; + +export class CreateContactResponseDto { + @ApiProperty({ + description: 'Status code for successfull inquiry.', + example: HttpStatus.CREATED, + }) + status_code: number; + + @ApiProperty({ description: 'Response message for sent enquiry', example: SYS_MSG.INQUIRY_SENT }) + messsage: string; +} diff --git a/src/modules/contact-us/dto/create-contact-us.dto.ts b/src/modules/contact-us/dto/create-contact-us.dto.ts index b0faa82e3..f5b3893e7 100644 --- a/src/modules/contact-us/dto/create-contact-us.dto.ts +++ b/src/modules/contact-us/dto/create-contact-us.dto.ts @@ -1,19 +1,19 @@ import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateContactDto { - @IsNotEmpty() - @IsString() + @IsNotEmpty({ message: 'Name should not be empty' }) + @IsString({ message: 'Name must be a string' }) name: string; - @IsNotEmpty() - @IsEmail() + @IsNotEmpty({ message: 'Email should not be empty' }) + @IsEmail({}, { message: 'Email must be an email' }) email: string; @IsOptional() @IsInt() phone: number; - @IsNotEmpty() - @IsString() + @IsNotEmpty({ message: 'Message should not be empty' }) + @IsString({ message: 'Message should not be a string' }) message: string; } From 4ed80dd9fd5a1c76dadeed11cada207d6f15d25d Mon Sep 17 00:00:00 2001 From: olamstevy Date: Sat, 24 Aug 2024 11:34:46 +0100 Subject: [PATCH 47/68] fix: fixed failing test from status code --- src/modules/contact-us/test/contact-us.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/contact-us/test/contact-us.service.spec.ts b/src/modules/contact-us/test/contact-us.service.spec.ts index 69a185716..a05c011a8 100644 --- a/src/modules/contact-us/test/contact-us.service.spec.ts +++ b/src/modules/contact-us/test/contact-us.service.spec.ts @@ -57,7 +57,7 @@ describe('ContactUsService', () => { expect(mockRepository.create).toHaveBeenCalledWith(createContactDto); expect(mockRepository.save).toHaveBeenCalledWith(createContactDto); expect(mockMailerService.sendMail).toHaveBeenCalled(); - expect(result).toEqual({ message: SYS_MSG.INQUIRY_SENT, status_code: HttpStatus.OK }); + expect(result).toEqual({ message: SYS_MSG.INQUIRY_SENT, status_code: HttpStatus.CREATED }); }); }); }); From 637a7748b62d2f4ddf624b2e697a5dd1ae37602a Mon Sep 17 00:00:00 2001 From: ObodoakorDavid Date: Sat, 24 Aug 2024 12:02:36 +0100 Subject: [PATCH 48/68] fix: adds return to promise array --- src/modules/api-status/api-status.service.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/modules/api-status/api-status.service.ts b/src/modules/api-status/api-status.service.ts index faf7e337f..e125a4676 100644 --- a/src/modules/api-status/api-status.service.ts +++ b/src/modules/api-status/api-status.service.ts @@ -24,15 +24,16 @@ export class ApiStatusService { if (!apiHealth) { const apiRequestList = []; - const apiHealth = await this.apiHealthRepository.save(eachApiStatus); + const savedApiHealth = await this.apiHealthRepository.save(eachApiStatus); await Promise.all( - eachApiStatus.requests.map(request => { - this.requestRepository.save({ + eachApiStatus.requests.map(async request => { + const savedRequest = await this.requestRepository.save({ ...request, - api_health: apiHealth, + api_health: savedApiHealth, updated_at: new Date(), }); - apiRequestList.push(request); + apiRequestList.push(savedRequest); + return savedRequest; }) ); @@ -50,9 +51,10 @@ export class ApiStatusService { await this.requestRepository.clear(); await Promise.all( - eachApiStatus.requests.map(request => { - this.requestRepository.save(request); - apiRequestList.push(request); + eachApiStatus.requests.map(async request => { + const savedRequest = await this.requestRepository.save(request); + apiRequestList.push(savedRequest); + return savedRequest; }) ); From 5fe232fe56fbe5e4c29941c273a232027e27b202 Mon Sep 17 00:00:00 2001 From: olamstevy Date: Sat, 24 Aug 2024 12:16:41 +0100 Subject: [PATCH 49/68] fix: refactored code and included superadmin guards when getting waiting list --- src/helpers/SystemMessages.ts | 5 +- .../waitlist/docs/waitlist-swagger.docs.ts | 38 ++++++++++++++ src/modules/waitlist/dto/get-waitlist.dto.ts | 14 +---- .../waitlist/entities/waitlist.entity.ts | 5 +- .../waitlist/tests/waitlist.service.spec.ts | 12 +++-- src/modules/waitlist/waitlist.controller.ts | 34 ++++--------- src/modules/waitlist/waitlist.module.ts | 6 ++- src/modules/waitlist/waitlist.service.ts | 51 ++++--------------- 8 files changed, 73 insertions(+), 92 deletions(-) create mode 100644 src/modules/waitlist/docs/waitlist-swagger.docs.ts diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 4c6350aaf..67b42b263 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -104,5 +104,6 @@ export const INVALID_FILE_TYPE = resource => { return `Invalid file type. Allowed types: ${resource}`; }; export const INQUIRY_SENT = 'Inquiry sent successfully'; -export const BILLING_PLAN_ALREADY_EXISTS = "Billing plan already exists"; -export const BILLING_PLAN_CREATED = "Billing plan successfully created"; +export const BILLING_PLAN_ALREADY_EXISTS = 'Billing plan already exists'; +export const BILLING_PLAN_CREATED = 'Billing plan successfully created'; +export const USER_ALREADY_WAITLISTED = 'User already on the waitlist'; diff --git a/src/modules/waitlist/docs/waitlist-swagger.docs.ts b/src/modules/waitlist/docs/waitlist-swagger.docs.ts new file mode 100644 index 000000000..97fa4f6a9 --- /dev/null +++ b/src/modules/waitlist/docs/waitlist-swagger.docs.ts @@ -0,0 +1,38 @@ +import { applyDecorators, HttpStatus, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { GetWaitlistResponseDto } from '../dto/get-waitlist.dto'; +import { ErrorResponseDto } from '../dto/waitlist-error-response.dto'; +import { WaitlistResponseDto } from '../dto/create-waitlist-response.dto'; + +export function createWaitlistDocs() { + return applyDecorators( + ApiOperation({ summary: 'Create a new waitlist entry' }), + ApiResponse({ + status: HttpStatus.CREATED, + description: 'Successfully created a waitlist entry.', + type: WaitlistResponseDto, + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input data.', + type: ErrorResponseDto, + }) + ); +} + +export function getAllWaitlistDocs() { + return applyDecorators( + ApiBearerAuth(), + ApiOperation({ summary: 'Get all waitlist entries' }), + ApiResponse({ + status: HttpStatus.OK, + description: 'Successfully retrieved all waitlist entries.', + type: GetWaitlistResponseDto, + }), + ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error.', + type: ErrorResponseDto, + }) + ); +} diff --git a/src/modules/waitlist/dto/get-waitlist.dto.ts b/src/modules/waitlist/dto/get-waitlist.dto.ts index d5d42f4ea..a06cc06c0 100644 --- a/src/modules/waitlist/dto/get-waitlist.dto.ts +++ b/src/modules/waitlist/dto/get-waitlist.dto.ts @@ -3,21 +3,9 @@ import { Waitlist } from '../entities/waitlist.entity'; import { HttpStatus } from '@nestjs/common'; export class GetWaitlistResponseDto { - @ApiProperty({ - description: 'HTTP status code indicating success.', - example: HttpStatus.OK, - }) - status_code: number; - - @ApiProperty({ - description: 'HTTP status code indicating success.', - example: HttpStatus.OK, - }) - status: number; - @ApiProperty({ description: 'Success message indicating the result of the operation.', - example: 'Added to waitlist', + example: 'Waitlist found successfully', }) message: string; diff --git a/src/modules/waitlist/entities/waitlist.entity.ts b/src/modules/waitlist/entities/waitlist.entity.ts index d4a08d70a..af93c7a9a 100644 --- a/src/modules/waitlist/entities/waitlist.entity.ts +++ b/src/modules/waitlist/entities/waitlist.entity.ts @@ -9,9 +9,6 @@ export class Waitlist extends AbstractBaseEntity { @Column({ nullable: false, unique: true }) email: string; - @Column({ nullable: false, default: false }) + @Column({ default: false }) status: boolean; - - @Column({ nullable: true }) - url_slug: string; } diff --git a/src/modules/waitlist/tests/waitlist.service.spec.ts b/src/modules/waitlist/tests/waitlist.service.spec.ts index 89d64b1e4..f0a2647d5 100644 --- a/src/modules/waitlist/tests/waitlist.service.spec.ts +++ b/src/modules/waitlist/tests/waitlist.service.spec.ts @@ -3,8 +3,6 @@ import { Waitlist } from '../entities/waitlist.entity'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { MailerService } from '@nestjs-modules/mailer'; -import { de } from '@faker-js/faker'; -import { request } from 'http'; import { CreateWaitlistDto } from '../dto/create-waitlist.dto'; import { WaitlistResponseDto } from '../dto/create-waitlist-response.dto'; import WaitlistService from '../waitlist.service'; @@ -15,17 +13,18 @@ describe('WaitlistService', () => { let mailerService: MailerService; let waitlistService: WaitlistService; - const mockUserRepository = { + const mockWaitlistRepository = { find: jest.fn(), save: jest.fn(), create: jest.fn(), + findOne: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ WaitlistService, - { provide: getRepositoryToken(Waitlist), useValue: mockUserRepository }, + { provide: getRepositoryToken(Waitlist), useValue: mockWaitlistRepository }, { provide: MailerService, useValue: { @@ -63,16 +62,19 @@ describe('WaitlistService', () => { email: 'johndoe@gmail.com', }; + const findOneSpy = jest.spyOn(waitlistRepository, 'findOne').mockResolvedValue(null); const saveSpy = jest.spyOn(waitlistRepository, 'save').mockResolvedValue(undefined); const sendMailSpy = jest.spyOn(mailerService, 'sendMail').mockResolvedValue(undefined); const result: WaitlistResponseDto = await waitlistService.createWaitlist(createWaitlistDto); + expect(findOneSpy).toHaveBeenCalledWith({ where: { email: createWaitlistDto.email } }); expect(saveSpy).toHaveBeenCalled(); expect(sendMailSpy).toHaveBeenCalledWith({ to: createWaitlistDto.email, subject: 'Waitlist Confirmation', - html: `

Hello John Doe,

Thank you for signing up for our waitlist! We will notify you once you are selected.

`, + template: 'waitlist-confirmation', + context: { recipientName: createWaitlistDto.full_name }, }); expect(result).toEqual({ message: 'You are all signed up!' }); }); diff --git a/src/modules/waitlist/waitlist.controller.ts b/src/modules/waitlist/waitlist.controller.ts index 98fcaa0f9..72dfd35db 100644 --- a/src/modules/waitlist/waitlist.controller.ts +++ b/src/modules/waitlist/waitlist.controller.ts @@ -1,47 +1,31 @@ -import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common'; import WaitlistService from './waitlist.service'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { CreateWaitlistDto } from './dto/create-waitlist.dto'; import { WaitlistResponseDto } from './dto/create-waitlist-response.dto'; import { GetWaitlistResponseDto } from './dto/get-waitlist.dto'; import { ErrorResponseDto } from './dto/waitlist-error-response.dto'; +import { skipAuth } from '../../helpers/skipAuth'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { createWaitlistDocs, getAllWaitlistDocs } from './docs/waitlist-swagger.docs'; -@ApiBearerAuth() @ApiTags('Waitlist') @Controller('waitlist') export class WaitlistController { constructor(private readonly waitlistService: WaitlistService) {} @Post() + @skipAuth() + @createWaitlistDocs() @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: 'Create a new waitlist entry' }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: 'Successfully created a waitlist entry.', - type: WaitlistResponseDto, - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Invalid input data.', - type: ErrorResponseDto, - }) async createWaitlist(@Body() createWaitlistDto: CreateWaitlistDto): Promise { return await this.waitlistService.createWaitlist(createWaitlistDto); } - @ApiOperation({ summary: 'Get all waitlist entries' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Successfully retrieved all waitlist entries.', - type: GetWaitlistResponseDto, - }) - @ApiResponse({ - status: HttpStatus.INTERNAL_SERVER_ERROR, - description: 'Internal server error.', - type: ErrorResponseDto, - }) @Get() - getAllWaitlist(): Promise { + @UseGuards(SuperAdminGuard) + @getAllWaitlistDocs() + async getAllWaitlist(): Promise { return this.waitlistService.getAllWaitlist(); } } diff --git a/src/modules/waitlist/waitlist.module.ts b/src/modules/waitlist/waitlist.module.ts index 25f5d4bda..dca9a3a23 100644 --- a/src/modules/waitlist/waitlist.module.ts +++ b/src/modules/waitlist/waitlist.module.ts @@ -3,10 +3,14 @@ import { WaitlistController } from './waitlist.controller'; import WaitlistService from './waitlist.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Waitlist } from './entities/waitlist.entity'; +import { User } from '../user/entities/user.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ controllers: [WaitlistController], providers: [WaitlistService], - imports: [TypeOrmModule.forFeature([Waitlist])], + imports: [TypeOrmModule.forFeature([Waitlist, User, OrganisationUserRole, Organisation, Role])], }) export class WaitlistModule {} diff --git a/src/modules/waitlist/waitlist.service.ts b/src/modules/waitlist/waitlist.service.ts index 0efaff3ff..eb59182c3 100644 --- a/src/modules/waitlist/waitlist.service.ts +++ b/src/modules/waitlist/waitlist.service.ts @@ -5,7 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { MailerService } from '@nestjs-modules/mailer'; import { CreateWaitlistDto } from './dto/create-waitlist.dto'; import { WaitlistResponseDto } from './dto/create-waitlist-response.dto'; -import { validate } from 'class-validator'; +import * as SYS_MSG from '../../helpers/SystemMessages'; import { CustomHttpException } from '../../helpers/custom-http-filter'; @Injectable() @@ -16,59 +16,26 @@ export default class WaitlistService { ) {} async createWaitlist(createWaitlistDto: CreateWaitlistDto): Promise { - const errors = await validate(createWaitlistDto); - if (errors.length > 0) { - const messages = errors.map(err => Object.values(err.constraints)).flat(); - throw new CustomHttpException( - { - status_code: HttpStatus.BAD_REQUEST, - message: messages, - error: 'Bad Request', - }, - HttpStatus.BAD_REQUEST - ); - } + const { full_name: name, email } = createWaitlistDto; - const { full_name, email } = createWaitlistDto; - - const url_slug = full_name - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); - - const waitlist = this.waitlistRepository.create({ - name: full_name, - email, - status: false, - url_slug, - }); + const alreadyWaitlisted = await this.waitlistRepository.findOne({ where: { email } }); + if (alreadyWaitlisted) throw new CustomHttpException(SYS_MSG.USER_ALREADY_WAITLISTED, HttpStatus.CONFLICT); + const waitlist = this.waitlistRepository.create({ name, email }); await this.waitlistRepository.save(waitlist); - const template = `

Hello {{recipientName}},

Thank you for signing up for our waitlist! We will notify you once you are selected.

`; - - const personalizedContent = template.replace('{{recipientName}}', full_name); - await this.mailerService.sendMail({ to: email, subject: 'Waitlist Confirmation', - html: personalizedContent, + template: 'waitlist-confirmation', + context: { recipientName: name }, }); - return { - message: 'You are all signed up!', - }; + return { message: 'You are all signed up!' }; } async getAllWaitlist() { const waitlist = await this.waitlistRepository.find(); - return { - status_code: HttpStatus.OK, - status: HttpStatus.OK, - message: 'Added to waitlist', - data: { - waitlist, - }, - }; + return { message: 'Waitlist found successfully', data: { waitlist } }; } } From a452e8e879dd24f8b60994f1516afae079c7c316 Mon Sep 17 00:00:00 2001 From: olamstevy Date: Sat, 24 Aug 2024 12:31:49 +0100 Subject: [PATCH 50/68] fix: added template for email confirmation --- .../hng-templates/waitlist-confirmatoin.hbs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/modules/email/hng-templates/waitlist-confirmatoin.hbs diff --git a/src/modules/email/hng-templates/waitlist-confirmatoin.hbs b/src/modules/email/hng-templates/waitlist-confirmatoin.hbs new file mode 100644 index 000000000..678aad54e --- /dev/null +++ b/src/modules/email/hng-templates/waitlist-confirmatoin.hbs @@ -0,0 +1,22 @@ + + + + + + Waitlist Confirmation + + + + +
+

Waitlist Confirmation

+

Hello {{recipientName}},

+

Thank you for signing up for our waitlist! We will notify you once you are selected.

+
+ + + \ No newline at end of file From 3f99b284b0314607d4651a73d80cb5d2e6bd89c4 Mon Sep 17 00:00:00 2001 From: olamstevy Date: Sat, 24 Aug 2024 12:35:23 +0100 Subject: [PATCH 51/68] fix: fixed template not found error --- .../{waitlist-confirmatoin.hbs => waitlist-confirmation.hbs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/modules/email/hng-templates/{waitlist-confirmatoin.hbs => waitlist-confirmation.hbs} (100%) diff --git a/src/modules/email/hng-templates/waitlist-confirmatoin.hbs b/src/modules/email/hng-templates/waitlist-confirmation.hbs similarity index 100% rename from src/modules/email/hng-templates/waitlist-confirmatoin.hbs rename to src/modules/email/hng-templates/waitlist-confirmation.hbs From cbea745b940974d1e8e9b5d5aea996aac1004fdb Mon Sep 17 00:00:00 2001 From: imperialis imperial Date: Sat, 24 Aug 2024 12:41:24 +0000 Subject: [PATCH 52/68] refactor: remove try catch blocks --- .../billing-plans/billing-plan.service.ts | 73 +++++-------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index 0b73c60ee..bdf812244 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -36,64 +36,31 @@ export class BillingPlanService { } async getAllBillingPlans() { - try { - const allPlans = await this.billingPlanRepository.find(); - - if (allPlans.length === 0) { - throw new NotFoundException('No billing plans found'); - } - - const plans = allPlans.map(plan => BillingPlanMapper.mapToResponseFormat(plan)); - - return { - message: 'Billing plans retrieved successfully', - data: plans, - }; - } catch (error) { - if (error instanceof NotFoundException || error instanceof BadRequestException) { - throw error; - } - - throw new HttpException( - { - message: `Internal server error: ${error.message}`, - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + const allPlans = await this.billingPlanRepository.find(); + if (allPlans.length === 0) { + throw new NotFoundException('No billing plans found'); } + const plans = allPlans.map(plan => BillingPlanMapper.mapToResponseFormat(plan)); + + return { + message: 'Billing plans retrieved successfully', + data: plans, + }; } async getSingleBillingPlan(id: string) { - try { - if (!id) { - throw new BadRequestException('Invalid billing plan ID'); - } - - const billingPlan = await this.billingPlanRepository.findOneBy({ id }); - - if (!billingPlan) { - throw new NotFoundException('Billing plan not found'); - } - - const plan = BillingPlanMapper.mapToResponseFormat(billingPlan); - - return { - message: 'Billing plan retrieved successfully', - data: plan, - }; - } catch (error) { - if (error instanceof NotFoundException || error instanceof BadRequestException) { - throw error; - } + if (!id) { + throw new BadRequestException('Invalid billing plan ID'); + } + const billingPlan = await this.billingPlanRepository.findOneBy({ id }); - throw new HttpException( - { - message: `Internal server error: ${error.message}`, - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + if (!billingPlan) { + throw new NotFoundException('Billing plan not found'); } + const plan = BillingPlanMapper.mapToResponseFormat(billingPlan); + return { + message: 'Billing plan retrieved successfully', + data: plan, + }; } } From 7bf5b466664a437e9ca46c7c6b8dac753f04e2d0 Mon Sep 17 00:00:00 2001 From: Alphacodez <35844366+Homoakin619@users.noreply.github.com> Date: Sat, 24 Aug 2024 13:43:19 +0100 Subject: [PATCH 53/68] Bootstrapped automated test workflow --- .github/workflows/test-cron.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/test-cron.yaml diff --git a/.github/workflows/test-cron.yaml b/.github/workflows/test-cron.yaml new file mode 100644 index 000000000..e946151db --- /dev/null +++ b/.github/workflows/test-cron.yaml @@ -0,0 +1,16 @@ +name: Scheduled Test + +on: + schedule: + - cron: '*/15 * * * *' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Run a script + run: echo "hello" From 3c2cda71c97bd1f4a141eaec51daa55db8881179 Mon Sep 17 00:00:00 2001 From: Alphacodez <35844366+Homoakin619@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:36:48 +0100 Subject: [PATCH 54/68] Update cron workflow --- .github/workflows/test-cron.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cron.yaml b/.github/workflows/test-cron.yaml index e946151db..e59cfcd4c 100644 --- a/.github/workflows/test-cron.yaml +++ b/.github/workflows/test-cron.yaml @@ -2,7 +2,7 @@ name: Scheduled Test on: schedule: - - cron: '*/15 * * * *' + - cron: '* * * * *' jobs: build: From 3606cb906bb888858489ea245813d1b278d83f5b Mon Sep 17 00:00:00 2001 From: Kaleab Endrias Date: Sat, 24 Aug 2024 11:32:18 +0300 Subject: [PATCH 55/68] fix: return 204 on delete, remove try/catch, protect create/update/delete with SuperAdminGuard --- .../help-center/help-center.controller.ts | 84 ++----------------- .../help-center/help-center.service.ts | 8 ++ 2 files changed, 13 insertions(+), 79 deletions(-) diff --git a/src/modules/help-center/help-center.controller.ts b/src/modules/help-center/help-center.controller.ts index e36c367ef..aae230bce 100644 --- a/src/modules/help-center/help-center.controller.ts +++ b/src/modules/help-center/help-center.controller.ts @@ -12,6 +12,7 @@ import { Query, UseGuards, Req, + HttpCode, } from '@nestjs/common'; import { HelpCenterService } from './help-center.service'; import { UpdateHelpCenterDto } from './dto/update-help-center.dto'; @@ -68,10 +69,7 @@ export class HelpCenterController { @Get('topics/:id') @GetByIdHelpCenterDocs() async findOne(@Param() params: GetHelpCenterDto): Promise { - const helpCenter = await this.helpCenterService.findOne(params.id); - if (!helpCenter) { - throw new CustomHttpException(SYS_MSG.TOPIC_NOT_FOUND, HttpStatus.NOT_FOUND); - } + const helpCenter = await this.helpCenterService.findHelpCenter(params.id); return helpCenter; } @@ -86,86 +84,14 @@ export class HelpCenterController { @Patch('topics/:id') @UpdateHelpCenterDocs() async update(@Param('id') id: string, @Body() updateHelpCenterDto: UpdateHelpCenterDto) { - try { - const updatedHelpCenter = await this.helpCenterService.updateTopic(id, updateHelpCenterDto); - return { - success: true, - message: SYS_MSG.TOPIC_UPDATE_SUCCESS, - data: updatedHelpCenter, - status_code: HttpStatus.OK, - }; - } catch (error) { - if (error.status === HttpStatus.UNAUTHORIZED) { - throw new HttpException( - { - success: false, - message: SYS_MSG.UNAUTHENTICATED_MESSAGE, - status_code: HttpStatus.UNAUTHORIZED, - }, - HttpStatus.UNAUTHORIZED - ); - } else if (error.status === HttpStatus.FORBIDDEN) { - throw new HttpException( - { - success: false, - message: SYS_MSG.FORBIDDEN_ACTION, - status_code: HttpStatus.FORBIDDEN, - }, - HttpStatus.FORBIDDEN - ); - } else if (error.status === HttpStatus.NOT_FOUND) { - throw new HttpException( - { - success: false, - message: SYS_MSG.TOPIC_NOT_FOUND, - status_code: HttpStatus.NOT_FOUND, - }, - HttpStatus.NOT_FOUND - ); - } - } + const updatedHelpCenter = await this.helpCenterService.updateTopic(id, updateHelpCenterDto); + return updatedHelpCenter; } @ApiBearerAuth() @Delete('topics/:id') @DeleteHelpCenterDocs() async remove(@Param('id') id: string) { - try { - await this.helpCenterService.removeTopic(id); - return { - success: true, - message: SYS_MSG.TOPIC_DELETED, - status_code: HttpStatus.OK, - }; - } catch (error) { - if (error.status === HttpStatus.UNAUTHORIZED) { - throw new HttpException( - { - success: false, - message: SYS_MSG.UNAUTHENTICATED_MESSAGE, - status_code: HttpStatus.UNAUTHORIZED, - }, - HttpStatus.UNAUTHORIZED - ); - } else if (error.status === HttpStatus.FORBIDDEN) { - throw new HttpException( - { - success: false, - message: SYS_MSG.FORBIDDEN_ACTION, - status_code: HttpStatus.FORBIDDEN, - }, - HttpStatus.FORBIDDEN - ); - } else if (error.status === HttpStatus.NOT_FOUND) { - throw new HttpException( - { - success: false, - message: SYS_MSG.TOPIC_NOT_FOUND, - status_code: HttpStatus.NOT_FOUND, - }, - HttpStatus.NOT_FOUND - ); - } - } + return await this.helpCenterService.removeTopic(id); } } diff --git a/src/modules/help-center/help-center.service.ts b/src/modules/help-center/help-center.service.ts index dcf929658..bd6ba6690 100644 --- a/src/modules/help-center/help-center.service.ts +++ b/src/modules/help-center/help-center.service.ts @@ -90,6 +90,14 @@ export class HelpCenterService { }; } + async findHelpCenter(id: string) { + const helpCenter = await this.findOne(id); + if (!helpCenter) { + throw new NotFoundException(`Help center topic with ID ${id} not found`); + } + return helpCenter; + } + async search(criteria: SearchHelpCenterDto) { const queryBuilder = this.helpCenterRepository.createQueryBuilder('help_center'); if (criteria.title) { From d96bf91feff73b99dd4d1eb4e5246eac67064d44 Mon Sep 17 00:00:00 2001 From: Kaleab Endrias Date: Sat, 24 Aug 2024 16:40:11 +0300 Subject: [PATCH 56/68] fix: make it use custom exception --- src/modules/help-center/help-center.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/help-center/help-center.service.ts b/src/modules/help-center/help-center.service.ts index bd6ba6690..fa49edb94 100644 --- a/src/modules/help-center/help-center.service.ts +++ b/src/modules/help-center/help-center.service.ts @@ -93,7 +93,7 @@ export class HelpCenterService { async findHelpCenter(id: string) { const helpCenter = await this.findOne(id); if (!helpCenter) { - throw new NotFoundException(`Help center topic with ID ${id} not found`); + throw new CustomHttpException(`Help center topic with ID ${id} not found`, HttpStatus.NOT_FOUND); } return helpCenter; } From 4ae5238a3103272f6b2172c2cdc2e3e16c251361 Mon Sep 17 00:00:00 2001 From: Kaleab Endrias Date: Sat, 24 Aug 2024 17:08:24 +0300 Subject: [PATCH 57/68] fix: billing docs --- .../billing-plans/billing-plan.controller.ts | 16 +++------- .../billing-plans/docs/billing-plan-docs.ts | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 src/modules/billing-plans/docs/billing-plan-docs.ts diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 417ec0ff0..03e4a61fb 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -4,6 +4,7 @@ import { SuperAdminGuard } from '../../guards/super-admin.guard'; import { BillingPlanService } from './billing-plan.service'; import { skipAuth } from '../../helpers/skipAuth'; import { BillingPlanDto } from './dto/billing-plan.dto'; +import { createBillingPlanDocs, getAllBillingPlansDocs, getSingleBillingPlan } from './docs/billing-plan-docs'; @ApiTags('Billing Plans') @Controller('billing-plans') @@ -11,31 +12,22 @@ export class BillingPlanController { constructor(private readonly billingPlanService: BillingPlanService) {} @Post('/') + @createBillingPlanDocs() @UseGuards(SuperAdminGuard) - @ApiBearerAuth() - @ApiOperation({ summary: 'Create billing plans' }) - @ApiBody({ type: BillingPlanDto }) - @ApiResponse({ status: 201, description: 'Billing plan created successfully.', type: BillingPlanDto }) - @ApiResponse({ status: 200, description: 'Billing plan already exists in the database.', type: [BillingPlanDto] }) async createBillingPlan(@Body() createBillingPlanDto: BillingPlanDto) { return this.billingPlanService.createBillingPlan(createBillingPlanDto); } @skipAuth() + @getAllBillingPlansDocs() @Get('/') - @ApiOperation({ summary: 'Get all billing plans' }) - @ApiResponse({ status: 200, description: 'Billing plans retrieved successfully.', type: [BillingPlanDto] }) - @ApiResponse({ status: 404, description: 'No billing plans found.' }) async getAllBillingPlans() { return this.billingPlanService.getAllBillingPlans(); } @skipAuth() + @getSingleBillingPlan() @Get('/:id') - @ApiOperation({ summary: 'Get single billing plan by ID' }) - @ApiResponse({ status: 200, description: 'Billing plan retrieved successfully', type: BillingPlanDto }) - @ApiResponse({ status: 400, description: 'Invalid billing plan ID' }) - @ApiResponse({ status: 404, description: 'Billing plan not found' }) async getSingleBillingPlan(@Param('id') id: string) { return this.billingPlanService.getSingleBillingPlan(id); } diff --git a/src/modules/billing-plans/docs/billing-plan-docs.ts b/src/modules/billing-plans/docs/billing-plan-docs.ts new file mode 100644 index 000000000..fe2bb3f44 --- /dev/null +++ b/src/modules/billing-plans/docs/billing-plan-docs.ts @@ -0,0 +1,30 @@ +import { applyDecorators } from '@nestjs/common'; +import { BillingPlanDto } from '../dto/billing-plan.dto'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; + +export function createBillingPlanDocs() { + return applyDecorators( + ApiBearerAuth(), + ApiOperation({ summary: 'Create billing plans' }), + ApiBody({ type: BillingPlanDto }), + ApiResponse({ status: 201, description: 'Billing plan created successfully.', type: BillingPlanDto }), + ApiResponse({ status: 200, description: 'Billing plan already exists in the database.', type: [BillingPlanDto] }) + ); +} + +export function getAllBillingPlansDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get all billing plans' }), + ApiResponse({ status: 200, description: 'Billing plans retrieved successfully.', type: [BillingPlanDto] }), + ApiResponse({ status: 404, description: 'No billing plans found.' }) + ); +} + +export function getSingleBillingPlan() { + return applyDecorators( + ApiOperation({ summary: 'Get single billing plan by ID' }), + ApiResponse({ status: 200, description: 'Billing plan retrieved successfully', type: BillingPlanDto }), + ApiResponse({ status: 400, description: 'Invalid billing plan ID' }), + ApiResponse({ status: 404, description: 'Billing plan not found' }) + ); +} From b0318a3ea09e4bd88c8b7c3d6da2df7e85b015ea Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 15:13:28 +0100 Subject: [PATCH 58/68] feat: updated qa automated workflow --- .github/workflows/scheduled-test.yaml | 19 +++++++ .github/workflows/test-cron.yaml | 16 ------ qa/index.js | 80 +++++++++++++++++++++++++++ qa/test.sh | 3 + 4 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/scheduled-test.yaml delete mode 100644 .github/workflows/test-cron.yaml create mode 100644 qa/index.js create mode 100644 qa/test.sh diff --git a/.github/workflows/scheduled-test.yaml b/.github/workflows/scheduled-test.yaml new file mode 100644 index 000000000..f2dd6b550 --- /dev/null +++ b/.github/workflows/scheduled-test.yaml @@ -0,0 +1,19 @@ +name: Scheduled Test + +on: + schedule: + - cron: '* * * * *' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Run a script + run: | + cd qa + chmod +x test.sh + ./test.sh diff --git a/.github/workflows/test-cron.yaml b/.github/workflows/test-cron.yaml deleted file mode 100644 index e59cfcd4c..000000000 --- a/.github/workflows/test-cron.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Scheduled Test - -on: - schedule: - - cron: '* * * * *' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Run a script - run: echo "hello" diff --git a/qa/index.js b/qa/index.js new file mode 100644 index 000000000..187ea2ffb --- /dev/null +++ b/qa/index.js @@ -0,0 +1,80 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +const resultFilePath = path.resolve(__dirname, 'result.json'); + +const postmanApiKey = process.env.POSTMAN_API_KEY; + +const runTasks = async () => { + try { + console.log('Generating test reports...'); + + // Spawn a new process for running Newman tests + const newmanProcess = spawn('npx', [ + 'newman', + 'run', + `https://api.getpostman.com/collections/37678338-b29374aa-a7b1-43e9-bdc8-fc3bcf39871b?apikey=${postmanApiKey}`, + '-e', + `https://api.getpostman.com/environments/37678787-5f6cbeff-c9d9-44c3-b670-7887cf48fc12?apikey=${postmanApiKey}`, + '--reporters', + 'cli,json', + '--reporter-json-export', + resultFilePath, + ]); + + // Handle stdout + newmanProcess.stdout.on('data', data => { + console.log(`stdout: ${data}`); + }); + + // Handle stderr + newmanProcess.stderr.on('data', data => { + console.error(`stderr: ${data}`); + }); + + // Handle process close + newmanProcess.on('close', code => { + if (code !== 0) { + console.warn(`Newman process exited with code ${code}. There may be test failures.`); + } + console.log('Finished running tests and generating reports'); + + // Spawn a new process for compressing the report and making API requests + console.log('Running test result compression and API request...'); + + const compressProcess = spawn('node', ['compress_send.js']); + + compressProcess.stdout.on('data', data => { + console.log(`stdout: ${data}`); + }); + + compressProcess.stderr.on('data', data => { + console.error(`stderr: ${data}`); + }); + + compressProcess.on('close', code => { + if (code !== 0) { + console.error(`Compression process exited with code ${code}`); + return; + } + console.log('Finished compressing test results and making API requests'); + + // Remove the result.json file after successful compression + fs.unlink(resultFilePath, err => { + if (err) { + console.error(`Error removing result.json: ${err.message}`); + return; + } + console.log('Successfully removed result.json'); + }); + }); + }); + } catch (error) { + console.error(`Error executing commands: ${error.message}`); + console.log(error.stack); + } +}; + +runTasks(); diff --git a/qa/test.sh b/qa/test.sh new file mode 100644 index 000000000..79161b2e9 --- /dev/null +++ b/qa/test.sh @@ -0,0 +1,3 @@ +npm install dot-env +npm install newman +node ./index.js From e0ad8914f5988578098572c8d54bcc80a694eded Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 15:30:54 +0100 Subject: [PATCH 59/68] feat: added environment variable to workflow --- .github/workflows/scheduled-test.yaml | 2 ++ qa/test.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scheduled-test.yaml b/.github/workflows/scheduled-test.yaml index f2dd6b550..c08ac6df4 100644 --- a/.github/workflows/scheduled-test.yaml +++ b/.github/workflows/scheduled-test.yaml @@ -13,6 +13,8 @@ jobs: uses: actions/checkout@v3 - name: Run a script + env: + POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} run: | cd qa chmod +x test.sh diff --git a/qa/test.sh b/qa/test.sh index 79161b2e9..91f551d3e 100644 --- a/qa/test.sh +++ b/qa/test.sh @@ -1,3 +1,3 @@ -npm install dot-env +npm install dotenv npm install newman node ./index.js From 51e9ecb2db34c27f61b883e1b8ff673aab8f9bef Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 15:37:34 +0100 Subject: [PATCH 60/68] feat: added compress_send.js --- .github/workflows/scheduled-test.yaml | 3 +- qa/compress_send.js | 204 ++++++++++++++++++++++++++ qa/test.sh | 2 + 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 qa/compress_send.js diff --git a/.github/workflows/scheduled-test.yaml b/.github/workflows/scheduled-test.yaml index c08ac6df4..a22baf528 100644 --- a/.github/workflows/scheduled-test.yaml +++ b/.github/workflows/scheduled-test.yaml @@ -12,9 +12,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Run a script + - name: Run a test script env: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} + API_URL: ${{ secrets.API_URL }} run: | cd qa chmod +x test.sh diff --git a/qa/compress_send.js b/qa/compress_send.js new file mode 100644 index 000000000..47bd2fa15 --- /dev/null +++ b/qa/compress_send.js @@ -0,0 +1,204 @@ +const fs = require('fs'); +const path = require('path'); +const json = require('big-json'); +const axios = require('axios'); + +const { pipeline } = require('stream'); + +const readStream = fs.createReadStream('./result.json'); +const parseStream = json.createParseStream(); + +parseStream.on('data', async function (result) { + const tests = () => { + const Authentication = []; + const Settings = []; + const Avatars = []; + const Blogs = []; + const Rooms = []; + const FAQs = []; + const ContactUs = []; + const Default = []; + const Payment = []; + const Game = []; + const AboutUs = []; + + result.collection.item?.map(cat => { + const category = cat.name; + const subCategories = []; + if (cat.name === 'Authentication' || cat.name === 'Settings') { + cat.item.forEach(element => { + element.item.forEach(el => { + subCategories.push(el.name); + }); + }); + } else { + cat.item.forEach(element => { + subCategories.push(element.name); + }); + } + + testPassesSummary.forEach(test => { + if (subCategories.includes(test.name)) { + switch (category) { + case 'Authentication': + Authentication.push(test); + break; + case 'Settings': + Settings.push(test); + break; + case 'Avatars': + Avatars.push(test); + break; + case 'Blogs': + Blogs.push(test); + break; + case 'Rooms': + Rooms.push(test); + break; + case 'FAQs': + FAQs.push(test); + break; + case 'Contact Us': + ContactUs.push(test); + break; + case 'Default': + Default.push(test); + break; + case 'Payment': + Payment.push(test); + break; + case 'Game': + Game.push(test); + break; + case 'About Us': + AboutUs.push(test); + break; + default: + break; + } + } + }); + }); + return { + AboutUs, + Authentication, + Avatars, + Blogs, + ContactUs, + Default, + FAQs, + Game, + Payment, + Rooms, + Settings, + }; + }; + const testPassesSummary = result.run.executions.map(execution => ({ + name: execution.item.name, + assertions: execution.assertions?.map(assertion => ({ + assertion: assertion.assertion, + error: assertion.error ? assertion.error.message : null, + })), + requestUrl: execution.request.url.path.join('/'), + host: execution.request.url.host.join('.'), + responseTime: execution.response.responseTime, + statusCode: execution.response.code, + status: execution.response.status, + })); + + // Create the summary object + const summary = { + last_checked: new Date(), + stats: result.run.stats, + tests: tests(), + }; + const parsedData = parseNewmanResults(summary); + await sendApiStatus(parsedData); +}); + +pipeline(readStream, parseStream, err => { + if (err) { + console.error('Pipeline failed.', err); + } else { + console.log('Pipeline succeeded.'); + } +}); + +const parseNewmanResults = summary => { + const endpoints = [ + 'Authentication', + 'Settings', + 'Avatars', + 'Blogs', + 'Rooms', + 'FAQs', + 'ContactUs', + 'Default', + 'Payment', + 'Game', + 'AboutUs', + ]; + + const finalData = []; + let assertions = 0; + endpoints.forEach(name => { + let responseTimeSum = 0; + let count = 0; + const api_group = `${name} API`; + let status = 'operational'; + let requests = []; + + if (summary.tests[name]) { + summary.tests[name].forEach(request => { + const eachRequest = { + requestName: request.name, + requestUrl: request.requestUrl, + statusCode: request.statusCode, + status: request.status, + responseTime: request.responseTime, + errors: [], + }; + count++; + responseTimeSum += request.responseTime; + request.assertions?.forEach(assertion => { + assertions++; + if (assertion.error) { + status = 'down'; + eachRequest.errors.push(assertion.error); + } + }); + requests.push(eachRequest); + }); + const details = status === 'down' ? 'Some tests failed' : 'All tests passed'; + finalData.push({ + api_group, + status, + details, + requests, + }); + } else { + finalData.push({ + api_group, + status: 'No tests available', + details: 'No data', + requests, + }); + } + }); + return finalData; +}; + +async function sendApiStatus(apiStatusData) { + try { + const response = await axios.post(`${process.env.API_URL}api-status`, apiStatusData); + console.log(response?.data); + if (response.status === 201) { + console.log(`Successfully sent data `); + } else { + console.error(`Failed to send data `); + } + } catch (error) { + console.error(`Error sending data:`, error.message); + console.log(error); + } +} diff --git a/qa/test.sh b/qa/test.sh index 91f551d3e..f9176307e 100644 --- a/qa/test.sh +++ b/qa/test.sh @@ -1,3 +1,5 @@ npm install dotenv npm install newman +npm install axios +npm install big-json node ./index.js From 81ef50361406dee0c104e85fc862173ea28a6ed5 Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 16:10:22 +0100 Subject: [PATCH 61/68] feat: updated api collection --- .github/workflows/scheduled-test.yaml | 2 +- qa/index.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/scheduled-test.yaml b/.github/workflows/scheduled-test.yaml index a22baf528..9a042984a 100644 --- a/.github/workflows/scheduled-test.yaml +++ b/.github/workflows/scheduled-test.yaml @@ -2,7 +2,7 @@ name: Scheduled Test on: schedule: - - cron: '* * * * *' + - cron: '*/15 * * * *' jobs: build: diff --git a/qa/index.js b/qa/index.js index 187ea2ffb..0dc3641bf 100644 --- a/qa/index.js +++ b/qa/index.js @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); require('dotenv').config(); -const resultFilePath = path.resolve(__dirname, 'result.json'); +const resultFilePath = path.resolve(__dirname, 'boilerplate_report.json'); const postmanApiKey = process.env.POSTMAN_API_KEY; @@ -15,9 +15,9 @@ const runTasks = async () => { const newmanProcess = spawn('npx', [ 'newman', 'run', - `https://api.getpostman.com/collections/37678338-b29374aa-a7b1-43e9-bdc8-fc3bcf39871b?apikey=${postmanApiKey}`, + `https://api.getpostman.com/collections/37678338-3145218a-98a5-49f6-87be-18ec1ca6e0db?apikey=${postmanApiKey}`, '-e', - `https://api.getpostman.com/environments/37678787-5f6cbeff-c9d9-44c3-b670-7887cf48fc12?apikey=${postmanApiKey}`, + `https://api.getpostman.com/environments/37678338-5b07d664-877b-40d0-ae8f-fddc791af401?apikey=${postmanApiKey}`, '--reporters', 'cli,json', '--reporter-json-export', From a4a0be4ac8643e654357d82e2ed41fbb32871704 Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Sat, 24 Aug 2024 16:12:01 +0100 Subject: [PATCH 62/68] feat: updated workflow --- .github/workflows/scheduled-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scheduled-test.yaml b/.github/workflows/scheduled-test.yaml index 9a042984a..4b4df439a 100644 --- a/.github/workflows/scheduled-test.yaml +++ b/.github/workflows/scheduled-test.yaml @@ -12,7 +12,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Run a test script + - name: Run test script env: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} API_URL: ${{ secrets.API_URL }} From 50e9d2272169b83023bee7560e376bc4f4866b5a Mon Sep 17 00:00:00 2001 From: Kaleab Endrias Date: Sat, 24 Aug 2024 18:27:34 +0300 Subject: [PATCH 63/68] feat: added update and delete enpoints --- src/helpers/SystemMessages.ts | 1 + .../billing-plans/billing-plan.controller.ts | 43 +++++++++++++++++-- .../billing-plans/billing-plan.service.ts | 19 ++++++++ .../billing-plans/docs/billing-plan-docs.ts | 17 ++++++++ .../dto/update-billing-plan.dto.ts | 4 ++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/modules/billing-plans/dto/update-billing-plan.dto.ts diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index bf7475b00..791db6133 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -109,3 +109,4 @@ export const BILLING_PLAN_CREATED = 'Billing plan successfully created'; export const TOPIC_NOT_FOUND = `Help center topic with ID not found`; export const TOPIC_UPDATE_SUCCESS = 'Topic updated successfully'; export const TOPIC_DELETED = 'Topic deleted successfully'; +export const BILLING_PLAN_NOT_FOUND = 'Billing plan not found'; diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 03e4a61fb..2d619d22a 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -1,10 +1,29 @@ -import { Controller, Get, Param, Post, Body, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags, ApiBody } from '@nestjs/swagger'; +import { + Controller, + Get, + Param, + Post, + Body, + UseGuards, + Patch, + ParseUUIDPipe, + Delete, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { SuperAdminGuard } from '../../guards/super-admin.guard'; import { BillingPlanService } from './billing-plan.service'; import { skipAuth } from '../../helpers/skipAuth'; import { BillingPlanDto } from './dto/billing-plan.dto'; -import { createBillingPlanDocs, getAllBillingPlansDocs, getSingleBillingPlan } from './docs/billing-plan-docs'; +import { + createBillingPlanDocs, + deleteBillingPlan, + getAllBillingPlansDocs, + getSingleBillingPlan, + updateBillingPlan, +} from './docs/billing-plan-docs'; +import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto'; @ApiTags('Billing Plans') @Controller('billing-plans') @@ -31,4 +50,22 @@ export class BillingPlanController { async getSingleBillingPlan(@Param('id') id: string) { return this.billingPlanService.getSingleBillingPlan(id); } + + @UseGuards(SuperAdminGuard) + @updateBillingPlan() + @Patch('/:id') + async updateBillingPlan( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() updateBillingPlanDto: UpdateBillingPlanDto + ) { + return this.billingPlanService.updateBillingPlan(id, updateBillingPlanDto); + } + + @UseGuards(SuperAdminGuard) + @deleteBillingPlan() + @Delete('/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async deleteBillingPlan(@Param('id', ParseUUIDPipe) id: string) { + return this.billingPlanService.deleteBillingPlan(id); + } } diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index bdf812244..096b83509 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -6,6 +6,7 @@ import { BillingPlanDto } from './dto/billing-plan.dto'; import * as SYS_MSG from '../../helpers/SystemMessages'; import { CustomHttpException } from '../../helpers/custom-http-filter'; import { BillingPlanMapper } from './mapper/billing-plan.mapper'; +import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto'; @Injectable() export class BillingPlanService { @@ -63,4 +64,22 @@ export class BillingPlanService { data: plan, }; } + + async updateBillingPlan(id: string, updateBillingPlanDto: UpdateBillingPlanDto): Promise { + const billing_plan = await this.billingPlanRepository.findOneBy({ id }); + console.log(billing_plan); + if (!billing_plan) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_NOT_FOUND, HttpStatus.NOT_FOUND); + } + Object.assign(billing_plan, updateBillingPlanDto); + return await this.billingPlanRepository.save(billing_plan); + } + + async deleteBillingPlan(id: string): Promise { + const billing_plan = await this.billingPlanRepository.findOne({ where: { id: id } }); + if (!billing_plan) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_NOT_FOUND, HttpStatus.NOT_FOUND); + } + await this.billingPlanRepository.delete(id); + } } diff --git a/src/modules/billing-plans/docs/billing-plan-docs.ts b/src/modules/billing-plans/docs/billing-plan-docs.ts index fe2bb3f44..53d1fe191 100644 --- a/src/modules/billing-plans/docs/billing-plan-docs.ts +++ b/src/modules/billing-plans/docs/billing-plan-docs.ts @@ -1,6 +1,7 @@ import { applyDecorators } from '@nestjs/common'; import { BillingPlanDto } from '../dto/billing-plan.dto'; import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BillingPlan } from '../entities/billing-plan.entity'; export function createBillingPlanDocs() { return applyDecorators( @@ -28,3 +29,19 @@ export function getSingleBillingPlan() { ApiResponse({ status: 404, description: 'Billing plan not found' }) ); } + +export function updateBillingPlan() { + return applyDecorators( + ApiOperation({ summary: 'Update a billing plan by ID' }), + ApiResponse({ status: 200, description: 'Billing plan updated successfully.', type: BillingPlan }), + ApiResponse({ status: 404, description: 'Billing plan not found.' }) + ); +} + +export function deleteBillingPlan() { + return applyDecorators( + ApiOperation({ summary: 'Delete a billing plan by ID' }), + ApiResponse({ status: 204, description: 'Billing plan deleted successfully.' }), + ApiResponse({ status: 404, description: 'Billing plan not found.' }) + ); +} diff --git a/src/modules/billing-plans/dto/update-billing-plan.dto.ts b/src/modules/billing-plans/dto/update-billing-plan.dto.ts new file mode 100644 index 000000000..f24653c00 --- /dev/null +++ b/src/modules/billing-plans/dto/update-billing-plan.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { BillingPlanDto } from './billing-plan.dto'; + +export class UpdateBillingPlanDto extends PartialType(BillingPlanDto) {} From 2d30e3fe45b9528b242182a07a4caa701146875f Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 19:34:38 +0300 Subject: [PATCH 64/68] fix: correcting link in forgot password --- src/modules/auth/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index cde8b3732..30b595c43 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -97,7 +97,7 @@ export default class AuthenticationService { } const token = (await this.otpService.createOtp(user.id)).token; - await this.emailService.sendForgotPasswordMail(user.email, `${process.env.BASE_URL}/auth/reset-password`, token); + await this.emailService.sendForgotPasswordMail(user.email, `${process.env.FRONTEND_URL}/forgot-password`, token); return { message: SYS_MSG.EMAIL_SENT, From a014f6b309da732fe61e7b35f4ee777eb3f2d7c0 Mon Sep 17 00:00:00 2001 From: imperialis imperial Date: Sat, 24 Aug 2024 16:47:35 +0000 Subject: [PATCH 65/68] fix(jobs): reformat swagger tags for jobs module --- src/modules/jobs/docs/jobs-swagger.ts | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/modules/jobs/docs/jobs-swagger.ts diff --git a/src/modules/jobs/docs/jobs-swagger.ts b/src/modules/jobs/docs/jobs-swagger.ts new file mode 100644 index 000000000..2c60ac25e --- /dev/null +++ b/src/modules/jobs/docs/jobs-swagger.ts @@ -0,0 +1,79 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiTags, + ApiQuery, + ApiInternalServerErrorResponse, + ApiBearerAuth, + ApiBadRequestResponse, + ApiUnprocessableEntityResponse, + ApiResponse, + ApiCreatedResponse, + ApiBody, + ApiOperation, +} from '@nestjs/swagger'; +import { JobApplicationDto } from '../dto/job-application.dto'; +import { JobApplicationResponseDto } from '../dto/job-application-response.dto'; +import { JobApplicationErrorDto } from '../dto/job-application-error.dto'; + +export function SubmitJobApplicationDocs() { + return applyDecorators( + ApiOperation({ summary: 'Submit job application' }), + ApiBody({ + type: JobApplicationDto, + description: 'Job application request body', + }), + ApiCreatedResponse({ + status: 201, + description: 'Job application submitted successfully', + type: JobApplicationResponseDto, + }), + ApiUnprocessableEntityResponse({ + description: 'Job application deadline passed', + status: 422, + }), + ApiBadRequestResponse({ status: 400, description: 'Invalid request body', type: JobApplicationErrorDto }), + ApiInternalServerErrorResponse({ status: 500, description: 'Internal server error', type: JobApplicationErrorDto }) + ); +} +export function CreateNewJobDocs() { + return applyDecorators( + ApiOperation({ summary: 'Create a new job' }), + ApiResponse({ status: 201, description: 'Job created successfully' }), + ApiResponse({ status: 404, description: 'User not found' }) + ); +} + +export function SearchForJoblistingsDocs() { + return applyDecorators( + ApiOperation({ summary: 'Search for job listings' }), + ApiQuery({ name: 'page', required: false, type: Number }), + ApiQuery({ name: 'limit', required: false, type: Number }), + ApiResponse({ status: 200, description: 'Successful response' }), + ApiResponse({ status: 400, description: 'Bad request' }) + ); +} + +export function GetsAllJobsDocs() { + return applyDecorators( + ApiOperation({ summary: 'Gets all jobs' }), + ApiResponse({ status: 200, description: 'Jobs returned successfully' }), + ApiResponse({ status: 404, description: 'Job not found' }) + ); +} + +export function GetAJobByIDDocs() { + return applyDecorators( + ApiOperation({ summary: 'Gets a job by ID' }), + ApiResponse({ status: 200, description: 'Job returned successfully' }), + ApiResponse({ status: 404, description: 'Job not found' }) + ); +} + +export function DeleteAJobDocs() { + return applyDecorators( + ApiOperation({ summary: 'Delete a job' }), + ApiResponse({ status: 200, description: 'Job deleted successfully' }), + ApiResponse({ status: 403, description: 'You do not have permission to perform this action' }), + ApiResponse({ status: 404, description: 'Job not found' }) + ); +} From 8f200aa526b3c5274899922e2b06d4a610bb2909 Mon Sep 17 00:00:00 2001 From: Michelle Ndiangui Date: Sat, 24 Aug 2024 19:58:21 +0300 Subject: [PATCH 66/68] fix: correcting link --- src/modules/auth/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 30b595c43..4aa4b3b15 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -97,7 +97,7 @@ export default class AuthenticationService { } const token = (await this.otpService.createOtp(user.id)).token; - await this.emailService.sendForgotPasswordMail(user.email, `${process.env.FRONTEND_URL}/forgot-password`, token); + await this.emailService.sendForgotPasswordMail(user.email, `${process.env.FRONTEND_URL}/reset-password`, token); return { message: SYS_MSG.EMAIL_SENT, From 849c96c1856d0fc878daadfa7e3b7d0a6b6a6660 Mon Sep 17 00:00:00 2001 From: Kaleab Endrias Date: Sat, 24 Aug 2024 21:20:01 +0300 Subject: [PATCH 67/68] fix: remove console.log --- src/modules/billing-plans/billing-plan.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index 096b83509..a948535fd 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -67,7 +67,6 @@ export class BillingPlanService { async updateBillingPlan(id: string, updateBillingPlanDto: UpdateBillingPlanDto): Promise { const billing_plan = await this.billingPlanRepository.findOneBy({ id }); - console.log(billing_plan); if (!billing_plan) { throw new CustomHttpException(SYS_MSG.BILLING_PLAN_NOT_FOUND, HttpStatus.NOT_FOUND); } From 3e111a573fa3ba7896f4e0c2e836291fcd5a8655 Mon Sep 17 00:00:00 2001 From: Kaleab Endrias Date: Sat, 24 Aug 2024 21:24:10 +0300 Subject: [PATCH 68/68] fix: rename function name --- src/modules/billing-plans/billing-plan.controller.ts | 12 ++++++------ src/modules/billing-plans/docs/billing-plan-docs.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 2d619d22a..dfde14eea 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -18,10 +18,10 @@ import { skipAuth } from '../../helpers/skipAuth'; import { BillingPlanDto } from './dto/billing-plan.dto'; import { createBillingPlanDocs, - deleteBillingPlan, + deleteBillingPlanDocs, getAllBillingPlansDocs, - getSingleBillingPlan, - updateBillingPlan, + getSingleBillingPlanDocs, + updateBillingPlanDocs, } from './docs/billing-plan-docs'; import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto'; @@ -45,14 +45,14 @@ export class BillingPlanController { } @skipAuth() - @getSingleBillingPlan() + @getSingleBillingPlanDocs() @Get('/:id') async getSingleBillingPlan(@Param('id') id: string) { return this.billingPlanService.getSingleBillingPlan(id); } @UseGuards(SuperAdminGuard) - @updateBillingPlan() + @updateBillingPlanDocs() @Patch('/:id') async updateBillingPlan( @Param('id', new ParseUUIDPipe()) id: string, @@ -62,7 +62,7 @@ export class BillingPlanController { } @UseGuards(SuperAdminGuard) - @deleteBillingPlan() + @deleteBillingPlanDocs() @Delete('/:id') @HttpCode(HttpStatus.NO_CONTENT) async deleteBillingPlan(@Param('id', ParseUUIDPipe) id: string) { diff --git a/src/modules/billing-plans/docs/billing-plan-docs.ts b/src/modules/billing-plans/docs/billing-plan-docs.ts index 53d1fe191..97faf5c4e 100644 --- a/src/modules/billing-plans/docs/billing-plan-docs.ts +++ b/src/modules/billing-plans/docs/billing-plan-docs.ts @@ -21,7 +21,7 @@ export function getAllBillingPlansDocs() { ); } -export function getSingleBillingPlan() { +export function getSingleBillingPlanDocs() { return applyDecorators( ApiOperation({ summary: 'Get single billing plan by ID' }), ApiResponse({ status: 200, description: 'Billing plan retrieved successfully', type: BillingPlanDto }), @@ -30,7 +30,7 @@ export function getSingleBillingPlan() { ); } -export function updateBillingPlan() { +export function updateBillingPlanDocs() { return applyDecorators( ApiOperation({ summary: 'Update a billing plan by ID' }), ApiResponse({ status: 200, description: 'Billing plan updated successfully.', type: BillingPlan }), @@ -38,7 +38,7 @@ export function updateBillingPlan() { ); } -export function deleteBillingPlan() { +export function deleteBillingPlanDocs() { return applyDecorators( ApiOperation({ summary: 'Delete a billing plan by ID' }), ApiResponse({ status: 204, description: 'Billing plan deleted successfully.' }),