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/.github/Unused/pr-deploy.yaml b/.github/Unused/pr-deploy.yaml new file mode 100644 index 000000000..3bf206500 --- /dev/null +++ b/.github/Unused/pr-deploy.yaml @@ -0,0 +1,27 @@ +# 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 }} 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..b1457f620 --- /dev/null +++ b/.github/workflows/main-deployment.yaml @@ -0,0 +1,21 @@ +name: Production Deployment + +on: + push: + branches: + - main + +jobs: + deploy-prod: + runs-on: ubuntu-latest + 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/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml deleted file mode 100644 index 19f6e69ea..000000000 --- a/.github/workflows/pr-deploy.yaml +++ /dev/null @@ -1,27 +0,0 @@ -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 }} 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/scheduled-test.yaml b/.github/workflows/scheduled-test.yaml new file mode 100644 index 000000000..4b4df439a --- /dev/null +++ b/.github/workflows/scheduled-test.yaml @@ -0,0 +1,22 @@ +name: Scheduled Test + +on: + schedule: + - cron: '*/15 * * * *' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Run test script + env: + POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} + API_URL: ${{ secrets.API_URL }} + run: | + cd qa + chmod +x test.sh + ./test.sh diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index 7e7af63ed..9d441dd67 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -1,41 +1,13 @@ name: Staging Deployment on: - workflow_dispatch: push: branches: - staging jobs: - build-and-push: - if: github.event.repository.fork == false + deploy-staging: 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 }} steps: - name: Deploy on server uses: appleboy/ssh-action@master @@ -44,5 +16,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/.vscode/settings.json b/.vscode/settings.json index b347739d5..33d4973e1 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": [ @@ -28,6 +33,6 @@ "source.fixAll": "explicit" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "dbaeumer.vscode-eslint" } } 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/package-lock.json b/package-lock.json index 2814bc273..b91b464ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "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", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.2.0", @@ -49,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", @@ -1114,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" }, @@ -1137,6 +1140,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -1152,6 +1156,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -1167,6 +1172,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -1182,6 +1188,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -1197,6 +1204,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1212,6 +1220,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1227,6 +1236,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1242,6 +1252,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1257,6 +1268,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -1272,6 +1284,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1285,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" @@ -1414,6 +1428,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", @@ -1514,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" @@ -18673,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 089c75965..a55b07bad 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", @@ -73,7 +74,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/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/index.js b/qa/index.js new file mode 100644 index 000000000..0dc3641bf --- /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, 'boilerplate_report.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-3145218a-98a5-49f6-87be-18ec1ca6e0db?apikey=${postmanApiKey}`, + '-e', + `https://api.getpostman.com/environments/37678338-5b07d664-877b-40d0-ae8f-fddc791af401?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..f9176307e --- /dev/null +++ b/qa/test.sh @@ -0,0 +1,5 @@ +npm install dotenv +npm install newman +npm install axios +npm install big-json +node ./index.js diff --git a/src/app.module.ts b/src/app.module.ts index 766e01a92..b134d4bdb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,8 +46,15 @@ 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'; +import { ApiStatusModule } from './modules/api-status/api-status.module'; + @Module({ providers: [ + { + provide: 'APP_GUARD', + useClass: LanguageGuard, + }, { provide: 'CONFIG', useClass: ConfigService, @@ -166,6 +173,7 @@ import { join } from 'path'; index: false, }, }), + ApiStatusModule, ], controllers: [HealthController, ProbeController], }) 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/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 6f79700fa..64b9f74fd 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -98,10 +98,16 @@ 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 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'; +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/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/api-status/api-status.controller.ts b/src/modules/api-status/api-status.controller.ts new file mode 100644 index 000000000..8fca699b0 --- /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 '../../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..e125a4676 --- /dev/null +++ b/src/modules/api-status/api-status.service.ts @@ -0,0 +1,86 @@ +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 savedApiHealth = await this.apiHealthRepository.save(eachApiStatus); + await Promise.all( + eachApiStatus.requests.map(async request => { + const savedRequest = await this.requestRepository.save({ + ...request, + api_health: savedApiHealth, + updated_at: new Date(), + }); + apiRequestList.push(savedRequest); + return savedRequest; + }) + ); + + 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(async request => { + const savedRequest = await this.requestRepository.save(request); + apiRequestList.push(savedRequest); + return savedRequest; + }) + ); + + 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/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 63e474ce3..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.BASE_URL}/auth/reset-password`, token); + await this.emailService.sendForgotPasswordMail(user.email, `${process.env.FRONTEND_URL}/reset-password`, token); return { message: SYS_MSG.EMAIL_SENT, @@ -285,14 +285,9 @@ export default class AuthenticationService { async googleAuth({ googleAuthPayload, isMobile }: { googleAuthPayload: GoogleAuthPayload; isMobile: string }) { 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}`); + 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 () => { diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 133a2b16b..dfde14eea 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -1,8 +1,29 @@ -import { Controller, Get, Param, Post } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } 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, + deleteBillingPlanDocs, + getAllBillingPlansDocs, + getSingleBillingPlanDocs, + updateBillingPlanDocs, +} from './docs/billing-plan-docs'; +import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto'; @ApiTags('Billing Plans') @Controller('billing-plans') @@ -10,29 +31,41 @@ export class BillingPlanController { constructor(private readonly billingPlanService: BillingPlanService) {} @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(); + @createBillingPlanDocs() + @UseGuards(SuperAdminGuard) + 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() + @getSingleBillingPlanDocs() @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); } + + @UseGuards(SuperAdminGuard) + @updateBillingPlanDocs() + @Patch('/:id') + async updateBillingPlan( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() updateBillingPlanDto: UpdateBillingPlanDto + ) { + return this.billingPlanService.updateBillingPlan(id, updateBillingPlanDto); + } + + @UseGuards(SuperAdminGuard) + @deleteBillingPlanDocs() + @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.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/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index c0d7fbc53..a948535fd 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -2,6 +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 { 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 { @@ -10,119 +15,70 @@ 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 })); + async createBillingPlan(createBillingPlanDto: BillingPlanDto) { + const billingPlan = await this.billingPlanRepository.findOne({ + where: { + name: createBillingPlanDto.name, + }, + }); - return { - status_code: HttpStatus.OK, - message: 'Billing plans already exist in the database', - data: plans, - }; - } + if (billingPlan) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); + } - 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 })); + const newPlan = this.billingPlanRepository.create(createBillingPlanDto); + const createdPlan = await this.billingPlanRepository.save(newPlan); + const plan = BillingPlanMapper.mapToResponseFormat(createdPlan); - 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 - ); - } + return { + message: SYS_MSG.BILLING_PLAN_CREATED, + data: plan, + }; } 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 => ({ id: plan.id, name: plan.name, price: plan.price })); - - 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 = { id: billingPlan.id, name: billingPlan.name, price: billingPlan.price }; - - 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, + }; } - 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, - }); + async updateBillingPlan(id: string, updateBillingPlanDto: UpdateBillingPlanDto): Promise { + const billing_plan = await this.billingPlanRepository.findOneBy({ id }); + 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); + } - return [freePlan, basicPlan, advancedPlan, premiumPlan]; + 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 new file mode 100644 index 000000000..97faf5c4e --- /dev/null +++ b/src/modules/billing-plans/docs/billing-plan-docs.ts @@ -0,0 +1,47 @@ +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( + 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 getSingleBillingPlanDocs() { + 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' }) + ); +} + +export function updateBillingPlanDocs() { + 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 deleteBillingPlanDocs() { + 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/billing-plan.dto.ts b/src/modules/billing-plans/dto/billing-plan.dto.ts index ddd5829de..20cad8604 100644 --- a/src/modules/billing-plans/dto/billing-plan.dto.ts +++ b/src/modules/billing-plans/dto/billing-plan.dto.ts @@ -1,13 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; -import { randomUUID } from 'crypto'; +import { IsString, IsOptional, IsNumberString, IsBoolean } from 'class-validator'; export class BillingPlanDto { - @ApiProperty({ example: randomUUID() }) - id: string; - @ApiProperty({ example: 'Free' }) + @IsString() name: string; + @ApiProperty({ example: 'Free' }) + @IsString() + @IsOptional() + description: string; + + @ApiProperty({ example: 'monthly' }) + @IsString() + frequency: string; + @ApiProperty({ example: 0 }) - price: number; + @IsNumberString() + amount: number; + + @ApiProperty({ example: 'true' }) + @IsBoolean() + is_active: boolean; } 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) {} 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..667d9560f --- /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, + }; + } +} 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..d16161069 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,10 @@ 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, 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; @@ -26,29 +28,67 @@ 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 }, - ]; - - jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); - - const result = await service.createBillingPlan(); - - expect(result).toEqual({ - status_code: 200, - message: 'Billing plans already exist in the database', - data: billingPlans, - }); + 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 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); + + await expect(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', 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[]); @@ -57,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)), }); }); @@ -70,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); @@ -78,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(billingPlan), }); }); diff --git a/src/modules/contact-us/contact-us.controller.ts b/src/modules/contact-us/contact-us.controller.ts index d4c41f969..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,10 +11,10 @@ import { skipAuth } from '../..//helpers/skipAuth'; export class ContactUsController { constructor(private readonly contactUsService: ContactUsService) {} - @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 2009cff26..19b2aa1a1 100644 --- a/src/modules/contact-us/contact-us.service.ts +++ b/src/modules/contact-us/contact-us.service.ts @@ -1,9 +1,11 @@ -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'; 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 { @@ -16,27 +18,22 @@ 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) { - throw new InternalServerErrorException('Failed to send email'); - } - + await this.sendEmail(createContactDto); return { - message: 'Inquiry sent successfully', - status_code: 200, + message: SYS_MSG.INQUIRY_SENT, + status_code: HttpStatus.CREATED, }; } private async sendEmail(contactDto: CreateContactDto) { await this.mailerService.sendMail({ - to: 'amal_salam@yahoo.com', - subject: 'New Contact Inquiry', + to: [contactDto.email, 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/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 453eb8432..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,15 +1,19 @@ -import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +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; - @IsNotEmpty() - @IsString() + @IsOptional() + @IsInt() + phone: number; + + @IsNotEmpty({ message: 'Message should not be empty' }) + @IsString({ message: 'Message should not be a string' }) 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/contact-us/test/contact-us.service.spec.ts b/src/modules/contact-us/test/contact-us.service.spec.ts index 15479c997..a05c011a8 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,15 +39,12 @@ 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 = { name: 'John Doe', email: 'john@example.com', + phone: 123456789, message: 'Test message', }; @@ -59,21 +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', - 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.CREATED }); }); }); }); 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 diff --git a/src/modules/email/hng-templates/waitlist-confirmation.hbs b/src/modules/email/hng-templates/waitlist-confirmation.hbs new file mode 100644 index 000000000..678aad54e --- /dev/null +++ b/src/modules/email/hng-templates/waitlist-confirmation.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 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..ae307ac31 100644 --- a/src/modules/faq/faq.module.ts +++ b/src/modules/faq/faq.module.ts @@ -7,10 +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'; @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..20fa4125d 100644 --- a/src/modules/faq/faq.service.ts +++ b/src/modules/faq/faq.service.ts @@ -1,43 +1,61 @@ -import { BadRequestException, Injectable, InternalServerErrorException, UnauthorizedException } 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'; @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); + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); + } + + async create(createFaqDto: CreateFaqDto, language?: string): Promise { + 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 this.faqRepository.save(faq); } - async findAllFaq() { - try { - const faqs = await this.faqRepository.find(); - return { - message: 'Faq fetched successfully', - status_code: 200, - data: faqs, - }; - } catch (error) { - if (error instanceof BadRequestException) { - return { - message: 'Invalid request', - status_code: 400, - }; - } else if (error instanceof InternalServerErrorException) { - throw error; - } - } + + async findAllFaq(language?: string) { + 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) { + async updateFaq(id: string, updateFaqDto: UpdateFaqDto, language?: string) { const faq = await this.faqRepository.findOne({ where: { id } }); if (!faq) { throw new BadRequestException({ @@ -45,28 +63,32 @@ export class FaqService { status_code: 400, }); } - try { - Object.assign(faq, updateFaqDto); - const updatedFaq = await this.faqRepository.save(faq); - return { - id: updatedFaq.id, - question: updatedFaq.question, - answer: updatedFaq.answer, - 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, - }; - } + + 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) { diff --git a/src/modules/faq/test/faq.create.service.spec.ts b/src/modules/faq/test/faq.create.service.spec.ts index 3135e421d..038ef30a4 100644 --- a/src/modules/faq/test/faq.create.service.spec.ts +++ b/src/modules/faq/test/faq.create.service.spec.ts @@ -4,6 +4,13 @@ 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 +31,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..9389f9261 100644 --- a/src/modules/faq/test/faq.service.spec.ts +++ b/src/modules/faq/test/faq.service.spec.ts @@ -4,6 +4,13 @@ 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 +27,10 @@ describe('FaqService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FaqService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Faq), useValue: mockRepository, 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..55929707a 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,75 @@ 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'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; + +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 +86,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/docs/helpCenter-swagger.ts b/src/modules/help-center/docs/helpCenter-swagger.ts new file mode 100644 index 000000000..5f7eb7672 --- /dev/null +++ b/src/modules/help-center/docs/helpCenter-swagger.ts @@ -0,0 +1,56 @@ +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.' }) + ); +} + +export function UpdateHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Update a help center topic by id' }), + ApiResponse({ status: 200, description: 'Topic updated successfully' }), + ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }), + ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }), + ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }), + ApiResponse({ status: 500, description: 'Internal Server Error' }) + ); +} + +export function DeleteHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Delete a help center topic by id' }), + ApiResponse({ status: 200, description: 'Topic deleted successfully' }), + ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }), + ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }), + ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }), + ApiResponse({ status: 500, description: 'Internal Server Error' }) + ); +} diff --git a/src/modules/help-center/help-center.controller.ts b/src/modules/help-center/help-center.controller.ts index 62c730b02..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'; @@ -27,6 +28,16 @@ import { } from './dto/help-center.response.dto'; import { SuperAdminGuard } from '../../guards/super-admin.guard'; import { User } from '../user/entities/user.entity'; +import { + CreateHelpCenterDocs, + DeleteHelpCenterDocs, + GetAllHelpCenterDocs, + GetByIdHelpCenterDocs, + SearchHelpCenterDocs, + UpdateHelpCenterDocs, +} from './docs/helpCenter-swagger'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import * as SYS_MSG from '../../helpers/SystemMessages'; @ApiTags('help-center') @Controller('help-center') @@ -36,158 +47,51 @@ export class HelpCenterController { @ApiBearerAuth() @Post('topics') @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.' }) + @CreateHelpCenterDocs() 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(); + @GetAllHelpCenterDocs() + async findAll(@Req() req: any): Promise { + const language = req.language; + return this.helpCenterService.findAll(language); } @skipAuth() @Get('topics/:id') - @ApiOperation({ summary: 'Get a help center topic by ID' }) - @ApiResponse({ status: 200, description: 'The found record' }) - @ApiResponse({ status: 404, description: 'Topic not found' }) + @GetByIdHelpCenterDocs() async findOne(@Param() params: GetHelpCenterDto): Promise { - const helpCenter = await this.helpCenterService.findOne(params.id); - if (!helpCenter) { - throw new NotFoundException(`Help center topic with ID ${params.id} not found`); - } + const helpCenter = await this.helpCenterService.findHelpCenter(params.id); return helpCenter; } @skipAuth() @Get('topics/search') - @ApiOperation({ summary: 'Search help center topics' }) - @ApiResponse({ status: 200, description: 'The found records' }) - @ApiResponse({ status: 422, description: 'Invalid search criteria.' }) + @SearchHelpCenterDocs() async search(@Query() query: SearchHelpCenterDto): Promise { return this.helpCenterService.search(query); } @ApiBearerAuth() @Patch('topics/:id') - @ApiOperation({ summary: 'Update a help center topic by id' }) - @ApiResponse({ status: 200, description: 'Topic updated successfully' }) - @ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }) - @ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }) - @ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }) - @ApiResponse({ status: 500, description: 'Internal Server Error' }) + @UpdateHelpCenterDocs() async update(@Param('id') id: string, @Body() updateHelpCenterDto: UpdateHelpCenterDto) { - try { - const updatedHelpCenter = await this.helpCenterService.updateTopic(id, updateHelpCenterDto); - return { - success: true, - message: 'Topic updated successfully', - data: updatedHelpCenter, - status_code: HttpStatus.OK, - }; - } catch (error) { - if (error.status === HttpStatus.UNAUTHORIZED) { - throw new HttpException( - { - success: false, - message: 'Unauthorized, please provide valid credentials', - status_code: HttpStatus.UNAUTHORIZED, - }, - HttpStatus.UNAUTHORIZED - ); - } else if (error.status === HttpStatus.FORBIDDEN) { - throw new HttpException( - { - success: false, - message: 'Access denied, only authorized users can access this endpoint', - status_code: HttpStatus.FORBIDDEN, - }, - HttpStatus.FORBIDDEN - ); - } else if (error.status === HttpStatus.NOT_FOUND) { - throw new HttpException( - { - success: false, - message: 'Topic not found, please check and try again', - status_code: HttpStatus.NOT_FOUND, - }, - HttpStatus.NOT_FOUND - ); - } else { - throw new HttpException( - { - success: false, - message: 'Internal Server Error', - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } + const updatedHelpCenter = await this.helpCenterService.updateTopic(id, updateHelpCenterDto); + return updatedHelpCenter; } @ApiBearerAuth() @Delete('topics/:id') - @ApiOperation({ summary: 'Delete a help center topic by id' }) - @ApiResponse({ status: 200, description: 'Topic deleted successfully' }) - @ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }) - @ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }) - @ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }) - @ApiResponse({ status: 500, description: 'Internal Server Error' }) + @DeleteHelpCenterDocs() async remove(@Param('id') id: string) { - try { - await this.helpCenterService.removeTopic(id); - return { - success: true, - message: 'Topic deleted successfully', - status_code: HttpStatus.OK, - }; - } catch (error) { - if (error.status === HttpStatus.UNAUTHORIZED) { - throw new HttpException( - { - success: false, - message: 'Unauthorized, please provide valid credentials', - status_code: HttpStatus.UNAUTHORIZED, - }, - HttpStatus.UNAUTHORIZED - ); - } else if (error.status === HttpStatus.FORBIDDEN) { - throw new HttpException( - { - success: false, - message: 'Access denied, only authorized users can access this endpoint', - status_code: HttpStatus.FORBIDDEN, - }, - HttpStatus.FORBIDDEN - ); - } else if (error.status === HttpStatus.NOT_FOUND) { - throw new HttpException( - { - success: false, - message: 'Topic not found, please check and try again', - status_code: HttpStatus.NOT_FOUND, - }, - HttpStatus.NOT_FOUND - ); - } else { - throw new HttpException( - { - success: false, - message: 'Internal Server Error', - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } + return await this.helpCenterService.removeTopic(id); } -} \ 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..fa49edb94 100644 --- a/src/modules/help-center/help-center.service.ts +++ b/src/modules/help-center/help-center.service.ts @@ -5,9 +5,10 @@ import { HelpCenterEntity } from '../help-center/entities/help-center.entity'; import { CreateHelpCenterDto } from './dto/create-help-center.dto'; import { UpdateHelpCenterDto } from './dto/update-help-center.dto'; import { SearchHelpCenterDto } from './dto/search-help-center.dto'; -import { REQUEST_SUCCESSFUL, QUESTION_ALREADY_EXISTS, USER_NOT_FOUND } from '../../helpers/SystemMessages'; +import * as SYS_MSG 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,16 +16,21 @@ 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 }, }); if (existingTopic) { - throw new CustomHttpException(QUESTION_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); + throw new CustomHttpException(SYS_MSG.QUESTION_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); } const fullUser = await this.userRepository.findOne({ @@ -33,11 +39,17 @@ export class HelpCenterService { }); if (!fullUser) { - throw new CustomHttpException(USER_NOT_FOUND, HttpStatus.NOT_FOUND); + throw new CustomHttpException(SYS_MSG.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,13 +61,20 @@ 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, + message: SYS_MSG.REQUEST_SUCCESSFUL, }; } @@ -66,11 +85,19 @@ export class HelpCenterService { } return { status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, + message: SYS_MSG.REQUEST_SUCCESSFUL, data: helpCenter, }; } + async findHelpCenter(id: string) { + const helpCenter = await this.findOne(id); + if (!helpCenter) { + throw new CustomHttpException(`Help center topic with ID ${id} not found`, HttpStatus.NOT_FOUND); + } + return helpCenter; + } + async search(criteria: SearchHelpCenterDto) { const queryBuilder = this.helpCenterRepository.createQueryBuilder('help_center'); if (criteria.title) { @@ -82,7 +109,7 @@ export class HelpCenterService { const query = await queryBuilder.getMany(); return { status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, + message: SYS_MSG.REQUEST_SUCCESSFUL, data: query, }; } @@ -93,7 +120,7 @@ export class HelpCenterService { throw new HttpException( { status: 'error', - message: 'Topic not found, please check and try again', + message: SYS_MSG.TOPIC_NOT_FOUND, status_code: HttpStatus.NOT_FOUND, }, HttpStatus.NOT_FOUND @@ -105,7 +132,7 @@ export class HelpCenterService { return { status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, + message: SYS_MSG.REQUEST_SUCCESSFUL, data: updatedTopic, }; } @@ -116,7 +143,7 @@ export class HelpCenterService { throw new HttpException( { status: 'error', - message: 'Topic not found, unable to delete', + message: SYS_MSG.TOPIC_NOT_FOUND, status_code: HttpStatus.NOT_FOUND, }, HttpStatus.NOT_FOUND 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 b5a2a9787..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,7 +47,22 @@ 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(); + 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..7f5c79509 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -33,6 +33,30 @@ export class InviteService { private readonly OrganisationService: OrganisationsService ) {} + async getPendingInvites(): Promise<{ message: string; data: InviteDto[] }> { + try { + const pendingInvites = await this.inviteRepository.find({ + where: { isAccepted: false }, + }); + + 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 { + message: 'Successfully fetched pending Invites', + data: pendingInvitesDto, + }; + } 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(); @@ -59,7 +83,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) { diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 90a5936b0..5f7fbf39e 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -167,9 +167,27 @@ 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); }); + 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({ + 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'; 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' }) + ); +} diff --git a/src/modules/permissions/entities/permissions.entity.ts b/src/modules/permissions/entities/permissions.entity.ts index 59d2a196d..d87cd34c4 100644 --- a/src/modules/permissions/entities/permissions.entity.ts +++ b/src/modules/permissions/entities/permissions.entity.ts @@ -6,7 +6,6 @@ import { Role } from '../../../modules/role/entities/role.entity'; export class Permissions extends AbstractBaseEntity { @Column() title: string; - @ManyToMany(() => Role, role => role.permissions) roles: Role[]; } 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..312fde31b 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 '../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..5793b1fe0 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..9751aebfe 100644 --- a/src/modules/testimonials/tests/testimonials.service.spec.ts +++ b/src/modules/testimonials/tests/testimonials.service.spec.ts @@ -12,6 +12,13 @@ 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 +30,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..282209637 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/modules/translation/translation.service.ts b/src/modules/translation/translation.service.ts new file mode 100644 index 000000000..6788de5f4 --- /dev/null +++ b/src/modules/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; + } +} 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 } }; } } 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 +}