├── .env.development ├── .github └── workflows │ ├── build.yml │ └── merge.yml ├── .gitignore ├── .sentryclirc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── config │ ├── dbConnection.ts │ ├── docs │ │ ├── MapCenterSchema.yml │ │ ├── ReportsSchema.yml │ │ ├── ReviewFeedbackSchema.yml │ │ ├── SellersItemSchema.yml │ │ ├── SellersSchema.yml │ │ ├── TogglesSchema.yml │ │ ├── UserPreferencesSchema.yml │ │ ├── UsersSchema.yml │ │ └── enum │ │ │ ├── DeviceLocationType.yml │ │ │ ├── FulfillmentType.yml │ │ │ ├── RatingScale.yml │ │ │ ├── SellerType.yml │ │ │ ├── StockLevelType.yml │ │ │ └── TrustMeterScale.yml │ ├── loggingConfig.ts │ ├── platformAPIclient.ts │ ├── sentryConnection.ts │ └── swagger.ts ├── controllers │ ├── admin │ │ ├── reportsController.ts │ │ ├── restrictionController.ts │ │ └── toggleController.ts │ ├── mapCenterController.ts │ ├── reviewFeedbackController.ts │ ├── sellerController.ts │ ├── userController.ts │ └── userPreferencesController.ts ├── cron │ ├── index.ts │ ├── jobs │ │ └── sanctionBot.job.ts │ └── utils │ │ ├── geoUtils.ts │ │ └── sanctionUtils.ts ├── helpers │ ├── imageUploader.ts │ ├── jwt.ts │ └── location.ts ├── index.ts ├── middlewares │ ├── isPioneerFound.ts │ ├── isSellerFound.ts │ ├── isToggle.ts │ ├── isUserSettingsFound.ts │ ├── logger.ts │ └── verifyToken.ts ├── models │ ├── ReviewFeedback.ts │ ├── Seller.ts │ ├── SellerItem.ts │ ├── User.ts │ ├── UserSettings.ts │ ├── enums │ │ ├── deviceLocationType.ts │ │ ├── fulfillmentType.ts │ │ ├── ratingScale.ts │ │ ├── restrictedArea.ts │ │ ├── sellerType.ts │ │ ├── stockLevelType.ts │ │ └── trustMeterScale.ts │ └── misc │ │ ├── SanctionedRegion.ts │ │ └── Toggle.ts ├── routes │ ├── home.routes.ts │ ├── index.ts │ ├── mapCenter.routes.ts │ ├── report.routes.ts │ ├── restriction.routes.ts │ ├── reviewFeedback.routes.ts │ ├── seller.routes.ts │ ├── toggle.routes.ts │ ├── user.routes.ts │ └── userPreferences.routes.ts ├── services │ ├── admin │ │ ├── report.service.ts │ │ ├── restriction.service.ts │ │ └── toggle.service.ts │ ├── mapCenter.service.ts │ ├── misc │ │ └── image.service.ts │ ├── reviewFeedback.service.ts │ ├── seller.service.ts │ ├── user.service.ts │ └── userSettings.service.ts ├── types.ts └── utils │ ├── app.ts │ ├── cloudinary.ts │ ├── env.ts │ └── multer.ts ├── test ├── controllers │ ├── admin │ │ ├── reportsController.spec.ts │ │ ├── restrictionController.spec.ts │ │ └── toggleController.spec.ts │ ├── mapCenterController.spec.ts │ ├── reviewFeedbackController.spec.ts │ ├── sellerController.spec.ts │ └── userController.spec.ts ├── cron │ ├── job │ │ └── sanctionBot.job.spec.ts │ └── utils │ │ ├── geoUtils.spec.ts │ │ └── sanctionUtils.spec.ts ├── helpers │ └── location.spec.ts ├── jest.setup.ts ├── middlewares │ ├── isToggle.spec.ts │ └── verifyToken.spec.ts ├── mockData.json ├── routes │ └── home.routes.spec.ts └── services │ ├── admin │ ├── report.service.spec.ts │ ├── restriction.service.spec.ts │ └── toggle.service.spec.ts │ ├── mapCenter.service.spec.ts │ ├── reviewFeedback.service.spec.ts │ ├── seller.service.spec.ts │ └── userSettings.service.spec.ts ├── tsconfig.json └── vercel.json /.env.development: -------------------------------------------------------------------------------- 1 | # The .env.development file is a template for environment variables. 2 | # Only add new variables or update existing ones here, but DO NOT include actual values. 3 | # Use placeholders in the form of "ADD YOUR $" where $ represents the purpose of the variable. 4 | # Your environment-specific values should be set in your .env.local file, which is not tracked in version control. 5 | 6 | NODE_ENV=development 7 | JWT_SECRET=MAP_OF_PI 8 | PLATFORM_API_URL=https://api.minepi.com 9 | PI_API_KEY="ADD YOUR PI API KEY" 10 | ADMIN_API_USERNAME="ADD YOUR ADMIN API USERNAME" 11 | ADMIN_API_PASSWORD="ADD YOUR ADMIN API PASSWORD" 12 | 13 | MONGODB_URL=mongodb://localhost:27017/demoDB 14 | MONGODB_MIN_POOL_SIZE=1 15 | MONGODB_MAX_POOL_SIZE=5 16 | 17 | SENTRY_DSN="ADD YOUR SENTRY DSN" 18 | 19 | PORT=8001 20 | UPLOAD_PATH=../../tmp/uploads 21 | 22 | CLOUDINARY_CLOUD_NAME="ADD YOUR CLOUDINARY CLOUD NAME" 23 | CLOUDINARY_API_KEY="ADD YOUR CLOUDINARY API KEY" 24 | CLOUDINARY_API_SECRET="ADD YOUR CLOUDINARY API SECRET" 25 | 26 | DEVELOPMENT_URL=http://localhost:8001 27 | PRODUCTION_URL=https://map-of-pi-backend-react.vercel.app/ 28 | CORS_ORIGIN_URL=http://localhost:4200 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Map of Pi Build 2 | 3 | # Controls when the workflow will execute 4 | on: 5 | # Triggers the workflow on push or pull request events for the "main" and "dev" branch 6 | push: 7 | branches: 8 | - main 9 | - dev 10 | pull_request: 11 | branches: 12 | - main 13 | - dev 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | jobs: 19 | build: 20 | name: 🏗️ Build 21 | runs-on: ubuntu-latest 22 | # The sequence of tasks that will be executed as part of the job 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - name: Install Node 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18.x 31 | 32 | # Backend CI/CD Process 33 | - name: Install Dependencies for Backend 34 | run: npm ci 35 | 36 | - name: Execute Tests for Backend 37 | run: npm run test -- --passWithNoTests 38 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Map of Pi Merge 2 | 3 | # Controls when the workflow will execute 4 | on: 5 | # Triggers the workflow when build successfully completes 6 | workflow_run: 7 | workflows: ["Map of Pi Build"] 8 | types: 9 | - completed 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | merge: 16 | name: 🔀 Merge 17 | runs-on: ubuntu-latest 18 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' }} 19 | 20 | # The sequence of tasks that will be executed as part of the job 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Merge dev to main 26 | uses: repo-sync/pull-request@v2 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | source_branch: "dev" 30 | destination_branch: "main" 31 | pr_title: "Automated PR to merge dev to main" 32 | pr_body: ":robot: Automated PR from **dev** to **main**; see commits." 33 | pr_label: "auto-pr" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # local env files 13 | .env 14 | .vercel 15 | 16 | # local misc 17 | uploads 18 | /scripts 19 | -------------------------------------------------------------------------------- /.sentryclirc: -------------------------------------------------------------------------------- 1 | 2 | [auth] 3 | token=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 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 9 | "args": [ 10 | "--runInBand", 11 | "--config", 12 | "${workspaceFolder}/jest.config.js", 13 | "--verbose" 14 | ], 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "env": { 18 | "NODE_ENV": "test" 19 | } 20 | }, 21 | { 22 | "name": "Attach by Process ID", 23 | "processId": "${command:PickProcess}", 24 | "request": "attach", 25 | "type": "node" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PiOS License 2 | 3 | Copyright (C) 2024 Map Of Pi Team 4 | 5 | Permission is hereby granted by the application software developer (“Software Developer”), free 6 | of charge, to any person obtaining a copy of this application, software and associated 7 | documentation files (the “Software”), which was developed by the Software Developer for use on 8 | Pi Network, whereby the purpose of this license is to permit the development of derivative works 9 | based on the Software, including the right to use, copy, modify, merge, publish, distribute, 10 | sub-license, and/or sell copies of such derivative works and any Software components incorporated 11 | therein, and to permit persons to whom such derivative works are furnished to do so, in each case, 12 | solely to develop, use and market applications for the official Pi Network. For purposes of this 13 | license, Pi Network shall mean any application, software, or other present or future platform 14 | developed, owned or managed by Pi Community Company, and its parents, affiliates or subsidiaries, 15 | for which the Software was developed, or on which the Software continues to operate. However, 16 | you are prohibited from using any portion of the Software or any derivative works thereof in any 17 | manner (a) which infringes on any Pi Network intellectual property rights, (b) to hack any of Pi 18 | Network’s systems or processes or (c) to develop any product or service which is competitive with 19 | the Pi Network. 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or 22 | substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 25 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 26 | AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, PUBLISHERS, OR COPYRIGHT HOLDERS OF THIS 27 | SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO BUSINESS INTERRUPTION, LOSS OF USE, DATA OR PROFITS) 29 | HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 30 | TORT (INCLUDING NEGLIGENCE) ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 31 | OR OTHER DEALINGS IN THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 32 | 33 | Pi, Pi Network and the Pi logo are trademarks of the Pi Community Company. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Map of Pi

2 | 3 |
4 | 5 | [![Hackathon](https://img.shields.io/badge/hackathon-PiCommerce-purple.svg)](https://github.com/pi-apps/PiOS/blob/main/pi-commerce.md) 6 | ![Status](https://img.shields.io/badge/status-active-success.svg) 7 | ![License](https://img.shields.io/badge/license-PIOS-blue.svg) 8 | 9 |
10 | 11 |
12 |

Map of Pi is a mobile application developed to help Pi community members easily locate local businesses that accept Pi as payment. This project was initiated as part of the Pi Commerce Hackathon with the goal of facilitating Pi transactions and connecting businesses with the Pi community.

13 |
14 | 15 | ## Table of Contents 16 | 17 | - [Brand Design](#brand-design) 18 | - [Tech Stack](#tech-stack) 19 | - [Backend Local Execution](#backend-local-execution) 20 | - [Team](#team) 21 | - [Contributions](#contributions) 22 | 23 | ## Brand Design 24 | 25 | | App Logo | App Icon | 26 | | ------------- |:-------------:| 27 | | map-of-pi-logo-revised-3 | map-of-pi-app-icon-revised-3b-transparent 28 | 29 | ## Tech Stack 📊 30 | 31 | - **Frontend**: NextJS/ React, TypeScript, HTML, SCSS, CSS 32 | - **Backend**: Express/ NodeJS, REST API 33 | - **Database**: MongoDB 34 | - **DevOps**: GitHub Actions, Netlify, Vercel 35 | 36 | ## Backend Local Execution 37 | 38 | The Map of Pi Back End is a [Node.js](https://nodejs.org/) project. 39 | 40 | ### Build the Project 41 | 42 | - Run `npm run build` to build the project; compile Typescript into Javascript for production to the `.dist` folder. 43 | - The build artifacts are bundled for production mode. 44 | 45 | ### Execute the Development Server 46 | 47 | - Create .env file from the .env.development template and replace placeholders with actual values. 48 | - Execute `npm run dev` to connect to nodemon and MongoDB server. 49 | - Navigate to http://localhost:8001/ in your browser. 50 | - Execute **[Frontend Local Execution](https://github.com/map-of-pi/map-of-pi-frontend-react/blob/dev/README.md#frontend-local-execution)** for integration testing. Alternatively, utilize API tools like Insomnia or Postman to execute the API endpoints. 51 | - The application will automatically reload if you change any of the source files. 52 | - For local debugging in VS Code, attach the runtime server to the appropriate Process ID. 53 | 54 | ### Execute Unit Tests 55 | 56 | - Run `npm run test` to execute the unit tests via [Jest](https://jestjs.io/). 57 | 58 | ## Team 🧑👩‍🦱🧔👨🏾‍🦱👨🏾 59 | 60 | ### Project Manager 61 | - Philip Jennings 62 | 63 | ### Marketing 64 | - Bonnie Ford 65 | - Joseph Ciccone 66 | 67 | ### Solution Design / UX 68 | - Femma Ashraf 69 | - Oluwabukola Adesina 70 | - Folorunsho Omotunde 71 | - Henry Fasakin 72 | 73 | ### Technical Lead/ DevOps 74 | - Danny Lee 75 | 76 | ### Technical Advisor 77 | - Zoltan Magyar 78 | 79 | ### Application Developers 80 | - Darin Hajou 81 | - Rokundo Soleil 82 | - Ayomikun Omotosho 83 | - Yusuf Adisa 84 | - Francis Mwaura 85 | - Samuel Oluyomi 86 | 87 | ## Contributions 88 | 89 |
90 |

We welcome contributions from the community to improve the Map of Pi project.

91 |
92 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["**/**/*.spec.ts"], 6 | verbose: true, 7 | forceExit: true, 8 | resetMocks: true, 9 | restoreMocks: true, 10 | clearMocks: true, 11 | setupFilesAfterEnv: ['/test/jest.setup.ts'] 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "map-of-pi-backend", 3 | "version": "1.0.0", 4 | "author": "Map of Pi Team", 5 | "description": "Map of Pi Backend", 6 | "license": "PiOS", 7 | "main": "index.js", 8 | "private": true, 9 | "scripts": { 10 | "build": "tsc", 11 | "dev": "nodemon src/index.ts", 12 | "start": "node dist/index.js", 13 | "test": "jest --detectOpenHandles --coverage" 14 | }, 15 | "dependencies": { 16 | "@sentry/integrations": "^7.114.0", 17 | "@sentry/node": "^9.3.0", 18 | "@sentry/profiling-node": "^9.3.0", 19 | "axios": "^1.8.2", 20 | "body-parser": "^1.20.2", 21 | "bottleneck": "^2.19.5", 22 | "cloudinary": "^2.4.0", 23 | "cookie-parser": "^1.4.7", 24 | "cors": "^2.8.5", 25 | "dotenv": "^16.4.5", 26 | "express": "^4.21.2", 27 | "jsonwebtoken": "^9.0.2", 28 | "mongoose": "^8.9.5", 29 | "multer": "^1.4.5-lts.1", 30 | "node-schedule": "^2.1.1", 31 | "nodemailer": "^6.9.13", 32 | "swagger-jsdoc": "^6.2.8", 33 | "swagger-ui-dist": "^5.17.14", 34 | "swagger-ui-express": "^5.0.0", 35 | "winston": "^3.14.2" 36 | }, 37 | "devDependencies": { 38 | "@types/cookie-parser": "^1.4.7", 39 | "@types/cors": "^2.8.17", 40 | "@types/express": "^4.17.21", 41 | "@types/jest": "^29.5.12", 42 | "@types/jsonwebtoken": "^9.0.6", 43 | "@types/mongodb-memory-server": "^2.3.0", 44 | "@types/multer": "^1.4.11", 45 | "@types/node": "^20.12.12", 46 | "@types/node-schedule": "^2.1.7", 47 | "@types/nodemailer": "^6.4.15", 48 | "@types/supertest": "^6.0.2", 49 | "@types/swagger-jsdoc": "^6.0.4", 50 | "@types/swagger-ui-dist": "^3.30.5", 51 | "@types/swagger-ui-express": "^4.1.6", 52 | "@vercel/node": "^2.10.3", 53 | "jest": "^29.7.0", 54 | "mongodb-memory-server": "^10.0.0", 55 | "nodemon": "^3.1.0", 56 | "supertest": "^7.0.0", 57 | "ts-jest": "^29.1.2", 58 | "ts-node": "^10.9.2", 59 | "typescript": "^5.4.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/config/dbConnection.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | import logger from "./loggingConfig"; 4 | import { env } from "../utils/env"; 5 | 6 | export const connectDB = async () => { 7 | try { 8 | // Only log the MongoDB URL in non-production environments 9 | if (env.NODE_ENV !== 'production') { 10 | logger.info(`Connecting to MongoDB with URL: ${env.MONGODB_URL}`); 11 | } 12 | await mongoose.connect(env.MONGODB_URL, { 13 | minPoolSize: env.MONGODB_MIN_POOL_SIZE, 14 | maxPoolSize: env.MONGODB_MAX_POOL_SIZE 15 | }); 16 | logger.info("Successful connection to MongoDB."); 17 | } catch (error) { 18 | logger.error('Failed connection to MongoDB:', error); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/config/docs/MapCenterSchema.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | GetMapCenterRs: 4 | type: object 5 | properties: 6 | origin: 7 | type: object 8 | properties: 9 | map_center_id: 10 | type: string 11 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 12 | search_map_center: 13 | type: object 14 | properties: 15 | latitude: 16 | type: number 17 | example: 40.7128 18 | longitude: 19 | type: number 20 | example: -74.0060 21 | sell_map_center: 22 | type: object 23 | properties: 24 | latitude: 25 | type: number 26 | example: 34.0522 27 | longitude: 28 | type: number 29 | example: -118.2437 30 | 31 | SaveMapCenterRq: 32 | type: object 33 | properties: 34 | latitude: 35 | type: number 36 | example: 40.7128 37 | longitude: 38 | type: number 39 | example: -74.0060 40 | type: 41 | type: string 42 | enum: 43 | - search 44 | - sell 45 | example: search 46 | 47 | SaveMapCenterRs: 48 | type: object 49 | properties: 50 | map_center_id: 51 | type: string 52 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 53 | search_map_center: 54 | type: object 55 | properties: 56 | latitude: 57 | type: number 58 | example: 40.7128 59 | longitude: 60 | type: number 61 | example: -74.0060 62 | sell_map_center: 63 | type: object 64 | properties: 65 | latitude: 66 | type: number 67 | example: 34.0522 68 | longitude: 69 | type: number 70 | example: -118.2437 71 | -------------------------------------------------------------------------------- /src/config/docs/ReportsSchema.yml: -------------------------------------------------------------------------------- 1 | components: 2 | securitySchemes: 3 | AdminPasswordAuth: 4 | type: http 5 | scheme: basic 6 | 7 | schemas: 8 | GetSanctionedSellersReportRs: 9 | type: array 10 | items: 11 | type: object 12 | properties: 13 | seller_id: 14 | type: string 15 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 16 | name: 17 | type: string 18 | example: Test Seller 19 | address: 20 | type: string 21 | example: 1234 Test St, Test City, Democratic People's Republic of Korea 22 | sanctioned_location: 23 | type: string 24 | example: North Korea 25 | sell_map_center: 26 | type: object 27 | properties: 28 | type: 29 | type: string 30 | example: Point 31 | coordinates: 32 | type: array 33 | items: 34 | type: number 35 | example: [-73.856077, 40.848447] 36 | required: 37 | - type 38 | - coordinates -------------------------------------------------------------------------------- /src/config/docs/ReviewFeedbackSchema.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | GetReviewsRs: 4 | type: object 5 | properties: 6 | givenReviews: 7 | type: array 8 | items: 9 | type: object 10 | properties: 11 | _id: 12 | type: string 13 | example: 6674ecee2ac4de0d31e8c048 14 | review_receiver_id: 15 | type: string 16 | example: 409fdceb-2824-4b62-baad-684efc5d6aaa 17 | review_giver_id: 18 | type: string 19 | example: a59c9e26-a6f0-471c-98f6-d14a1978eabb 20 | reply_to_review_id: 21 | type: string 22 | example: 670ae8c3b26e067dead30750 23 | giver: 24 | type: string 25 | example: test_giver 26 | receiver: 27 | type: string 28 | example: test_receiver 29 | rating: 30 | $ref: '/api/docs/enum/RatingScale.yml#/components/schemas/RatingScale' 31 | comment: 32 | type: string 33 | example: test comment 34 | image: 35 | type: string 36 | format: binary 37 | example: http://example.com/image.jpg 38 | review_date: 39 | type: string 40 | format: date-time 41 | example: 2024-06-20T00:00:00.000Z 42 | receivedReviews: 43 | type: array 44 | items: 45 | type: object 46 | properties: 47 | _id: 48 | type: string 49 | example: 6712038d469fb839b610151f 50 | review_receiver_id: 51 | type: string 52 | example: a59c9e26-a6f0-471c-98f6-d14a1978eabb 53 | review_giver_id: 54 | type: string 55 | example: 409fdceb-2824-4b62-baad-684efc5d6aaa 56 | reply_to_review_id: 57 | type: string 58 | example: 6708212c008b4344d6be5b1a 59 | giver: 60 | type: string 61 | example: test_giver 62 | receiver: 63 | type: string 64 | example: test_receiver 65 | rating: 66 | $ref: '/api/docs/enum/RatingScale.yml#/components/schemas/RatingScale' 67 | comment: 68 | type: string 69 | example: test comment 70 | image: 71 | type: string 72 | format: binary 73 | example: http://example.com/image.jpg 74 | review_date: 75 | type: string 76 | format: date-time 77 | example: 2024-07-20T00:00:00.000Z 78 | 79 | GetSingleReviewRs: 80 | type: object 81 | properties: 82 | _id: 83 | type: string 84 | example: 6674ecee2ac4de0d31e8c048 85 | review_receiver_id: 86 | type: string 87 | example: a59c9e26-a6f0-471c-98f6-d14a1978eabb 88 | review_giver_id: 89 | type: string 90 | example: 409fdceb-2824-4b62-baad-684efc5d6aaa 91 | reply_to_review_id: 92 | type: string 93 | example: 6708212c008b4344d6be5b1a 94 | rating: 95 | $ref: '/api/docs/enum/RatingScale.yml#/components/schemas/RatingScale' 96 | comment: 97 | type: string 98 | example: test comment 99 | image: 100 | type: string 101 | format: binary 102 | example: http://example.com/image.jpg 103 | review_date: 104 | type: string 105 | format: date-time 106 | example: 2024-06-20T00:00:00.000Z 107 | __v: 108 | type: number 109 | example: 0 110 | 111 | AddReviewRq: 112 | type: object 113 | properties: 114 | review_receiver_id: 115 | type: string 116 | example: a59c9e26-a6f0-471c-98f6-d14a1978eabb 117 | review_giver_id: 118 | type: string 119 | example: 409fdceb-2824-4b62-baad-684efc5d6aaa 120 | reply_to_review_id: 121 | type: string 122 | example: 6708212c008b4344d6be5b1a 123 | rating: 124 | $ref: '/api/docs/enum/RatingScale.yml#/components/schemas/RatingScale' 125 | comment: 126 | type: string 127 | example: This is a sample review comment. 128 | image: 129 | type: string 130 | format: binary 131 | example: http://example.com/image.jpg 132 | 133 | AddReviewRs: 134 | type: object 135 | properties: 136 | newReview: 137 | type: object 138 | properties: 139 | review_receiver_id: 140 | type: string 141 | example: a59c9e26-a6f0-471c-98f6-d14a1978eabb 142 | review_giver_id: 143 | type: string 144 | example: 409fdceb-2824-4b62-baad-684efc5d6aaa 145 | reply_to_review_id: 146 | type: string 147 | example: 6708212c008b4344d6be5b1a 148 | rating: 149 | $ref: '/api/docs/enum/RatingScale.yml#/components/schemas/RatingScale' 150 | comment: 151 | type: string 152 | example: test comment 153 | image: 154 | type: string 155 | format: binary 156 | example: http://example.com/image.jpg 157 | review_date: 158 | type: string 159 | format: date-time 160 | example: 2024-06-20T00:00:00.000Z 161 | _id: 162 | type: string 163 | example: 6674ecee2ac4de0d31e8c048 164 | __v: 165 | type: number 166 | example: 0 167 | -------------------------------------------------------------------------------- /src/config/docs/SellersItemSchema.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | GetSellerItemRs: 4 | type: object 5 | properties: 6 | seller_id: 7 | type: string 8 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 9 | name: 10 | type: string 11 | example: Test Item 12 | description: 13 | type: string 14 | example: This is a sample seller item description. 15 | price: 16 | type: number 17 | format: double 18 | example: 0.01 19 | stock_level: 20 | $ref: '/api/docs/enum/StockLevelType.yml#/components/schemas/StockLevelType' 21 | image: 22 | type: string 23 | format: binary 24 | example: https://example.com/image.jpg 25 | duration: 26 | type: number 27 | example: 1 28 | created_at: 29 | type: string 30 | format: date-time 31 | example: 2024-12-21T00:00:00.000Z 32 | updated_at: 33 | type: string 34 | format: date-time 35 | example: 2024-12-21T00:00:00.000Z 36 | expired_by: 37 | type: string 38 | format: date-time 39 | example: 2024-12-28T00:00:00.000Z 40 | _id: 41 | type: string 42 | example: 66741c62b175e7d059a2639e 43 | __v: 44 | type: number 45 | example: 0 46 | 47 | AddSellerItemRq: 48 | type: object 49 | properties: 50 | _id: 51 | type: string 52 | example: 66741c62b175e7d059a2639e 53 | seller_id: 54 | type: string 55 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 56 | name: 57 | type: string 58 | example: Test Item 59 | description: 60 | type: string 61 | example: This is a sample seller item description. 62 | price: 63 | type: number 64 | format: double 65 | example: 0.01 66 | stock_level: 67 | $ref: '/api/docs/enum/StockLevelType.yml#/components/schemas/StockLevelType' 68 | image: 69 | type: string 70 | format: binary 71 | example: https://example.com/image.jpg 72 | duration: 73 | type: number 74 | example: 1 75 | 76 | AddSellerItemRs: 77 | type: object 78 | properties: 79 | seller_id: 80 | type: string 81 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 82 | name: 83 | type: string 84 | example: Test Item 85 | description: 86 | type: string 87 | example: This is a sample seller item description. 88 | price: 89 | type: number 90 | format: double 91 | example: 0.01 92 | stock_level: 93 | $ref: '/api/docs/enum/StockLevelType.yml#/components/schemas/StockLevelType' 94 | image: 95 | type: string 96 | format: binary 97 | example: https://example.com/image.jpg 98 | duration: 99 | type: number 100 | example: 1 101 | created_at: 102 | type: string 103 | format: date-time 104 | example: 2024-12-21T00:00:00.000Z 105 | updated_at: 106 | type: string 107 | format: date-time 108 | example: 2024-12-21T00:00:00.000Z 109 | expired_by: 110 | type: string 111 | format: date-time 112 | example: 2024-12-28T00:00:00.000Z 113 | _id: 114 | type: string 115 | example: 66741c62b175e7d059a2639e 116 | __v: 117 | type: number 118 | example: 0 119 | 120 | DeleteSellerItemRs: 121 | type: object 122 | properties: 123 | message: 124 | type: string 125 | example: Seller item deleted successfully. 126 | deletedSellerItem: 127 | type: object 128 | properties: 129 | seller_id: 130 | type: string 131 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 132 | name: 133 | type: string 134 | example: Test Item 135 | description: 136 | type: string 137 | example: This is a sample seller item description. 138 | price: 139 | type: number 140 | format: double 141 | example: 0.01 142 | stock_level: 143 | $ref: '/api/docs/enum/StockLevelType.yml#/components/schemas/StockLevelType' 144 | image: 145 | type: string 146 | format: binary 147 | example: https://example.com/image.jpg 148 | duration: 149 | type: number 150 | example: 1 151 | created_at: 152 | type: string 153 | format: date-time 154 | example: 2024-12-21T00:00:00.000Z 155 | updated_at: 156 | type: string 157 | format: date-time 158 | example: 2024-12-21T00:00:00.000Z 159 | expired_by: 160 | type: string 161 | format: date-time 162 | example: 2024-12-28T00:00:00.000Z 163 | _id: 164 | type: string 165 | example: 66741c62b175e7d059a2639e 166 | __v: 167 | type: number 168 | example: 0 -------------------------------------------------------------------------------- /src/config/docs/TogglesSchema.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | AddToggleRq: 4 | type: object 5 | properties: 6 | name: 7 | type: string 8 | example: testToggle 9 | enabled: 10 | type: boolean 11 | example: false 12 | description: 13 | type: string 14 | example: This is a toggle description. 15 | required: 16 | - name 17 | - enabled 18 | - description 19 | 20 | UpdateToggleRq: 21 | type: object 22 | properties: 23 | name: 24 | type: string 25 | example: testToggle 26 | enabled: 27 | type: boolean 28 | example: false 29 | description: 30 | type: string 31 | example: This is a toggle description. 32 | 33 | GetAllTogglesRs: 34 | type: array 35 | items: 36 | type: object 37 | properties: 38 | name: 39 | type: string 40 | example: testToggle 41 | enabled: 42 | type: boolean 43 | example: false 44 | description: 45 | type: string 46 | example: This is a toggle description. 47 | 48 | GetSingleToggleRs: 49 | type: object 50 | properties: 51 | name: 52 | type: string 53 | example: testToggle 54 | enabled: 55 | type: boolean 56 | example: false 57 | description: 58 | type: string 59 | example: This is a toggle description. 60 | 61 | AddToggleRs: 62 | type: object 63 | properties: 64 | addedToggle: 65 | name: 66 | type: string 67 | example: testToggle 68 | enabled: 69 | type: boolean 70 | example: false 71 | description: 72 | type: string 73 | example: This is a toggle description. 74 | 75 | UpdateToggleRs: 76 | type: object 77 | properties: 78 | updatedToggle: 79 | name: 80 | type: string 81 | example: testToggle 82 | enabled: 83 | type: boolean 84 | example: false 85 | description: 86 | type: string 87 | example: This is a toggle description. 88 | 89 | DeleteToggleRs: 90 | type: object 91 | properties: 92 | deletedToggle: 93 | name: 94 | type: string 95 | example: testToggle 96 | enabled: 97 | type: boolean 98 | example: false 99 | description: 100 | type: string 101 | example: This is a toggle description. 102 | -------------------------------------------------------------------------------- /src/config/docs/UsersSchema.yml: -------------------------------------------------------------------------------- 1 | components: 2 | securitySchemes: 3 | BearerAuth: 4 | type: http 5 | scheme: bearer 6 | 7 | schemas: 8 | AuthenticateUserRq: 9 | type: object 10 | properties: 11 | user: 12 | type: object 13 | properties: 14 | pi_uid: 15 | type: string 16 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 17 | pi_username: 18 | type: string 19 | example: test_alias 20 | user_name: 21 | type: string 22 | example: Test Alias 23 | required: 24 | - pi_uid 25 | required: 26 | - user 27 | 28 | AuthenticateUserRs: 29 | type: object 30 | properties: 31 | user: 32 | type: object 33 | properties: 34 | _id: 35 | type: string 36 | example: 666bbae4a05bcc3d8dfab563 37 | pi_uid: 38 | type: string 39 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 40 | pi_username: 41 | type: string 42 | example: test_alias 43 | user_name: 44 | type: string 45 | example: Test Alias 46 | __v: 47 | type: integer 48 | example: 0 49 | token: 50 | type: string 51 | example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NjZiYmFlNGEwNWJjYzNkOGRmYWI1NjMiLCJpYXQiOjE3MTgzMzk0MDksImV4cCI6MTcyMDkzMTQwOX0.gFz-EdHoOqz3-AuFX5R4uGtruFaTMH8sTOXEX-3c7yw 52 | required: 53 | - user 54 | - token 55 | 56 | GetUserRs: 57 | type: object 58 | properties: 59 | user: 60 | type: object 61 | properties: 62 | _id: 63 | type: string 64 | example: 666bbae4a05bcc3d8dfab563 65 | pi_uid: 66 | type: string 67 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 68 | pi_username: 69 | type: string 70 | example: test_alias 71 | user_name: 72 | type: string 73 | example: Test Alias 74 | __v: 75 | type: integer 76 | example: 0 77 | required: 78 | - user 79 | 80 | DeleteUserRs: 81 | type: object 82 | properties: 83 | message: 84 | type: string 85 | example: User deleted successfully. 86 | deletedData: 87 | type: object 88 | properties: 89 | user: 90 | type: object 91 | properties: 92 | _id: 93 | type: string 94 | example: 666bbae4a05bcc3d8dfab563 95 | pi_uid: 96 | type: string 97 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 98 | pi_username: 99 | type: string 100 | example: test_alias 101 | user_name: 102 | type: string 103 | example: Test Alias 104 | __v: 105 | type: integer 106 | example: 0 107 | sellers: 108 | type: array 109 | items: 110 | type: object 111 | properties: 112 | seller_id: 113 | type: string 114 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 115 | name: 116 | type: string 117 | example: Test Seller 118 | description: 119 | type: string 120 | example: This is a sample seller description. 121 | image: 122 | type: string 123 | example: http://example.com/image.jpg 124 | address: 125 | type: string 126 | example: 1234 Test St, Test City, SC 12345 127 | average_rating: 128 | type: object 129 | properties: 130 | $numberDecimal: 131 | type: string 132 | example: 4.5 133 | required: 134 | - $numberDecimal 135 | trust_meter_rating: 136 | $ref: '/api/docs/enum/TrustMeterScale.yml#/components/schemas/TrustMeterScale' 137 | sell_map_center: 138 | type: object 139 | properties: 140 | type: 141 | type: string 142 | example: Point 143 | coordinates: 144 | type: array 145 | items: 146 | type: number 147 | example: [-73.856077, 40.848447] 148 | required: 149 | - type 150 | - coordinates 151 | order_online_enabled_pref: 152 | type: boolean 153 | example: true 154 | userSetting: 155 | type: object 156 | properties: 157 | user_settings_id: 158 | type: string 159 | example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0 160 | email: 161 | type: string 162 | nullable: true 163 | example: test_user_preferences@example.com 164 | phone_number: 165 | type: string 166 | example: 123456789 167 | image: 168 | type: string 169 | example: https://example.com/image.jpg 170 | search_map_center: 171 | type: object 172 | properties: 173 | type: 174 | type: string 175 | example: Point 176 | coordinates: 177 | type: array 178 | items: 179 | type: number 180 | example: [-73.856077, 40.848447] 181 | required: 182 | - type 183 | - coordinates 184 | -------------------------------------------------------------------------------- /src/config/docs/enum/DeviceLocationType.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | DeviceLocationType: 4 | type: string 5 | enum: 6 | - auto 7 | - deviceGPS 8 | - searchCenter 9 | description: The type of FindMe device location. 10 | -------------------------------------------------------------------------------- /src/config/docs/enum/FulfillmentType.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | FulfillmentType: 4 | type: string 5 | enum: 6 | - Collection by buyer 7 | - Delivered to buyer 8 | description: The type of fulfillment method. 9 | -------------------------------------------------------------------------------- /src/config/docs/enum/RatingScale.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | RatingScale: 4 | type: number 5 | enum: 6 | - 0 7 | - 2 8 | - 3 9 | - 4 10 | - 5 11 | description: The rating scale where 0 is DESPAIR, 2 is SAD, 3 is OKAY, 4 is HAPPY, 5 is DELIGHT. 12 | -------------------------------------------------------------------------------- /src/config/docs/enum/SellerType.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | SellerType: 4 | type: string 5 | enum: 6 | - activeSeller 7 | - inactiveSeller 8 | - testSeller 9 | description: The type of seller. 10 | -------------------------------------------------------------------------------- /src/config/docs/enum/StockLevelType.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | StockLevelType: 4 | type: string 5 | enum: 6 | - 1 available 7 | - 2 available 8 | - 3 available 9 | - Many available 10 | - Made to order 11 | - Ongoing service 12 | - Sold 13 | description: The type of stock level. 14 | -------------------------------------------------------------------------------- /src/config/docs/enum/TrustMeterScale.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | TrustMeterScale: 4 | type: number 5 | enum: 6 | - 0 7 | - 50 8 | - 80 9 | - 100 10 | description: The trust meter scale measured in increments for the seller. 11 | -------------------------------------------------------------------------------- /src/config/loggingConfig.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports, Logger } from "winston"; 2 | 3 | import { env } from "../utils/env"; 4 | import { SentryTransport } from "./sentryConnection"; 5 | 6 | // define the logging configuration logic 7 | export const getLoggerConfig = (): { level: string; format: any; transports: any[] } => { 8 | let logLevel: string = ''; 9 | let logFormat: any; 10 | const loggerTransports: any[] = []; 11 | 12 | if (env.NODE_ENV === 'development' || env.NODE_ENV === 'sandbox') { 13 | logLevel = 'info'; 14 | logFormat = format.combine(format.colorize(), format.simple()); 15 | loggerTransports.push(new transports.Console({ format: logFormat })); 16 | } else if (env.NODE_ENV === 'production') { 17 | logLevel = 'error'; 18 | logFormat = format.combine(format.timestamp(), format.json()); 19 | loggerTransports.push(new SentryTransport({ stream: process.stdout })); 20 | } 21 | 22 | return { level: logLevel, format: logFormat, transports: loggerTransports }; 23 | }; 24 | 25 | // Create the logger using the configuration 26 | const loggerConfig = getLoggerConfig(); 27 | 28 | // set up Winston logger accordingly 29 | const logger: Logger = createLogger({ 30 | level: loggerConfig.level, 31 | format: loggerConfig.format, 32 | transports: loggerConfig.transports 33 | }); 34 | 35 | export default logger; 36 | -------------------------------------------------------------------------------- /src/config/platformAPIclient.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { env } from "../utils/env"; 3 | 4 | export const platformAPIClient = axios.create({ 5 | baseURL: env.PLATFORM_API_URL, 6 | timeout: 20000, 7 | headers: { 8 | Authorization: `Key ${env.PI_API_KEY}`, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/config/sentryConnection.ts: -------------------------------------------------------------------------------- 1 | import { env } from "../utils/env"; 2 | import * as Sentry from "@sentry/node"; 3 | import { nodeProfilingIntegration } from "@sentry/profiling-node"; 4 | import { transports } from "winston"; 5 | 6 | // initialize Sentry only in production environment 7 | if (env.NODE_ENV === 'production') { 8 | try { 9 | // initialize Sentry 10 | Sentry.init({ 11 | dsn: env.SENTRY_DSN, 12 | integrations: [ 13 | nodeProfilingIntegration(), 14 | ], 15 | tracesSampleRate: 1.0, // adjust this based on your need for performance monitoring 16 | profilesSampleRate: 1.0 17 | }); 18 | 19 | } catch (error: any) { 20 | throw new Error(`Failed connection to Sentry: ${error.message}`); 21 | } 22 | } 23 | 24 | // create a custom Sentry transport for Winston in production 25 | class SentryTransport extends transports.Stream { 26 | log(info: any, callback: () => void) { 27 | setImmediate(() => this.emit('logged', info)); 28 | 29 | if (info.level === 'error') { 30 | Sentry.captureException(new Error(info.message)); 31 | } 32 | callback(); 33 | return true; 34 | } 35 | } 36 | 37 | export { SentryTransport }; 38 | -------------------------------------------------------------------------------- /src/config/swagger.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { serve, setup } from "swagger-ui-express"; 3 | import swaggerJsDoc from "swagger-jsdoc"; 4 | import swaggerUI from "swagger-ui-dist"; 5 | import path from "path"; 6 | 7 | import { env } from "../utils/env"; 8 | 9 | const docRouter = Router(); 10 | 11 | const options = { 12 | definition: { 13 | openapi: "3.1.0", 14 | info: { 15 | title: "Map of Pi API Documentation", 16 | version: "1.0.0", 17 | description: "Map of Pi is a mobile application developed to help Pi community members easily locate local businesses that accept Pi as payment. This Swagger documentation provides comprehensive details on the Map of Pi API, including endpoints + request and response structures.", 18 | contact: { 19 | name: "Map of Pi Team", 20 | email: "philip@mapofpi.com" 21 | }, 22 | }, 23 | servers: [ 24 | { 25 | url: "http://localhost:8001/", 26 | description: "Development server", 27 | }, 28 | { 29 | url: env.PRODUCTION_URL, 30 | description: "Production server", 31 | }, 32 | ], 33 | components: { 34 | securitySchemes: { 35 | bearerAuth: { 36 | type: "http", 37 | scheme: "bearer", 38 | bearerFormat: "JWT", 39 | } 40 | } 41 | }, 42 | security: [ 43 | { 44 | bearerAuth: [] 45 | } 46 | ] 47 | }, 48 | apis: [ 49 | path.join(__dirname, '../routes/*.{ts,js}'), 50 | path.join(__dirname, '../config/docs/*.yml'), 51 | ] 52 | }; 53 | 54 | const specs = swaggerJsDoc(options); 55 | 56 | docRouter.use("/", serve, setup(specs, { 57 | customCss: '.swagger-ui .opblock .opblock-summary-path-description-wrapper { align-items: center; display: flex; flex-wrap: wrap; gap: 0 10px; padding: 0 10px; width: 100%; }', 58 | customCssUrl: 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.min.css', 59 | swaggerUrl: path.join(__dirname, swaggerUI.getAbsoluteFSPath()) 60 | })); 61 | 62 | export default docRouter; 63 | -------------------------------------------------------------------------------- /src/controllers/admin/reportsController.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | import {reportSanctionedSellers} from "../../services/admin/report.service"; 3 | import logger from "../../config/loggingConfig"; 4 | 5 | export const getSanctionedSellersReport = async (req: Request, res: Response) => { 6 | try { 7 | const sanctionedSellers = await reportSanctionedSellers(); 8 | logger.info(`Sanctioned Sellers Report generated successfully with ${sanctionedSellers.length} sellers identified.`); 9 | return res.status(200).json({ 10 | message: `${sanctionedSellers.length} Sanctioned seller(s) retrieved successfully`, 11 | sanctionedSellers 12 | }); 13 | } catch (error) { 14 | logger.error('An error occurred while generating Sanctioned Sellers Report:', error); 15 | return res.status(500).json({ message: 'An error occurred while generating Sanctioned Sellers Report; please try again later' }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/controllers/admin/restrictionController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import * as restrictionService from "../../services/admin/restriction.service"; 3 | 4 | import logger from "../../config/loggingConfig"; 5 | 6 | export const checkSanctionStatus = async (req: Request, res: Response) => { 7 | const { latitude, longitude } = req.body; 8 | 9 | if (typeof latitude !== 'number' || typeof longitude !== 'number') { 10 | logger.error(`Invalid coordinates provided as ${latitude}, ${longitude}`); 11 | return res.status(400).json({ error: 'Unexpected coordinates provided' }); 12 | } 13 | 14 | try { 15 | const result = await restrictionService.validateSellerLocation(longitude, latitude); 16 | const isSanctioned = !!result; 17 | 18 | const status = isSanctioned ? 'in a sanctioned zone' : 'not in a sanctioned zone'; 19 | logger.info(`User at [${latitude}, ${longitude}] is ${status}.`); 20 | return res.status(200).json({ 21 | message: `Sell center is set within a ${isSanctioned ? 'sanctioned' : 'unsanctioned' } zone`, 22 | isSanctioned 23 | }); 24 | } catch (error) { 25 | logger.error('Failed to get sanctioned status:', error); 26 | return res.status(500).json({ 27 | message: 'An error occurred while checking sanction status; please try again later' 28 | }); 29 | } 30 | }; -------------------------------------------------------------------------------- /src/controllers/admin/toggleController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import * as toggleService from "../../services/admin/toggle.service"; 3 | import { IToggle } from "../../types"; 4 | 5 | import logger from "../../config/loggingConfig"; 6 | 7 | export const getToggles = async (req: Request, res: Response) => { 8 | try { 9 | const existingToggles = await toggleService.getToggles(); 10 | logger.info(`Successfully fetched ${ existingToggles.length } toggles`); 11 | return res.status(200).json(existingToggles); 12 | } catch (error) { 13 | logger.error('Failed to get toggles', error); 14 | return res.status(500).json({ message: 'An error occurred while fetching toggles; please try again later' }); 15 | } 16 | }; 17 | 18 | export const getToggle = async (req: Request, res: Response) => { 19 | const { toggle_name } = req.params; 20 | try { 21 | const currentToggle = await toggleService.getToggleByName(toggle_name); 22 | if (!currentToggle) { 23 | logger.warn(`Toggle with identifier ${toggle_name} not found.`); 24 | return res.status(404).json({ message: "Toggle not found" }); 25 | } 26 | logger.info(`Fetched toggle with identifier ${toggle_name}`); 27 | return res.status(200).json(currentToggle); 28 | } catch (error) { 29 | logger.error(`Failed to get toggle for identifier ${ toggle_name }:`, error); 30 | return res.status(500).json({ message: 'An error occurred while fetching toggle; please try again later' }); 31 | } 32 | }; 33 | 34 | export const addToggle = async (req: Request, res: Response) => { 35 | const { name, enabled, description } = req.body; 36 | try { 37 | const newToggle = await toggleService.addToggle({ name, enabled, description } as IToggle); 38 | logger.info(`Successfully added toggle with identifier ${name}`); 39 | return res.status(201).json({ message: "Toggle successfully added", newToggle }); 40 | } catch (error) { 41 | logger.error(`Failed to add toggle for identifier ${ name }:`, error); 42 | return res.status(500).json({ message: 'An error occurred while adding toggle; please try again later' }); 43 | } 44 | }; 45 | 46 | export const updateToggle = async (req: Request, res: Response) => { 47 | const { name, enabled, description } = req.body; 48 | try { 49 | const updatedToggle = await toggleService.updateToggle(name, enabled, description); 50 | logger.info(`Successfully updated toggle with identifier ${name}`); 51 | return res.status(200).json({ message: "Toggle successfully updated", updatedToggle }); 52 | } catch (error) { 53 | logger.error(`Failed to update toggle for identifier ${ name }:`, error); 54 | return res.status(500).json({ message: 'An error occurred while updating toggle; please try again later' }); 55 | } 56 | }; 57 | 58 | export const deleteToggle = async (req: Request, res: Response) => { 59 | const { toggle_name } = req.params; 60 | try { 61 | const deletedToggle = await toggleService.deleteToggleByName(toggle_name); 62 | if (!deletedToggle) { 63 | logger.warn(`Toggle with identifier ${toggle_name} not found.`); 64 | return res.status(404).json({ message: "Toggle not found" }); 65 | } 66 | logger.info(`Successfully deleted toggle with identifier ${toggle_name}`); 67 | return res.status(200).json({ message: "Toggle successfully deleted", deletedToggle }); 68 | } catch (error) { 69 | logger.error(`Failed to delete toggle for identifier ${ toggle_name }:`, error); 70 | return res.status(500).json({ message: 'An error occurred while deleting toggle; please try again later' }); 71 | } 72 | }; -------------------------------------------------------------------------------- /src/controllers/mapCenterController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | import * as mapCenterService from '../services/mapCenter.service'; 4 | import { IMapCenter } from '../types'; 5 | 6 | import logger from '../config/loggingConfig'; 7 | 8 | export const saveMapCenter = async (req: Request, res: Response) => { 9 | try { 10 | const authUser = req.currentUser; 11 | // early authentication check 12 | if (!authUser) { 13 | logger.warn('User not found; Map Center failed to save'); 14 | return res.status(404).json({ message: 'User not found: Map Center failed to save' }); 15 | } 16 | 17 | const map_center_id = authUser.pi_uid; 18 | const { latitude, longitude, type } = req.body; 19 | const mapCenter = await mapCenterService.createOrUpdateMapCenter(map_center_id, latitude, longitude, type); 20 | logger.info(`${type === 'search' ? 'Search' : 'Sell'} Center saved successfully for user ${map_center_id} with Longitude: ${longitude}, Latitude: ${latitude} `); 21 | 22 | return res.status(200).json({uid: map_center_id, map_center: mapCenter}); 23 | 24 | } catch (error) { 25 | logger.error('Failed to save Map Center:', error); 26 | return res.status(500).json({ message: 'An error occurred while saving the Map Center; please try again later' }); 27 | } 28 | }; 29 | 30 | export const getMapCenter = async (req: Request, res: Response) => { 31 | try { 32 | const map_center_id = req.currentUser?.pi_uid; 33 | const {type} = req.params; 34 | if (map_center_id) { 35 | const mapCenter: IMapCenter | null = await mapCenterService.getMapCenterById(map_center_id, type); 36 | if (!mapCenter) { 37 | logger.warn(`Map Center not found for user ${map_center_id}`); 38 | return res.status(404).json({ message: "Map Center not found" }); 39 | } 40 | logger.info(`Map Center retrieved successfully for user ${map_center_id}`); 41 | return res.status(200).json(mapCenter); 42 | } else { 43 | logger.warn('No user found; cannot retrieve Map Center.'); 44 | return res.status(404).json({ message: "User not found" }); 45 | } 46 | } catch (error) { 47 | logger.error('Failed to retrieve Map Center:', error); 48 | return res.status(500).json({ message: 'An error occurred while getting the Map Center; please try again later' }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/controllers/reviewFeedbackController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | import * as reviewFeedbackService from "../services/reviewFeedback.service"; 4 | import { uploadImage } from "../services/misc/image.service"; 5 | 6 | import logger from "../config/loggingConfig"; 7 | 8 | export const getReviews = async (req: Request, res: Response) => { 9 | const { review_receiver_id } = req.params; 10 | const { searchQuery } = req.query; 11 | 12 | try { 13 | // Call the service with the review_receiver_id and searchQuery 14 | const completeReviews = await reviewFeedbackService.getReviewFeedback( 15 | review_receiver_id, 16 | searchQuery as string 17 | ); 18 | 19 | logger.info(`Retrieved reviews for receiver ID ${review_receiver_id} with search query "${searchQuery ?? 'none'}"`); 20 | return res.status(200).json(completeReviews); 21 | } catch (error) { 22 | logger.error(`Failed to get reviews for receiverID ${review_receiver_id}:`, error); 23 | return res.status(500).json({ message: 'An error occurred while getting reviews; please try again later' }); 24 | } 25 | }; 26 | 27 | export const getSingleReviewById = async (req: Request, res: Response) => { 28 | const { review_id } = req.params; 29 | try { 30 | const associatedReview = await reviewFeedbackService.getReviewFeedbackById(review_id); 31 | if (!associatedReview) { 32 | logger.warn(`Review with ID ${review_id} not found.`); 33 | return res.status(404).json({ message: "Review not found" }); 34 | } 35 | logger.info(`Retrieved review with ID ${review_id}`); 36 | res.status(200).json(associatedReview); 37 | } catch (error) { 38 | logger.error(`Failed to get review for reviewID ${ review_id }:`, error); 39 | return res.status(500).json({ message: 'An error occurred while getting single review; please try again later' }); 40 | } 41 | }; 42 | 43 | export const addReview = async (req: Request, res: Response) => { 44 | try { 45 | const authUser = req.currentUser; 46 | const formData = req.body; 47 | 48 | if (!authUser) { 49 | logger.warn("No authenticated user found for adding review."); 50 | return res.status(401).json({ message: "Unauthorized" }); 51 | } else if (authUser.pi_uid === formData.review_receiver_id) { 52 | logger.warn(`Attempted self review by user ${authUser.pi_uid}`); 53 | return res.status(400).json({ message: "Self review is prohibited" }); 54 | } 55 | 56 | // image file handling 57 | const file = req.file; 58 | const image = file ? await uploadImage(authUser.pi_uid, file, 'review-feedback') : ''; 59 | 60 | const newReview = await reviewFeedbackService.addReviewFeedback(authUser, formData, image); 61 | logger.info(`Added new review by user ${authUser.pi_uid} for receiver ID ${newReview.review_receiver_id}`); 62 | return res.status(200).json({ newReview }); 63 | } catch (error) { 64 | logger.error(`Failed to add review for userID ${ req.currentUser?.pi_uid }:`, error); 65 | return res.status(500).json({ message: 'An error occurred while adding review; please try again later' }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/controllers/sellerController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import * as sellerService from "../services/seller.service"; 3 | import { uploadImage } from "../services/misc/image.service"; 4 | import * as userSettingsService from '../services/userSettings.service'; 5 | import { ISeller } from "../types"; 6 | import logger from "../config/loggingConfig"; 7 | 8 | export const fetchSellersByCriteria = async (req: Request, res: Response) => { 9 | try { 10 | const { bounds, search_query } = req.body; // bounds: [sw_lat, sw_lng, ne_lat, ne_lng] 11 | const userId = req.currentUser?.pi_uid; 12 | const sellers = await sellerService.getAllSellers(bounds, search_query, userId); 13 | 14 | if (!sellers || sellers.length === 0) { 15 | logger.warn(`No sellers found within bounds (${bounds?.sw_lat}, ${bounds?.sw_lng}) to (${bounds?.ne_lat}, ${bounds?.ne_lng}) with ${search_query}`); 16 | return res.status(204).json({ message: "Sellers not found" }); 17 | } 18 | logger.info(`Fetched ${sellers.length} sellers within bounds (${bounds?.sw_lat}, ${bounds?.sw_lng}) to (${bounds?.ne_lat}, ${bounds?.ne_lng}) with ${search_query}`); 19 | return res.status(200).json(sellers); 20 | } catch (error) { 21 | logger.error('Failed to fetch sellers by criteria:', error); 22 | return res.status(500).json({ message: 'An error occurred while fetching sellers; please try again later' }); 23 | } 24 | }; 25 | 26 | export const getSingleSeller = async (req: Request, res: Response) => { 27 | const { seller_id } = req.params; 28 | try { 29 | const currentSeller = await sellerService.getSingleSellerById(seller_id); 30 | if (!currentSeller) { 31 | logger.warn(`Seller with ID ${seller_id} not found.`); 32 | return res.status(404).json({ message: "Seller not found" }); 33 | } 34 | logger.info(`Fetched seller with ID ${seller_id}`); 35 | res.status(200).json(currentSeller); 36 | } catch (error) { 37 | logger.error(`Failed to get single seller for sellerID ${ seller_id }:`, error); 38 | return res.status(500).json({ message: 'An error occurred while getting single seller; please try again later' }); 39 | } 40 | }; 41 | 42 | export const fetchSellerRegistration = async (req: Request, res: Response) => { 43 | try { 44 | if (!req.currentUser || !req.currentSeller) { 45 | logger.warn(`Seller registration not found for user ${req.currentUser?.pi_uid || "NULL"}`); 46 | return res.status(404).json({ message: "Seller registration not found" }); 47 | } 48 | const currentSeller = req.currentSeller; 49 | logger.info(`Fetched seller registration for user ${req.currentUser.pi_uid}`); 50 | res.status(200).json(currentSeller); 51 | } catch (error) { 52 | logger.error('Failed to fetch seller registration:', error); 53 | return res.status(500).json({ message: 'An error occurred while fetching seller registration; please try again later' }); 54 | } 55 | }; 56 | 57 | export const registerSeller = async (req: Request, res: Response) => { 58 | const authUser = req.currentUser; 59 | 60 | // Check if authUser is defined 61 | if (!authUser) { 62 | console.warn('No authenticated user found when trying to register seller.'); 63 | return res.status(401).json({ error: 'Unauthorized' }); 64 | } 65 | 66 | const formData = req.body; 67 | logger.debug('Received formData for registration:', { formData }); 68 | 69 | try { 70 | // Image file handling 71 | const file = req.file; 72 | const image = file ? await uploadImage(authUser.pi_uid, file, 'seller-registration') : ''; 73 | formData.image = image; 74 | 75 | // Register or update seller 76 | const registeredSeller = await sellerService.registerOrUpdateSeller(authUser, formData); 77 | logger.info(`Registered or updated seller for user ${authUser.pi_uid}`); 78 | 79 | // Update UserSettings with email and phone_number 80 | const userSettings = await userSettingsService.addOrUpdateUserSettings(authUser, formData, ''); 81 | logger.debug('UserSettings updated for user:', { pi_uid: authUser.pi_uid }); 82 | 83 | // Send response 84 | return res.status(200).json({ 85 | seller: registeredSeller, 86 | email: userSettings.email, 87 | phone_number: userSettings.phone_number 88 | }); 89 | } catch (error) { 90 | logger.error(`Failed to register seller for userID ${authUser.pi_uid}:`, error); 91 | return res.status(500).json({ 92 | message: 'An error occurred while registering seller; please try again later', 93 | }); 94 | } 95 | }; 96 | 97 | export const deleteSeller = async (req: Request, res: Response) => { 98 | try { 99 | const authUser = req.currentUser; 100 | const deletedSeller = await sellerService.deleteSeller(authUser?.pi_uid); 101 | logger.info(`Deleted seller with ID ${authUser?.pi_uid}`); 102 | res.status(200).json({ message: "Seller deleted successfully", deletedSeller }); 103 | } catch (error) { 104 | logger.error(`Failed to delete seller for userID ${ req.currentUser?.pi_uid }:`, error); 105 | return res.status(500).json({ message: 'An error occurred while deleting seller; please try again later' }); 106 | } 107 | }; 108 | 109 | export const getSellerItems = async (req: Request, res: Response) => { 110 | const { seller_id } = req.params 111 | try { 112 | const items = await sellerService.getAllSellerItems(seller_id); 113 | 114 | if (!items || items.length === 0) { 115 | logger.warn(`No items are found for seller: ${seller_id}`); 116 | return res.status(204).json({ message: 'Seller items not found' }); 117 | } 118 | logger.info(`Fetched ${items.length} items for seller: ${seller_id}`); 119 | return res.status(200).json(items); 120 | } catch (error) { 121 | logger.error('Failed to fetch seller items:', error); 122 | return res.status(500).json({ message: 'An error occurred while fetching seller Items; please try again later' }); 123 | } 124 | }; 125 | 126 | export const addOrUpdateSellerItem = async (req: Request, res: Response) => { 127 | const currentSeller = req.currentSeller as ISeller; 128 | 129 | const formData = req.body; 130 | logger.debug('Received formData for seller item:', { formData }); 131 | 132 | try { 133 | // Image file handling 134 | const file = req.file; 135 | const image = file ? await uploadImage(currentSeller.seller_id, file, 'seller-item') : ''; 136 | formData.image = image; 137 | 138 | logger.debug('Form data being sent:', { formData }); 139 | // Add or update Item 140 | const sellerItem = await sellerService.addOrUpdateSellerItem(currentSeller, formData); 141 | logger.info(`Added/ updated seller item for seller ${currentSeller.seller_id}`); 142 | 143 | // Send response 144 | return res.status(200).json({ 145 | sellerItem: sellerItem, 146 | }); 147 | } catch (error) { 148 | logger.error(`Failed to add or update seller item for userID ${currentSeller.seller_id}:`, error); 149 | return res.status(500).json({ 150 | message: 'An error occurred while adding/ updating seller item; please try again later', 151 | }); 152 | } 153 | }; 154 | 155 | export const deleteSellerItem = async (req: Request, res: Response) => { 156 | try { 157 | const currentSeller = req.currentSeller as ISeller; 158 | 159 | const { item_id } = req.params; 160 | const deletedSellerItem = await sellerService.deleteSellerItem(item_id); 161 | logger.info(`Deleted seller item with ID ${currentSeller.seller_id}`); 162 | res.status(200).json({ message: "Seller item deleted successfully", deletedSellerItem: deletedSellerItem }); 163 | } catch (error) { 164 | logger.error(`Failed to delete seller item for userID ${ req.currentUser?.pi_uid }:`, error); 165 | return res.status(500).json({ message: 'An error occurred while deleting seller item; please try again later' }); 166 | } 167 | }; -------------------------------------------------------------------------------- /src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | import * as jwtHelper from "../helpers/jwt"; 4 | import * as userService from "../services/user.service"; 5 | import { IUser } from "../types"; 6 | 7 | import logger from '../config/loggingConfig'; 8 | 9 | export const authenticateUser = async (req: Request, res: Response) => { 10 | const auth = req.body; 11 | 12 | try { 13 | const user = await userService.authenticate(auth.user); 14 | const token = jwtHelper.generateUserToken(user); 15 | const expiresDate = new Date(Date.now() + 1 * 24 * 60 * 60 * 1000); // 1 day 16 | 17 | logger.info(`User authenticated: ${user.pi_uid}`); 18 | 19 | return res.cookie("token", token, {httpOnly: true, expires: expiresDate, secure: true, priority: "high", sameSite: "lax"}).status(200).json({ 20 | user, 21 | token, 22 | }); 23 | } catch (error) { 24 | logger.error('Failed to authenticate user:', error); 25 | return res.status(500).json({ message: 'An error occurred while authenticating user; please try again later' }); 26 | } 27 | }; 28 | 29 | export const autoLoginUser = async(req: Request, res: Response) => { 30 | try { 31 | const currentUser = req.currentUser; 32 | logger.info(`Auto-login successful for user: ${currentUser?.pi_uid || "NULL"}`); 33 | res.status(200).json(currentUser); 34 | } catch (error) { 35 | logger.error(`Failed to auto-login user for userID ${ req.currentUser?.pi_uid }:`, error); 36 | return res.status(500).json({ message: 'An error occurred while auto-logging the user; please try again later' }); 37 | } 38 | }; 39 | 40 | export const getUser = async(req: Request, res: Response) => { 41 | const { pi_uid } = req.params; 42 | try { 43 | const currentUser: IUser | null = await userService.getUser(pi_uid); 44 | if (!currentUser) { 45 | logger.warn(`User not found with PI_UID: ${pi_uid}`); 46 | return res.status(404).json({ message: "User not found" }); 47 | } 48 | logger.info(`Fetched user with PI_UID: ${pi_uid}`); 49 | res.status(200).json(currentUser); 50 | } catch (error) { 51 | logger.error(`Failed to fetch user for userID ${ pi_uid }:`, error); 52 | return res.status(500).json({ message: 'An error occurred while getting user; please try again later' }); 53 | } 54 | }; 55 | 56 | export const deleteUser = async (req: Request, res: Response) => { 57 | const currentUser = req.currentUser; 58 | try { 59 | const deletedData = await userService.deleteUser(currentUser?.pi_uid); 60 | logger.info(`Deleted user with PI_UID: ${currentUser?.pi_uid}`); 61 | res.status(200).json({ message: "User deleted successfully", deletedData }); 62 | } catch (error) { 63 | logger.error(`Failed to delete user for userID ${ currentUser?.pi_uid }:`, error); 64 | return res.status(500).json({ message: 'An error occurred while deleting user; please try again later' }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/controllers/userPreferencesController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | import * as userSettingsService from "../services/userSettings.service"; 4 | import { uploadImage } from "../services/misc/image.service"; 5 | import { IUserSettings } from "../types"; 6 | 7 | import logger from "../config/loggingConfig"; 8 | 9 | export const getUserPreferences = async (req: Request, res: Response) => { 10 | const { user_settings_id } = req.params; 11 | try { 12 | const userPreferences: IUserSettings | null = await userSettingsService.getUserSettingsById(user_settings_id); 13 | if (!userPreferences) { 14 | logger.warn(`User Preferences not found for ID: ${user_settings_id}`); 15 | return res.status(404).json({ message: "User Preferences not found" }); 16 | } 17 | logger.info(`Fetched User Preferences for ID: ${user_settings_id}`); 18 | res.status(200).json(userPreferences); 19 | } catch (error) { 20 | logger.error(`Failed to fetch user preferences for userSettingsID ${ user_settings_id }:`, error); 21 | return res.status(500).json({ message: 'An error occurred while getting user preferences; please try again later' }); 22 | } 23 | }; 24 | 25 | export const fetchUserPreferences = async (req: Request, res: Response) => { 26 | try { 27 | const currentUserPreferences = req.currentUserSettings; 28 | if (!req.currentUser || !currentUserPreferences) { 29 | logger.warn(`User Preferences not found for user with ID: ${req.currentUser?.pi_uid || "NULL"}`); 30 | return res.status(404).json({ message: "User Preferences not found" }); 31 | } 32 | logger.info(`Fetched User Preferences for user with ID: ${req.currentUser.pi_uid}`); 33 | res.status(200).json(currentUserPreferences); 34 | 35 | } catch (error) { 36 | logger.error(`Failed to fetch user preferences for userID ${ req.currentUser?.pi_uid }:`, error); 37 | return res.status(500).json({ message: 'An error occurred while fetching user preferences; please try again later' }); 38 | } 39 | }; 40 | 41 | export const addUserPreferences = async (req: Request, res: Response) => { 42 | try { 43 | const authUser = req.currentUser 44 | const formData = req.body; 45 | 46 | if (!authUser) { 47 | logger.warn("No authenticated user found for user preferences."); 48 | return res.status(401).json({ message: "Unauthorized user" }); 49 | } 50 | 51 | // image file handling 52 | const file = req.file; 53 | const image = file ? await uploadImage(authUser.pi_uid, file, 'user-preferences') : ''; 54 | 55 | const userPreferences = await userSettingsService.addOrUpdateUserSettings(authUser, formData, image); 56 | logger.info(`Added or updated User Preferences for user with ID: ${authUser.pi_uid}`); 57 | res.status(200).json({ settings: userPreferences }); 58 | } catch (error) { 59 | logger.error(`Failed to add or update user preferences for userID ${ req.currentUser?.pi_uid }:`, error); 60 | return res.status(500).json({ message: 'An error occurred while adding or updating user preferences; please try again later' }); 61 | } 62 | }; 63 | 64 | export const deleteUserPreferences = async (req: Request, res: Response) => { 65 | const { user_settings_id: user_settings_id } = req.params; 66 | try { 67 | const deletedUserSettings = await userSettingsService.deleteUserSettings(user_settings_id); 68 | logger.info(`Deleted user preferences with ID ${user_settings_id}`); 69 | res.status(200).json({ message: "User Preferences deleted successfully", deletedUserSettings: deletedUserSettings }); 70 | } catch (error) { 71 | logger.error(`Failed to delete user preferences for userSettingsID ${ user_settings_id }:`, error); 72 | return res.status(500).json({ message: 'An error occurred while deleting user preferences; please try again later' }); 73 | } 74 | }; 75 | 76 | export const getUserLocation = async (req: Request, res: Response) => { 77 | let location: { lat: number; lng: number } | null; 78 | 79 | try { 80 | const authUser = req.currentUser; 81 | const zoom = 13; 82 | if (!authUser?.pi_uid) { 83 | logger.warn(`User not authenticated`); 84 | return res.status(401).json({ message: "Unauthorized" }); 85 | } 86 | 87 | location = await userSettingsService.userLocation(authUser.pi_uid); 88 | if (!location) { 89 | logger.warn(`User location not found for piUID: ${authUser.pi_uid}`); 90 | return res.status(404).json({ message: "User location not found for piUID: " + authUser.pi_uid }); 91 | } 92 | logger.info('User location from backend:', location) 93 | return res.status(200).json({ origin: location, zoom: zoom }); 94 | 95 | } catch (error) { 96 | logger.error(`Failed to get user location for piUID ${ req.currentUser?.pi_uid }:`, error); 97 | return res.status(500).json({ message: 'An error occurred while getting user location; please try again later' }); 98 | } 99 | }; -------------------------------------------------------------------------------- /src/cron/index.ts: -------------------------------------------------------------------------------- 1 | import schedule from "node-schedule"; 2 | import { runSanctionBot } from "./jobs/sanctionBot.job"; 3 | import logger from "../config/loggingConfig"; 4 | 5 | export const scheduleCronJobs = () => { 6 | logger.info("Initializing scheduled cron jobs..."); 7 | 8 | // Run the Sanction Bot job daily at 22:00 UTC 9 | const sanctionJobTime = '0 0 22 * * *'; 10 | 11 | schedule.scheduleJob(sanctionJobTime, async () => { 12 | logger.info('🕒 Sanction Bot job triggered (22:00 UTC).'); 13 | 14 | try { 15 | await runSanctionBot(); 16 | logger.info("✅ Sanction Bot job completed successfully."); 17 | } catch (error) { 18 | logger.error("❌ Sanction Bot job failed:", error); 19 | } 20 | }); 21 | 22 | logger.info("✅ All cron jobs have been scheduled."); 23 | }; 24 | -------------------------------------------------------------------------------- /src/cron/jobs/sanctionBot.job.ts: -------------------------------------------------------------------------------- 1 | import Seller from "../../models/Seller"; 2 | import { SanctionedSellerStatus } from "../../types"; 3 | import { getAllSanctionedRegions } from "../../services/admin/report.service"; 4 | import { 5 | createBulkPreRestrictionOperation, 6 | createGeoQueries 7 | } from "../utils/geoUtils"; 8 | import { 9 | getSellersToEvaluate, 10 | processSellersGeocoding, 11 | processSanctionedSellers, 12 | processUnsanctionedSellers 13 | } from "../utils/sanctionUtils"; 14 | import logger from "../../config/loggingConfig"; 15 | 16 | export async function runSanctionBot(): Promise { 17 | logger.info('Sanction Bot cron job started.'); 18 | 19 | try { 20 | /* Step 1: Reset the 'isPreRestricted' field to 'false' for all sellers 21 | This clears any pre-existing restrictions before applying new ones. */ 22 | await Seller.updateMany({}, { isPreRestricted: false }).exec(); 23 | logger.info('Reset [isPreRestricted] for all sellers.'); 24 | 25 | /* Step 2: Get the list of all sanctioned regions */ 26 | const sanctionedRegions = await getAllSanctionedRegions(); 27 | // If no sanctioned regions are found, log the info and exit the job 28 | if (!sanctionedRegions.length) { 29 | logger.info('No sanctioned regions found. Exiting job.'); 30 | return; 31 | } 32 | 33 | /* Step 3: Create geo-based queries and identify sellers to evaluate */ 34 | const geoQueries = createGeoQueries(sanctionedRegions); 35 | const sellersToEvaluate = await getSellersToEvaluate(geoQueries); 36 | logger.info(`Evaluating ${sellersToEvaluate.length} sellers flagged or currently Restricted.`); 37 | 38 | /* Step 4: Create the bulk update operations to mark sellers as pre-restricted */ 39 | const bulkPreRestrictionOps = createBulkPreRestrictionOperation(sellersToEvaluate); 40 | if (bulkPreRestrictionOps.length > 0) { 41 | await Seller.bulkWrite(bulkPreRestrictionOps); 42 | logger.info(`Marked ${bulkPreRestrictionOps.length} sellers as Pre-Restricted`) 43 | } 44 | 45 | /* Step 5: Retrieve all sellers who are marked as pre-restricted */ 46 | const preRestrictedSellers = await Seller.find({isPreRestricted: true}).exec(); 47 | logger.info(`${preRestrictedSellers.length} sellers are Pre-Restricted`); 48 | 49 | /* Step 6: Process geocoding validation */ 50 | const results: SanctionedSellerStatus[] = await processSellersGeocoding( 51 | preRestrictedSellers, 52 | sanctionedRegions 53 | ); 54 | const inZone = results.filter(r => r.isSanctionedRegion); 55 | const outOfZone = results.filter(r => !r.isSanctionedRegion); 56 | 57 | /* Step 7: Apply restrictions of in-zone sellers or restoration of out-zone sellers */ 58 | await processSanctionedSellers(inZone); 59 | await processUnsanctionedSellers(outOfZone); 60 | 61 | /* Step 8: Clean up temp pre-restriction flags */ 62 | await Seller.updateMany({isPreRestricted: true}, {isPreRestricted: false}).exec(); 63 | logger.info('SanctionBot job completed.'); 64 | } catch (error) { 65 | logger.error('Error in Sanction Bot cron job:', error); 66 | } 67 | } -------------------------------------------------------------------------------- /src/cron/utils/geoUtils.ts: -------------------------------------------------------------------------------- 1 | import { SellerType } from "../../models/enums/sellerType"; 2 | import { ISanctionedRegion, ISeller } from "../../types"; 3 | 4 | export function createBulkPreRestrictionOperation(sellersToEvaluate: ISeller[]) { 5 | return sellersToEvaluate.map(seller => { 6 | // Create a single update object for $set that includes both fields. 7 | const setObj: any = { isPreRestricted: true }; 8 | if (seller.seller_type !== SellerType.Restricted) { 9 | // Copy seller type value into pre_restriction_seller_type field. 10 | setObj.pre_restriction_seller_type = seller.seller_type; 11 | } 12 | return { 13 | updateOne: { 14 | filter: { seller_id: seller.seller_id }, 15 | update: { $set: setObj }, 16 | } 17 | }; 18 | }); 19 | } 20 | 21 | export function createGeoQueries(sanctionedRegions: ISanctionedRegion[]) { 22 | return sanctionedRegions.map(region => ({ 23 | sell_map_center: { 24 | $geoWithin: { 25 | $geometry: region.boundary, 26 | } 27 | } 28 | })); 29 | } -------------------------------------------------------------------------------- /src/cron/utils/sanctionUtils.ts: -------------------------------------------------------------------------------- 1 | import Seller from "../../models/Seller"; 2 | import { SellerType } from "../../models/enums/sellerType"; 3 | import { ISanctionedRegion, ISeller, SanctionedSellerStatus } from "../../types"; 4 | import { processSellerGeocoding } from "../../services/admin/report.service"; 5 | import logger from "../../config/loggingConfig"; 6 | 7 | interface GeoQuery { 8 | sell_map_center: { 9 | $geoWithin: { 10 | $geometry: { 11 | type: "Polygon"; 12 | coordinates: [[[number, number]]]; 13 | }; 14 | }; 15 | }; 16 | } 17 | 18 | export async function getSellersToEvaluate(geoQueries: GeoQuery[]) { 19 | return await Seller.find({ 20 | $or: [ 21 | ...geoQueries, 22 | { seller_type: SellerType.Restricted } 23 | ] 24 | }).exec(); 25 | } 26 | 27 | export async function processSellersGeocoding( 28 | flaggedSellers: ISeller[], 29 | sanctionedRegions: ISanctionedRegion[] 30 | ): Promise { 31 | return await Promise.all( 32 | flaggedSellers.map(async seller => { 33 | for (const region of sanctionedRegions) { 34 | const match = await processSellerGeocoding(seller, region.location); 35 | if (match) return { 36 | seller_id: match.seller_id, 37 | pre_restriction_seller_type: match.pre_restriction_seller_type ?? null, 38 | isSanctionedRegion: true 39 | }; 40 | } 41 | return { 42 | seller_id: seller.seller_id, 43 | pre_restriction_seller_type: seller.pre_restriction_seller_type ?? null, 44 | isSanctionedRegion: false 45 | }; 46 | }) 47 | ); 48 | } 49 | 50 | export async function processSanctionedSellers(inZone: SanctionedSellerStatus[]) { 51 | if (!inZone.length) return; 52 | 53 | await Seller.bulkWrite( 54 | inZone.map(r => ({ 55 | updateOne: { 56 | filter: {seller_id: r.seller_id}, 57 | update: { 58 | $set: 59 | { 60 | seller_type: SellerType.Restricted, 61 | pre_restriction_seller_type: r.pre_restriction_seller_type 62 | } 63 | } 64 | } 65 | })) 66 | ); 67 | logger.info(`Restricted ${inZone.length} sanctioned sellers.`); 68 | } 69 | 70 | export async function processUnsanctionedSellers(outOfZone: SanctionedSellerStatus[]) { 71 | if (!outOfZone.length) return; 72 | 73 | const result = await Seller.bulkWrite( 74 | outOfZone.map(s => ({ 75 | updateOne: { 76 | filter: {seller_id: s.seller_id}, 77 | update: { 78 | $set: { 79 | seller_type: s.pre_restriction_seller_type ?? SellerType.Test, 80 | pre_restriction_seller_type: null 81 | }, 82 | } 83 | } 84 | })) 85 | ); 86 | logger.info(`Restored ${outOfZone.length} sellers.`); 87 | } -------------------------------------------------------------------------------- /src/helpers/imageUploader.ts: -------------------------------------------------------------------------------- 1 | import cloudinary from "../utils/cloudinary"; 2 | 3 | import logger from '../config/loggingConfig'; 4 | 5 | export const uploadMultipleImages = async (files: any) => { 6 | try { 7 | const uploadedImages = []; 8 | logger.info(`Starting upload of ${files.length} images`); 9 | 10 | for (const file of files) { 11 | const result = await cloudinary.uploader.upload(file.path, { 12 | folder: "uploads", 13 | use_filename: true, 14 | }); 15 | logger.debug(`Uploaded image: ${result.secure_url}`); 16 | uploadedImages.push(result.secure_url); 17 | } 18 | logger.info(`Successfully uploaded ${uploadedImages.length} images`); 19 | return uploadedImages; 20 | } catch (error) { 21 | logger.error('Failed to upload multiple images to Cloudinary:', error); 22 | throw new Error('Failed to upload images; please try again'); 23 | } 24 | }; 25 | 26 | export const uploadSingleImage = async (file: any) => { 27 | try { 28 | logger.info(`Starting upload of single image: ${file.path}`); 29 | const result = await cloudinary.uploader.upload(file.path, { 30 | folder: "uploads", 31 | use_filename: true, 32 | }); 33 | logger.info(`Successfully uploaded single image: ${result.secure_url}`); 34 | return result.secure_url; 35 | } catch (error) { 36 | logger.error(`Failed to upload single image to Cloudinary:`, error); 37 | throw new Error('Failed to upload single image; please try again'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/helpers/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | import { IUser } from "../types"; 4 | import User from "../models/User"; 5 | import { env } from "../utils/env"; 6 | 7 | import logger from '../config/loggingConfig'; 8 | 9 | export const generateUserToken = (user: IUser) => { 10 | try { 11 | logger.info(`Generating token for user: ${user.pi_uid}`); 12 | const token = jwt.sign({ userId: user.pi_uid, _id: user._id }, env.JWT_SECRET, { 13 | expiresIn: "1d", // 1 day 14 | }); 15 | logger.info(`Successfully generated token for user: ${user.pi_uid}`); 16 | return token; 17 | } catch (error) { 18 | logger.error(`Failed to generate user token for piUID ${ user.pi_uid }:`, error); 19 | throw new Error('Failed to generate user token; please try again'); 20 | } 21 | }; 22 | 23 | export const decodeUserToken = async (token: string) => { 24 | try { 25 | logger.info(`Decoding token.`); 26 | const decoded = jwt.verify(token, env.JWT_SECRET) as { userId: string }; 27 | if (!decoded.userId) { 28 | logger.warn(`Invalid token: Missing userID.`); 29 | throw new Error("Invalid token: Missing userID."); 30 | } 31 | logger.info(`Finding user associated with token: ${decoded.userId}`); 32 | const associatedUser = await User.findOne({pi_uid: decoded.userId}); 33 | if (!associatedUser) { 34 | logger.warn(`User not found for token: ${decoded.userId}`); 35 | throw new Error("User not found."); 36 | } 37 | logger.info(`Successfully decoded token and found user: ${associatedUser.pi_uid}`); 38 | return associatedUser; 39 | } catch (error) { 40 | logger.error('Failed to decode user token:', error); 41 | throw new Error('Failed to decode user token; please try again'); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/helpers/location.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // Geocode using the Nominatim API 4 | export const reverseLocationDetails = async (latitude: number, longitude: number) =>{ 5 | return await axios.get("https://nominatim.openstreetmap.org/reverse", { 6 | headers: { 7 | "User-Agent": "mapofpi/1.0 (mapofpi@gmail.com)" 8 | }, 9 | params: { 10 | lat: latitude, 11 | lon: longitude, 12 | zoom: 6, 13 | format: "jsonv2", 14 | "accept-language": "en" 15 | } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | 3 | import { scheduleCronJobs } from "./cron"; 4 | import "./config/sentryConnection"; 5 | import { connectDB } from "./config/dbConnection"; 6 | import app from "./utils/app"; 7 | import { env } from "./utils/env"; 8 | import logger from "./config/loggingConfig"; 9 | 10 | dotenv.config(); 11 | 12 | const startServer = async () => { 13 | logger.info("Initiating server setup..."); 14 | try { 15 | // Establish connection to MongoDB 16 | await connectDB(); 17 | 18 | // In a non-serverless environment, start the server 19 | if (env.NODE_ENV !== 'production') { 20 | await new Promise((resolve) => { 21 | // Start listening on the specified port 22 | app.listen(env.PORT, () => { 23 | logger.info(`Server is running on port ${env.PORT}`); 24 | resolve(); 25 | }); 26 | }); 27 | } 28 | 29 | 30 | 31 | logger.info("Server setup initiated."); 32 | } catch (error) { 33 | logger.error('Server failed to initialize:', error); 34 | } 35 | }; 36 | 37 | // Start the server setup process 38 | startServer(); 39 | // Start the scheduled cron job(s) 40 | scheduleCronJobs(); 41 | 42 | export default app; -------------------------------------------------------------------------------- /src/middlewares/isPioneerFound.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | import { platformAPIClient } from "../config/platformAPIclient"; 4 | import logger from '../config/loggingConfig'; 5 | 6 | export const isPioneerFound = async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | const authHeader = req.headers.authorization; 12 | const tokenFromHeader = authHeader && authHeader.split(" ")[1]; 13 | 14 | try { 15 | logger.info("Verifying user's access token with the /me endpoint."); 16 | // Verify the user's access token with the /me endpoint: 17 | const me = await platformAPIClient.get(`/v2/me`, { 18 | headers: { 'Authorization': `Bearer ${ tokenFromHeader }` } 19 | }); 20 | 21 | if (me && me.data) { 22 | const user = { 23 | pi_uid: me.data.uid, 24 | pi_username: me.data.username, 25 | user_name: me.data.username 26 | } 27 | req.body.user = user; 28 | logger.info(`Pioneer found: ${user.pi_uid} - ${user.pi_username}`); 29 | return next(); 30 | } else { 31 | logger.warn("Pioneer not found."); 32 | return res.status(404).json({message: "Pioneer not found"}); 33 | } 34 | } catch (error) { 35 | logger.error('Failed to identify pioneer:', error); 36 | res.status(500).json({ message: 'Failed to identify | pioneer not found; please try again later'}); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/middlewares/isSellerFound.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | import Seller from "../models/Seller"; 4 | import { ISeller } from "../types"; 5 | 6 | import logger from '../config/loggingConfig'; 7 | 8 | declare module 'express-serve-static-core' { 9 | interface Request { 10 | currentSeller: ISeller; 11 | } 12 | } 13 | 14 | export const isSellerFound = async ( 15 | req: Request, 16 | res: Response, 17 | next: NextFunction 18 | ) => { 19 | const seller_id = req.currentUser?.pi_uid; 20 | 21 | try { 22 | logger.info(`Checking if seller exists for user ID: ${seller_id}`); 23 | const currentSeller: ISeller | null = await Seller.findOne({seller_id}); 24 | 25 | if (currentSeller) { 26 | req.currentSeller = currentSeller; 27 | logger.info(`Seller found: ${currentSeller._id}`); 28 | return next(); 29 | } else { 30 | logger.warn(`Seller not found for user ID: ${seller_id}`); 31 | return res.status(404).json({message: "Seller not found"}); 32 | } 33 | } catch (error) { 34 | logger.error('Failed to identify seller:', error); 35 | res.status(500).json({ message: 'Failed to identify | seller not found; please try again later'}); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/middlewares/isToggle.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import Toggle from "../models/misc/Toggle"; 3 | import logger from '../config/loggingConfig'; 4 | 5 | export const isToggle = (toggleName: string) => async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | try { 11 | const toggle = await Toggle.findOne({ name: toggleName }); 12 | 13 | if (!toggle || !toggle.enabled) { 14 | return res.status(403).json({ 15 | message: "Feature is currently disabled", 16 | }); 17 | } 18 | 19 | return next(); 20 | } catch (error) { 21 | logger.error(`Failed to fetch toggle ${toggleName}:`, error); 22 | return res.status(500).json({ 23 | message: 'Failed to determine feature state; please try again later' 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/middlewares/isUserSettingsFound.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | import UserSettings from "../models/UserSettings"; 4 | import { IUserSettings } from "../types"; 5 | 6 | import logger from '../config/loggingConfig' 7 | 8 | declare module 'express-serve-static-core' { 9 | interface Request { 10 | currentUserSettings: IUserSettings; 11 | } 12 | } 13 | 14 | export const isUserSettingsFound = async ( 15 | req: Request, 16 | res: Response, 17 | next: NextFunction 18 | ) => { 19 | const userSettingsId = req.currentUser?.pi_uid; 20 | 21 | try { 22 | logger.info(`Checking if user settings exist for user ID: ${userSettingsId}`); 23 | const currentUserSettings: IUserSettings | null = await UserSettings.findOne({user_settings_id: userSettingsId}); 24 | 25 | if (currentUserSettings) { 26 | req.currentUserSettings = currentUserSettings; 27 | logger.info(`User settings found for user ID: ${userSettingsId}`); 28 | return next(); 29 | } else { 30 | logger.warn(`User settings not found for user ID: ${userSettingsId}`); 31 | return res.status(404).json({message: "User Settings not found"}); 32 | } 33 | } catch (error) { 34 | logger.error('Failed to identify user settings:', error); 35 | res.status(500).json({ message: 'Failed to identify | user settings not found; please try again later'}); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/middlewares/logger.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | import logger from '../config/loggingConfig'; 4 | 5 | const requestLogger = ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ): void => { 10 | logger.info(`Endpoint: ${req.method} ${req.originalUrl}`); 11 | logger.debug("Request Body:", req.body); 12 | return next(); 13 | }; 14 | 15 | export default requestLogger; 16 | -------------------------------------------------------------------------------- /src/middlewares/verifyToken.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { decodeUserToken } from "../helpers/jwt"; 3 | import { IUser } from "../types"; 4 | 5 | import logger from "../config/loggingConfig"; 6 | 7 | declare module 'express-serve-static-core' { 8 | interface Request { 9 | currentUser?: IUser; 10 | token?: string; 11 | } 12 | } 13 | 14 | export const verifyToken = async ( 15 | req: Request, 16 | res: Response, 17 | next: NextFunction 18 | ) => { 19 | // First, checking if the token exists in the cookies 20 | const tokenFromCookie = req.cookies.token; 21 | 22 | // Fallback to the authorization header if token is not in the cookie 23 | const authHeader = req.headers.authorization; 24 | const tokenFromHeader = authHeader && authHeader.split(" ")[1]; 25 | 26 | // Prioritize token from cookies, then from header 27 | const token = tokenFromCookie || tokenFromHeader; 28 | 29 | if (!token) { 30 | logger.warn("Authentication token is missing."); 31 | return res.status(401).json({ message: "Unauthorized" }); 32 | } 33 | 34 | try { 35 | // Decode the token to get the user information 36 | const currentUser = await decodeUserToken(token); 37 | 38 | if (!currentUser) { 39 | logger.warn("Authentication token is invalid or expired."); 40 | return res.status(401).json({ message: "Unauthorized" }); 41 | } 42 | 43 | // Attach currentUser to the request object 44 | req.currentUser = currentUser; 45 | req.token = token; 46 | next(); 47 | } catch (error) { 48 | logger.error('Failed to verify token:', error); 49 | return res.status(500).json({ message: 'Failed to verify token; please try again later' }); 50 | } 51 | }; 52 | 53 | export const verifyAdminToken = ( 54 | req: Request, 55 | res: Response, 56 | next: NextFunction 57 | ) => { 58 | const { ADMIN_API_USERNAME, ADMIN_API_PASSWORD } = process.env; 59 | 60 | const authHeader = req.headers.authorization; 61 | const base64Credentials = authHeader && authHeader.split(" ")[1]; 62 | if (!base64Credentials) { 63 | logger.warn("Admin credentials are missing."); 64 | return res.status(401).json({ message: "Unauthorized" }); 65 | } 66 | 67 | const credentials = Buffer.from(base64Credentials, "base64").toString("ascii"); 68 | const [username, password] = credentials.split(":"); 69 | 70 | if (username !== ADMIN_API_USERNAME || password !== ADMIN_API_PASSWORD) { 71 | logger.warn("Admin credentials are invalid."); 72 | return res.status(401).json({ message: "Unauthorized" }); 73 | } 74 | 75 | logger.info("Admin credentials verified successfully."); 76 | next(); 77 | }; 78 | -------------------------------------------------------------------------------- /src/models/ReviewFeedback.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | import { IReviewFeedback } from "../types"; 4 | 5 | import { RatingScale } from "./enums/ratingScale"; 6 | 7 | const reviewFeedbackSchema = new Schema( 8 | { 9 | review_receiver_id: { 10 | type: String, 11 | required: true 12 | }, 13 | review_giver_id: { 14 | type: String, 15 | required: true 16 | }, 17 | reply_to_review_id: { 18 | type: String, 19 | required: false, 20 | default: null 21 | }, 22 | rating: { 23 | type: Number, 24 | enum: Object.values(RatingScale).filter(value => typeof value === 'number'), 25 | required: true 26 | }, 27 | comment: { 28 | type: String, 29 | required: false 30 | }, 31 | image: { 32 | type: String, 33 | required: false 34 | }, 35 | review_date: { 36 | type: Date, 37 | required: true 38 | } 39 | }, 40 | ); 41 | 42 | const ReviewFeedback = mongoose.model("Review-Feedback", reviewFeedbackSchema); 43 | 44 | export default ReviewFeedback; 45 | -------------------------------------------------------------------------------- /src/models/Seller.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Types } from "mongoose"; 2 | 3 | import { ISeller } from "../types"; 4 | import { SellerType } from "./enums/sellerType"; 5 | import { FulfillmentType } from "./enums/fulfillmentType"; 6 | 7 | const sellerSchema = new Schema( 8 | { 9 | seller_id: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | name: { 15 | type: String, 16 | required: true, 17 | }, 18 | seller_type: { 19 | type: String, 20 | enum: Object.values(SellerType).filter(value => typeof value === 'string'), 21 | required: true, 22 | default: SellerType.Test, 23 | }, 24 | description: { 25 | type: String, 26 | required: false, 27 | }, 28 | image: { 29 | type: String, 30 | required: false, 31 | }, 32 | address: { 33 | type: String, 34 | required: false, 35 | }, 36 | average_rating: { 37 | type: Types.Decimal128, 38 | required: true, 39 | default: 5.0, 40 | }, 41 | sell_map_center: { 42 | type: { 43 | type: String, 44 | enum: ['Point'], 45 | required: true, 46 | default: 'Point', 47 | }, 48 | coordinates: { 49 | type: [Number], 50 | required: true, 51 | default: [0, 0] 52 | }, 53 | }, 54 | order_online_enabled_pref: { 55 | type: Boolean, 56 | required: false, 57 | }, 58 | fulfillment_method: { 59 | type: String, 60 | enum: Object.values(FulfillmentType).filter(value => typeof value === 'string'), 61 | default: FulfillmentType.CollectionByBuyer 62 | }, 63 | fulfillment_description: { 64 | type: String, 65 | default: null, 66 | required: false 67 | }, 68 | pre_restriction_seller_type: { 69 | type: String, 70 | enum: Object.values(SellerType).filter(value => typeof value === 'string'), 71 | required: false, 72 | default: null 73 | }, 74 | isPreRestricted: { 75 | type: Boolean, 76 | default: false, 77 | required: false 78 | } 79 | }, 80 | { timestamps: true } // Adds timestamps to track creation and update times 81 | ); 82 | 83 | // Creating a text index on the 'name' and 'description' fields 84 | sellerSchema.index({ 'name': 'text', 'description': 'text' }); 85 | 86 | // Creating a 2dsphere index for the sell_map_center field 87 | sellerSchema.index({ 'sell_map_center.coordinates': '2dsphere' }); 88 | sellerSchema.index({ 'sell_map_center': '2dsphere', 'updatedAt': -1 }); 89 | 90 | // Creating the Seller model from the schema 91 | const Seller = mongoose.model("Seller", sellerSchema); 92 | 93 | export default Seller; 94 | -------------------------------------------------------------------------------- /src/models/SellerItem.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Types } from "mongoose"; 2 | 3 | import { ISellerItem } from "../types"; 4 | import { StockLevelType } from "./enums/stockLevelType"; 5 | 6 | const sellerItemSchema = new Schema( 7 | { 8 | seller_id: { 9 | type: String, 10 | required: true, 11 | }, 12 | name: { 13 | type: String, 14 | required: true, 15 | }, 16 | description: { 17 | type: String, 18 | default: null 19 | }, 20 | price: { 21 | type: Types.Decimal128, 22 | required: true, 23 | default: 0.01 24 | }, 25 | stock_level: { 26 | type: String, 27 | enum: Object.values(StockLevelType).filter(value => typeof value === 'string') 28 | }, 29 | image: { 30 | type: String, 31 | required: false, 32 | default: null 33 | }, 34 | duration: { 35 | type: Number, 36 | default: 1, 37 | min: 1 38 | }, 39 | expired_by: { 40 | type: Date, 41 | required: true, 42 | } 43 | }, 44 | { 45 | timestamps: true, // Enables createdAt and updatedAt 46 | } 47 | ); 48 | 49 | sellerItemSchema.index({ seller_id: 1 }); 50 | 51 | // Creating the Seller model from the schema 52 | const SellerItem = mongoose.model("Seller-Item", sellerItemSchema); 53 | 54 | export default SellerItem; 55 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | import { IUser } from "../types"; 4 | 5 | const userSchema = new Schema( 6 | { 7 | pi_uid: { 8 | type: String, 9 | required: true, 10 | unique: true, 11 | }, 12 | pi_username: { 13 | type: String, 14 | required: true, 15 | }, 16 | user_name: { 17 | type: String, 18 | required: true, 19 | } 20 | } 21 | ); 22 | 23 | const User = mongoose.model("User", userSchema); 24 | 25 | export default User; 26 | -------------------------------------------------------------------------------- /src/models/UserSettings.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | import { IUserSettings } from "../types"; 4 | import { DeviceLocationType } from "./enums/deviceLocationType"; 5 | import { TrustMeterScale } from "./enums/trustMeterScale"; 6 | 7 | const userSettingsSchema = new Schema( 8 | { 9 | user_settings_id: { 10 | type: String, 11 | required: true, 12 | unique: true 13 | }, 14 | user_name: { 15 | type: String, 16 | required: true, 17 | }, 18 | email: { 19 | type: String, 20 | required: false, 21 | default: null, 22 | }, 23 | phone_number: { 24 | type: String, 25 | required: false, 26 | default: null, 27 | }, 28 | image: { 29 | type: String, 30 | required: false, 31 | default: '' 32 | }, 33 | findme: { 34 | type: String, 35 | enum: Object.values(DeviceLocationType).filter(value => typeof value === 'string'), 36 | required: true, 37 | default: DeviceLocationType.SearchCenter 38 | }, 39 | trust_meter_rating: { 40 | type: Number, 41 | enum: Object.values(TrustMeterScale).filter(value => typeof value === 'number'), 42 | required: true, 43 | default: TrustMeterScale.HUNDRED 44 | }, 45 | search_map_center: { 46 | type: { 47 | type: String, 48 | enum: ['Point'], 49 | required: false, 50 | default: 'Point', 51 | }, 52 | coordinates: { 53 | type: [Number], 54 | required: false, 55 | default: [0, 0] 56 | }, 57 | }, 58 | search_filters: { 59 | type: { 60 | include_active_sellers: { type: Boolean, default: true }, 61 | include_inactive_sellers: { type: Boolean, default: false }, 62 | include_test_sellers: { type: Boolean, default: false }, 63 | include_trust_level_100: { type: Boolean, default: true }, 64 | include_trust_level_80: { type: Boolean, default: true }, 65 | include_trust_level_50: { type: Boolean, default: true }, 66 | include_trust_level_0: { type: Boolean, default: false }, 67 | }, 68 | required: true, 69 | default: {}, 70 | }, 71 | } 72 | ); 73 | 74 | // use GeoJSON format to store geographical data i.e., points using '2dsphere' index. 75 | userSettingsSchema.index({ search_map_center: '2dsphere' }); 76 | 77 | const UserSettings = mongoose.model("User-Settings", userSettingsSchema); 78 | 79 | export default UserSettings; 80 | -------------------------------------------------------------------------------- /src/models/enums/deviceLocationType.ts: -------------------------------------------------------------------------------- 1 | export enum DeviceLocationType { 2 | Automatic = 'auto', 3 | GPS = 'deviceGPS', 4 | SearchCenter = 'searchCenter' 5 | } 6 | -------------------------------------------------------------------------------- /src/models/enums/fulfillmentType.ts: -------------------------------------------------------------------------------- 1 | export enum FulfillmentType { 2 | CollectionByBuyer = 'Collection by buyer', 3 | DeliveredToBuyer = 'Delivered to buyer' 4 | } 5 | -------------------------------------------------------------------------------- /src/models/enums/ratingScale.ts: -------------------------------------------------------------------------------- 1 | export enum RatingScale { 2 | DESPAIR = 0, 3 | SAD = 2, 4 | OKAY = 3, 5 | HAPPY = 4, 6 | DELIGHT = 5 7 | } 8 | -------------------------------------------------------------------------------- /src/models/enums/restrictedArea.ts: -------------------------------------------------------------------------------- 1 | export enum RestrictedArea { 2 | CUBA = "Cuba", 3 | IRAN = "Iran", 4 | NORTH_KOREA = "North Korea", 5 | SYRIA = "Syria", 6 | REPUBLIC_OF_CRIMEA = "Republic of Crimea", 7 | DONETSK_OBLAST = "Donetsk Oblast", 8 | LUHANSK_OBLAST = "Luhansk Oblast", 9 | RUSSIA = "Russia" 10 | } 11 | 12 | export const RestrictedAreaBoundaries = { 13 | [RestrictedArea.CUBA]: { 14 | type: "Polygon", 15 | // (longitude, latitude) 16 | coordinates: [[ 17 | [-85.3, 19.4], // bottom-left corner 18 | [-73.8, 19.4], // bottom-right corner 19 | [-73.8, 23.7], // top-right corner 20 | [-85.3, 23.7], // top-left corner 21 | [-85.3, 19.4], // close the polygon shape 22 | ]], 23 | }, 24 | [RestrictedArea.IRAN]: { 25 | type: "Polygon", 26 | coordinates: [[ 27 | [43.0, 24.0], 28 | [63.5, 24.0], 29 | [63.5, 40.5], 30 | [43.0, 40.5], 31 | [43.0, 24.0], 32 | ]], 33 | }, 34 | [RestrictedArea.NORTH_KOREA]: { 35 | type: "Polygon", 36 | coordinates: [[ 37 | [123.5, 37.5], 38 | [131.2, 37.5], 39 | [131.2, 43.0], 40 | [123.5, 43.0], 41 | [123.5, 37.5], 42 | ]], 43 | }, 44 | [RestrictedArea.SYRIA]: { 45 | type: "Polygon", 46 | coordinates: [[ 47 | [35.5, 32.0], 48 | [42.5, 32.0], 49 | [42.5, 37.5], 50 | [35.5, 37.5], 51 | [35.5, 32.0], 52 | ]], 53 | }, 54 | [RestrictedArea.REPUBLIC_OF_CRIMEA]: { 55 | type: "Polygon", 56 | coordinates: [[ 57 | [32.1, 43.8], 58 | [36.8, 43.8], 59 | [36.8, 46.4], 60 | [32.1, 46.4], 61 | [32.1, 43.8], 62 | ]], 63 | }, 64 | [RestrictedArea.DONETSK_OBLAST]: { 65 | type: "Polygon", 66 | coordinates: [[ 67 | [36.2, 46.6], 68 | [39.1, 46.6], 69 | [39.1, 49.3], 70 | [36.2, 49.3], 71 | [36.2, 46.6], 72 | ]], 73 | }, 74 | [RestrictedArea.LUHANSK_OBLAST]: { 75 | type: "Polygon", 76 | coordinates: [[ 77 | [37.7, 47.7], 78 | [40.3, 47.7], 79 | [40.3, 50.1], 80 | [37.7, 50.1], 81 | [37.7, 47.7], 82 | ]], 83 | }, 84 | [RestrictedArea.RUSSIA]: { 85 | type: "MultiPolygon", // Need multiple values because MongoDB gets confused with really large 2dsphere Polygons 86 | coordinates: [ 87 | [[ 88 | [27.3, 77.0], 89 | [27.3, 41.1], 90 | [69.1, 41.1], 91 | [69.1, 77.0], 92 | [27.3, 77.0], 93 | ]], 94 | [[ 95 | [69.1, 77.8], 96 | [69.1, 49.1], 97 | [114.0, 49.1], 98 | [114.0, 77.8], 99 | [69.1, 77.8], 100 | ]], 101 | [[ 102 | [114.0, 77.2], 103 | [114.0, 42.3], 104 | [180, 42.3], 105 | [180, 77.2], 106 | [114.0, 77.2], 107 | ]], 108 | [[ 109 | [-180.0, 62.0], 110 | [-170.0, 62.0], 111 | [-170.0, 72.0], 112 | [-180.0, 72.0], 113 | [-180.0, 62.0], 114 | ]], 115 | [[ 116 | [44.8, 81.9], 117 | [44.8, 77.9], 118 | [107.9, 77.9], 119 | [107.9, 81.9], 120 | [44.8, 81.9], 121 | ]] 122 | ], 123 | }, 124 | }; -------------------------------------------------------------------------------- /src/models/enums/sellerType.ts: -------------------------------------------------------------------------------- 1 | export enum SellerType { 2 | Active = 'activeSeller', 3 | Inactive = 'inactiveSeller', 4 | Test = 'testSeller', 5 | Restricted = 'restrictedSeller' 6 | } 7 | -------------------------------------------------------------------------------- /src/models/enums/stockLevelType.ts: -------------------------------------------------------------------------------- 1 | export enum StockLevelType { 2 | AVAILABLE_1 = '1 available', 3 | AVAILABLE_2 = '2 available', 4 | AVAILABLE_3 = '3 available', 5 | MANY_AVAILABLE = 'Many available', 6 | MADE_TO_ORDER = 'Made to order', 7 | ONGOING_SERVICE = 'Ongoing service', 8 | SOLD = 'Sold' 9 | } 10 | -------------------------------------------------------------------------------- /src/models/enums/trustMeterScale.ts: -------------------------------------------------------------------------------- 1 | export enum TrustMeterScale { 2 | ZERO = 0, 3 | FIFTY = 50, 4 | EIGHTY = 80, 5 | HUNDRED = 100 6 | } 7 | -------------------------------------------------------------------------------- /src/models/misc/SanctionedRegion.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | import { ISanctionedRegion } from "../../types"; 4 | import { RestrictedArea } from "../enums/restrictedArea"; 5 | 6 | const sanctionedRegionSchema = new Schema( 7 | { 8 | location: { 9 | type: String, 10 | enum: Object.values(RestrictedArea).filter(value => typeof value === 'string'), 11 | required: true, 12 | }, 13 | boundary: { 14 | type: { type: String, enum: ["Polygon", "MultiPolygon"], required: true }, 15 | coordinates: { type: Array, required: true }, 16 | } 17 | } 18 | ); 19 | 20 | sanctionedRegionSchema.index({ boundary: '2dsphere' }); 21 | 22 | // Creating the SanctionedRegion model from the schema 23 | const SanctionedRegion = mongoose.model("Sanctioned-Region", sanctionedRegionSchema); 24 | 25 | export default SanctionedRegion; 26 | -------------------------------------------------------------------------------- /src/models/misc/Toggle.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | import { IToggle } from "../../types"; 4 | 5 | const toggleSchema = new Schema( 6 | { 7 | name: { 8 | type: String, 9 | required: true, 10 | unique: true, 11 | }, 12 | enabled: { 13 | type: Boolean, 14 | required: true, 15 | default: false, 16 | }, 17 | description: { 18 | type: String, 19 | required: false, 20 | }, 21 | }, 22 | { 23 | timestamps: true, // Automatically creates createdAt and updatedAt 24 | toJSON: { 25 | transform: function (doc, ret) { 26 | delete ret._id; 27 | delete ret.__v; 28 | return ret; 29 | } 30 | } 31 | } 32 | ); 33 | 34 | const Toggle = mongoose.model("Toggle", toggleSchema); 35 | 36 | export default Toggle; 37 | -------------------------------------------------------------------------------- /src/routes/home.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | const homeRoutes = Router() 4 | 5 | /** 6 | * @swagger 7 | * /: 8 | * get: 9 | * summary: Get server status 10 | * tags: 11 | * - Home 12 | * responses: 13 | * 200: 14 | * description: Successful response | Server is running 15 | * content: 16 | * application/json: 17 | * schema: 18 | * type: object 19 | * properties: 20 | * message: 21 | * type: string 22 | * example: Server is running 23 | * 500: 24 | * description: Internal server error 25 | */ 26 | homeRoutes.get("/", (req, res) => { 27 | res.status(200).json({ 28 | message:"Server is running" 29 | }) 30 | }) 31 | 32 | export default homeRoutes 33 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import homeRoutes from "./home.routes"; 3 | 4 | const appRouter = Router(); 5 | 6 | appRouter.use("/", homeRoutes); 7 | 8 | export default appRouter; 9 | -------------------------------------------------------------------------------- /src/routes/mapCenter.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as mapCenterController from '../controllers/mapCenterController'; 3 | import { verifyToken } from '../middlewares/verifyToken'; 4 | 5 | /** 6 | * @swagger 7 | * components: 8 | * schemas: 9 | * MapCenterSchema: 10 | * type: object 11 | * properties: 12 | * map_center_id: 13 | * type: string 14 | * description: Pi user ID 15 | * longitude: 16 | * type: string 17 | * description: Longitude of the map center 18 | * latitude: 19 | * type: string 20 | * description: Latitude of the map center 21 | */ 22 | const mapCenterRoutes = Router(); 23 | 24 | /** 25 | * @swagger 26 | * /api/v1/map-center/{type}: 27 | * get: 28 | * tags: 29 | * - Map Center 30 | * summary: Get the user's map center by type [search | sell] * 31 | * parameters: 32 | * - name: type 33 | * in: path 34 | * required: true 35 | * schema: 36 | * type: string 37 | * description: The type of the map center to retrieve 38 | * responses: 39 | * 200: 40 | * description: Successful response 41 | * content: 42 | * application/json: 43 | * schema: 44 | * $ref: '/api/docs/MapCenterSchema.yml#/components/schemas/GetMapCenterRs' 45 | * 404: 46 | * description: Map Center not found | User not found 47 | * 401: 48 | * description: Unauthorized 49 | * 400: 50 | * description: Bad request 51 | * 500: 52 | * description: Internal server error 53 | */ 54 | mapCenterRoutes.get( 55 | '/:type', 56 | verifyToken, 57 | mapCenterController.getMapCenter 58 | ); 59 | 60 | /** 61 | * @swagger 62 | * /api/v1/map-center/save: 63 | * put: 64 | * tags: 65 | * - Map Center 66 | * summary: Save a new map center or update existing map center * 67 | * requestBody: 68 | * required: true 69 | * content: 70 | * application/json: 71 | * schema: 72 | * $ref: '/api/docs/MapCenterSchema.yml#/components/schemas/SaveMapCenterRq' 73 | * responses: 74 | * 200: 75 | * description: Successful response 76 | * content: 77 | * application/json: 78 | * schema: 79 | * $ref: '/api/docs/MapCenterSchema.yml#/components/schemas/SaveMapCenterRs' 80 | * 404: 81 | * description: User not found | Seller not found; Map Center failed to save 82 | * 401: 83 | * description: Unauthorized 84 | * 400: 85 | * description: Bad request 86 | * 500: 87 | * description: Internal server error 88 | */ 89 | mapCenterRoutes.put( 90 | '/save', 91 | verifyToken, 92 | mapCenterController.saveMapCenter 93 | ); 94 | 95 | export default mapCenterRoutes; 96 | -------------------------------------------------------------------------------- /src/routes/report.routes.ts: -------------------------------------------------------------------------------- 1 | import {Router} from "express"; 2 | import * as reportController from "../controllers/admin/reportsController"; 3 | import { verifyAdminToken } from "../middlewares/verifyToken"; 4 | 5 | const reportRoutes = Router(); 6 | 7 | /** 8 | * @swagger 9 | * /api/v1/reports/sanctioned-sellers-report: 10 | * post: 11 | * tags: 12 | * - Report 13 | * summary: Gather and build a report for sellers in sanctioned regions * 14 | * security: 15 | * - AdminPasswordAuth: [] 16 | * responses: 17 | * 200: 18 | * description: Successful response 19 | * content: 20 | * application/json: 21 | * schema: 22 | * type: array 23 | * items: 24 | * $ref: '/api/docs/ReportsSchema.yml#/components/schemas/GetSanctionedSellersReportRs' 25 | * 401: 26 | * description: Unauthorized 27 | * 500: 28 | * description: Internal server error 29 | */ 30 | reportRoutes.post( 31 | "/sanctioned-sellers-report", 32 | verifyAdminToken, 33 | reportController.getSanctionedSellersReport 34 | ); 35 | 36 | export default reportRoutes; 37 | -------------------------------------------------------------------------------- /src/routes/restriction.routes.ts: -------------------------------------------------------------------------------- 1 | import {Router} from "express"; 2 | import * as restrictionController from "../controllers/admin/restrictionController"; 3 | 4 | const restrictionRoutes = Router(); 5 | 6 | /** 7 | * @swagger 8 | * /api/v1/restrictions/check-sanction-status: 9 | * post: 10 | * tags: 11 | * - Restriction 12 | * summary: Check if a [latitude, longitude] coordinate is within sanctioned boundaries. 13 | * requestBody: 14 | * required: true 15 | * content: 16 | * application/json: 17 | * schema: 18 | * type: object 19 | * properties: 20 | * latitude: 21 | * type: number 22 | * example: -1.94995 23 | * longitude: 24 | * type: number 25 | * example: 30.0588 26 | * required: 27 | * - latitude 28 | * - longitude 29 | * responses: 30 | * 200: 31 | * description: Successful response 32 | * content: 33 | * application/json: 34 | * schema: 35 | * type: object 36 | * properties: 37 | * isSanctioned: 38 | * type: boolean 39 | * example: true 40 | * 400: 41 | * description: Bad request 42 | * 500: 43 | * description: Internal server error 44 | */ 45 | restrictionRoutes.post( 46 | "/check-sanction-status", 47 | restrictionController.checkSanctionStatus 48 | ); 49 | 50 | export default restrictionRoutes; -------------------------------------------------------------------------------- /src/routes/reviewFeedback.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import * as reviewFeedbackController from "../controllers/reviewFeedbackController"; 4 | import { verifyToken } from "../middlewares/verifyToken"; 5 | import upload from "../utils/multer"; 6 | 7 | /** 8 | * @swagger 9 | * components: 10 | * schemas: 11 | * ReviewFeedbackSchema: 12 | * type: object 13 | * properties: 14 | * _id: 15 | * type: string 16 | * description: Unique ID of the review 17 | * review_receiver_id: 18 | * type: string 19 | * description: Pi user ID of the user receiving the review 20 | * review_giver_id: 21 | * type: string 22 | * description: Pi user ID of the user giving the review 23 | * reply_to_review_id: 24 | * type: string 25 | * description: Unique ID of the replied review 26 | * rating: 27 | * $ref: '/api/docs/enum/RatingScale.yml#/components/schemas/RatingScale' 28 | * comment: 29 | * type: string 30 | * description: Comment given in the review 31 | * image: 32 | * type: string 33 | * description: Image associated with the review 34 | * review_date: 35 | * type: string 36 | * format: date-time 37 | * description: Date when the review was given 38 | */ 39 | const reviewFeedbackRoutes = Router(); 40 | 41 | /** 42 | * @swagger 43 | * /api/v1/review-feedback/{review_receiver_id}: 44 | * get: 45 | * tags: 46 | * - Review Feedback 47 | * summary: Get all associated reviews for seller according to search criteria, or all reviews if no search criteria provided 48 | * parameters: 49 | * - name: review_receiver_id 50 | * in: path 51 | * required: true 52 | * schema: 53 | * type: string 54 | * description: The piUID of the review receiver to retrieve 55 | * - name: searchQuery 56 | * in: query 57 | * required: false 58 | * schema: 59 | * type: string 60 | * description: The Pi username to search 61 | * responses: 62 | * 200: 63 | * description: Successful response 64 | * content: 65 | * application/json: 66 | * schema: 67 | * type: array 68 | * items: 69 | * $ref: '/api/docs/ReviewFeedbackSchema.yml#/components/schemas/GetReviewsRs' 70 | * 400: 71 | * description: Bad request 72 | * 500: 73 | * description: Internal server error 74 | */ 75 | reviewFeedbackRoutes.get("/:review_receiver_id", reviewFeedbackController.getReviews); 76 | 77 | /** 78 | * @swagger 79 | * /api/v1/review-feedback/single/{review_id}: 80 | * get: 81 | * tags: 82 | * - Review Feedback 83 | * summary: Get a single review by review ID 84 | * parameters: 85 | * - name: review_id 86 | * in: path 87 | * required: true 88 | * schema: 89 | * type: string 90 | * description: The ID of the review to retrieve 91 | * responses: 92 | * 200: 93 | * description: Successful response 94 | * content: 95 | * application/json: 96 | * schema: 97 | * type: array 98 | * items: 99 | * $ref: '/api/docs/ReviewFeedbackSchema.yml#/components/schemas/GetSingleReviewRs' 100 | * 404: 101 | * description: Review not found 102 | * 400: 103 | * description: Bad request 104 | * 500: 105 | * description: Internal server error 106 | */ 107 | reviewFeedbackRoutes.get("/single/:review_id", reviewFeedbackController.getSingleReviewById); 108 | 109 | /** 110 | * @swagger 111 | * /api/v1/review-feedback/add: 112 | * post: 113 | * tags: 114 | * - Review Feedback 115 | * summary: Add a new review * 116 | * requestBody: 117 | * required: true 118 | * content: 119 | * multipart/form-data: 120 | * schema: 121 | * $ref: '/api/docs/ReviewFeedbackSchema.yml#/components/schemas/AddReviewRq' 122 | * responses: 123 | * 200: 124 | * description: Successful response 125 | * content: 126 | * application/json: 127 | * schema: 128 | * $ref: '/api/docs/ReviewFeedbackSchema.yml#/components/schemas/AddReviewRs' 129 | * 401: 130 | * description: Unauthorized 131 | * 400: 132 | * description: Self review is prohibited 133 | * 500: 134 | * description: Internal server error 135 | */ 136 | reviewFeedbackRoutes.post( 137 | "/add", 138 | verifyToken, 139 | upload.single("image"), 140 | reviewFeedbackController.addReview 141 | ); 142 | 143 | export default reviewFeedbackRoutes; 144 | -------------------------------------------------------------------------------- /src/routes/toggle.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import * as toggleController from "../controllers/admin/toggleController"; 4 | import { verifyAdminToken } from "../middlewares/verifyToken"; 5 | 6 | /** 7 | * @swagger 8 | * components: 9 | * schemas: 10 | * ToggleSchema: 11 | * type: object 12 | * properties: 13 | * name: 14 | * type: string 15 | * description: Name of the toggle 16 | * enabled: 17 | * type: boolean 18 | * description: State of the toggle 19 | * description: 20 | * type: string 21 | * description: Description of the toggle 22 | */ 23 | const toggleRoutes = Router(); 24 | 25 | /** 26 | * @swagger 27 | * /api/v1/toggles: 28 | * get: 29 | * tags: 30 | * - Toggle 31 | * summary: Get all existing toggles 32 | * responses: 33 | * 200: 34 | * description: Successful response 35 | * content: 36 | * application/json: 37 | * schema: 38 | * type: array 39 | * items: 40 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/GetAllTogglesRs' 41 | * 500: 42 | * description: Internal server error 43 | */ 44 | toggleRoutes.get("/", toggleController.getToggles); 45 | 46 | /** 47 | * @swagger 48 | * /api/v1/toggles/{toggle_name}: 49 | * get: 50 | * tags: 51 | * - Toggle 52 | * summary: Get the corresponding toggle by toggle name 53 | * parameters: 54 | * - name: toggle_name 55 | * in: path 56 | * required: true 57 | * schema: 58 | * type: string 59 | * description: The name of the toggle to retrieve 60 | * responses: 61 | * 200: 62 | * description: Successful response 63 | * content: 64 | * application/json: 65 | * schema: 66 | * type: array 67 | * items: 68 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/GetSingleToggleRs' 69 | * 404: 70 | * description: Toggle not found 71 | * 500: 72 | * description: Internal server error 73 | */ 74 | toggleRoutes.get("/:toggle_name", toggleController.getToggle); 75 | 76 | /** 77 | * @swagger 78 | * /api/v1/toggles/add: 79 | * post: 80 | * tags: 81 | * - Toggle 82 | * summary: Add a new toggle * 83 | * security: 84 | * - AdminPasswordAuth: [] 85 | * requestBody: 86 | * required: true 87 | * content: 88 | * application/json: 89 | * schema: 90 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/AddToggleRq' 91 | * responses: 92 | * 201: 93 | * description: Successful response 94 | * content: 95 | * application/json: 96 | * schema: 97 | * type: object 98 | * properties: 99 | * message: 100 | * type: string 101 | * example: "Toggle successfully added" 102 | * addedToggle: 103 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/AddToggleRs' 104 | * 401: 105 | * description: Unauthorized 106 | * 500: 107 | * description: Internal server error 108 | */ 109 | toggleRoutes.post("/add", verifyAdminToken, toggleController.addToggle); 110 | 111 | /** 112 | * @swagger 113 | * /api/v1/toggles/update: 114 | * put: 115 | * tags: 116 | * - Toggle 117 | * summary: Update an existing toggle * 118 | * security: 119 | * - AdminPasswordAuth: [] 120 | * requestBody: 121 | * required: true 122 | * content: 123 | * application/json: 124 | * schema: 125 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/UpdateToggleRq' 126 | * responses: 127 | * 200: 128 | * description: Successful response 129 | * content: 130 | * application/json: 131 | * schema: 132 | * type: object 133 | * properties: 134 | * message: 135 | * type: string 136 | * example: "Toggle successfully updated" 137 | * updatedToggle: 138 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/UpdateToggleRs' 139 | * 401: 140 | * description: Unauthorized 141 | * 500: 142 | * description: Internal server error 143 | */ 144 | toggleRoutes.put("/update", verifyAdminToken, toggleController.updateToggle); 145 | 146 | /** 147 | * @swagger 148 | * /api/v1/toggles/delete/{toggle_name}: 149 | * delete: 150 | * tags: 151 | * - Toggle 152 | * summary: Delete the corresponding toggle by toggle name * 153 | * security: 154 | * - AdminPasswordAuth: [] 155 | * parameters: 156 | * - name: toggle_name 157 | * in: path 158 | * required: true 159 | * schema: 160 | * type: string 161 | * description: The name of the toggle to delete 162 | * responses: 163 | * 200: 164 | * description: Successful response 165 | * content: 166 | * application/json: 167 | * schema: 168 | * type: object 169 | * properties: 170 | * message: 171 | * type: string 172 | * example: "Toggle successfully deleted" 173 | * deletedToggle: 174 | * $ref: '/api/docs/TogglesSchema.yml#/components/schemas/DeleteToggleRs' 175 | * 404: 176 | * description: Toggle not found 177 | * 401: 178 | * description: Unauthorized 179 | * 500: 180 | * description: Internal server error 181 | */ 182 | toggleRoutes.delete("/delete/:toggle_name", verifyAdminToken, toggleController.deleteToggle); 183 | 184 | export default toggleRoutes; 185 | -------------------------------------------------------------------------------- /src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import * as userController from "../controllers/userController"; 4 | import { isPioneerFound } from "../middlewares/isPioneerFound"; 5 | import { verifyToken } from "../middlewares/verifyToken"; 6 | 7 | /** 8 | * @swagger 9 | * components: 10 | * schemas: 11 | * UserSchema: 12 | * type: object 13 | * properties: 14 | * pi_uid: 15 | * type: string 16 | * description: Pi user ID 17 | * pi_username: 18 | * type: string 19 | * description: Pi user alias 20 | * user_name: 21 | * type: string 22 | * description: Name of Pi user; preset to Pi user alias 23 | */ 24 | const userRoutes = Router(); 25 | 26 | /** 27 | * @swagger 28 | * /api/v1/users/authenticate: 29 | * post: 30 | * tags: 31 | * - User 32 | * summary: Authenticate the user's access token * 33 | * security: 34 | * - BearerAuth: [] 35 | * responses: 36 | * 200: 37 | * description: Successful response 38 | * content: 39 | * application/json: 40 | * schema: 41 | * $ref: '/api/docs/UsersSchema.yml#/components/schemas/AuthenticateUserRs' 42 | * 404: 43 | * description: Pioneer not found 44 | * 400: 45 | * description: Bad request 46 | * 500: 47 | * description: Internal server error 48 | */ 49 | userRoutes.post("/authenticate", isPioneerFound, userController.authenticateUser); 50 | 51 | /** 52 | * @swagger 53 | * /api/v1/users/me: 54 | * get: 55 | * tags: 56 | * - User 57 | * summary: Fetch the user's information using Bearer Auth token * 58 | * responses: 59 | * 200: 60 | * description: Successful response 61 | * content: 62 | * application/json: 63 | * schema: 64 | * $ref: '/api/docs/UsersSchema.yml#/components/schemas/GetUserRs' 65 | * 404: 66 | * description: User not found | Pioneer not found 67 | * 401: 68 | * description: Unauthorized 69 | * 400: 70 | * description: Bad request 71 | * 500: 72 | * description: Internal server error 73 | */ 74 | userRoutes.get("/me", verifyToken, userController.autoLoginUser); 75 | 76 | /** 77 | * @swagger 78 | * /api/v1/users/{pi_uid}: 79 | * get: 80 | * tags: 81 | * - User 82 | * summary: Get a user by Pi UID 83 | * parameters: 84 | * - name: pi_uid 85 | * in: path 86 | * required: true 87 | * schema: 88 | * type: string 89 | * description: The Pi uid of the user to retrieve 90 | * responses: 91 | * 200: 92 | * description: Successful response 93 | * content: 94 | * application/json: 95 | * schema: 96 | * $ref: '/api/docs/UsersSchema.yml#/components/schemas/GetUserRs' 97 | * 404: 98 | * description: User not found 99 | * 400: 100 | * description: Bad request 101 | * 500: 102 | * description: Internal server error 103 | */ 104 | userRoutes.get("/:pi_uid", userController.getUser); 105 | 106 | /** 107 | * @swagger 108 | * /api/v1/users/delete: 109 | * delete: 110 | * tags: 111 | * - User 112 | * summary: Delete a user and user associated data using Bearer Auth token * 113 | * responses: 114 | * 200: 115 | * description: Successful response | User deleted successfully 116 | * content: 117 | * application/json: 118 | * schema: 119 | * $ref: '/api/docs/UsersSchema.yml#/components/schemas/DeleteUserRs' 120 | * 404: 121 | * description: User not found 122 | * 401: 123 | * description: Unauthorized 124 | * 400: 125 | * description: Bad request 126 | * 500: 127 | * description: Internal server error 128 | */ 129 | userRoutes.delete( 130 | "/delete", 131 | verifyToken, 132 | userController.deleteUser 133 | ); 134 | 135 | export default userRoutes; 136 | -------------------------------------------------------------------------------- /src/routes/userPreferences.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import * as userPreferencesController from "../controllers/userPreferencesController"; 4 | import { isUserSettingsFound } from "../middlewares/isUserSettingsFound"; 5 | import { verifyToken } from "../middlewares/verifyToken"; 6 | import upload from "../utils/multer"; 7 | 8 | /** 9 | * @swagger 10 | * components: 11 | * schemas: 12 | * UserPreferencesSchema: 13 | * type: object 14 | * properties: 15 | * user_settings_id: 16 | * type: string 17 | * description: Pi user ID 18 | * user_name: 19 | * type: string 20 | * description: Name of the user 21 | * email: 22 | * type: string 23 | * description: Email address of the user 24 | * phone_number: 25 | * type: string 26 | * description: Phone number of the user 27 | * image: 28 | * type: string 29 | * description: Image of the user 30 | * findme_preference: 31 | * type: string 32 | * description: FindMe preference of the user 33 | * trust_meter_rating: 34 | * $ref: '/api/docs/enum/TrustMeterScale.yml#/components/schemas/TrustMeterScale' 35 | * search_map_center: 36 | * type: object 37 | * description: Geographical coordinates of the user's search center location 38 | * properties: 39 | * type: 40 | * type: string 41 | * coordinates: 42 | * type: array 43 | * items: 44 | * type: number 45 | * required: 46 | * - type 47 | * - coordinates 48 | */ 49 | const userPreferencesRoutes = Router(); 50 | 51 | /** 52 | * @swagger 53 | * /api/v1/user-preferences/{user_settings_id}: 54 | * get: 55 | * tags: 56 | * - User Preferences 57 | * summary: Get the user preferences by user settings ID 58 | * parameters: 59 | * - name: user_settings_id 60 | * in: path 61 | * required: true 62 | * schema: 63 | * type: string 64 | * description: The Pi UID of the user preferences to retrieve 65 | * responses: 66 | * 200: 67 | * description: Successful response 68 | * content: 69 | * application/json: 70 | * schema: 71 | * $ref: '/api/docs/UserPreferencesSchema.yml#/components/schemas/GetUserPreferencesRs' 72 | * 404: 73 | * description: User Preferences not found 74 | * 400: 75 | * description: Bad request 76 | * 500: 77 | * description: Internal server error 78 | */ 79 | userPreferencesRoutes.get("/:user_settings_id", userPreferencesController.getUserPreferences); 80 | 81 | /** 82 | * @swagger 83 | * /api/v1/user-preferences/me: 84 | * post: 85 | * tags: 86 | * - User Preferences 87 | * summary: Fetch the user's preference using Bearer Auth token * 88 | * responses: 89 | * 200: 90 | * description: Successful response 91 | * content: 92 | * application/json: 93 | * schema: 94 | * $ref: '/api/docs/UserPreferencesSchema.yml#/components/schemas/GetUserPreferencesRs' 95 | * 404: 96 | * description: User Preferences not found | User Settings not found 97 | * 401: 98 | * description: Unauthorized 99 | * 400: 100 | * description: Bad request 101 | * 500: 102 | * description: Internal server error 103 | */ 104 | userPreferencesRoutes.post( 105 | "/me", 106 | verifyToken, 107 | isUserSettingsFound, 108 | userPreferencesController.fetchUserPreferences); 109 | 110 | /** 111 | * @swagger 112 | * /api/v1/user-preferences/add: 113 | * put: 114 | * tags: 115 | * - User Preferences 116 | * summary: Add new user preferences or update existing user preferences * 117 | * requestBody: 118 | * required: true 119 | * content: 120 | * multipart/form-data: 121 | * schema: 122 | * $ref: '/api/docs/UserPreferencesSchema.yml#/components/schemas/AddUserPreferencesRq' 123 | * responses: 124 | * 200: 125 | * description: Successful response 126 | * content: 127 | * application/json: 128 | * schema: 129 | * $ref: '/api/docs/UserPreferencesSchema.yml#/components/schemas/AddUserPreferencesRs' 130 | * 401: 131 | * description: Unauthorized 132 | * 400: 133 | * description: Bad request 134 | * 500: 135 | * description: Internal server error 136 | */ 137 | userPreferencesRoutes.put( 138 | "/add", 139 | verifyToken, 140 | upload.single("image"), 141 | userPreferencesController.addUserPreferences 142 | ); 143 | 144 | /** 145 | * @swagger 146 | * /api/v1/user-preferences/{user_settings_id}: 147 | * delete: 148 | * tags: 149 | * - User Preferences 150 | * summary: Delete user preferences by user settings ID * 151 | * parameters: 152 | * - name: user_settings_id 153 | * in: path 154 | * required: true 155 | * schema: 156 | * type: string 157 | * description: The Pi UID of the user preferences to delete 158 | * responses: 159 | * 200: 160 | * description: Successful response | User Preferences deleted successfully 161 | * content: 162 | * application/json: 163 | * schema: 164 | * $ref: '/api/docs/UserPreferencesSchema.yml#/components/schemas/DeleteUserPreferencesRs' 165 | * 404: 166 | * description: User Preferences not found 167 | * 401: 168 | * description: Unauthorized 169 | * 400: 170 | * description: Bad request 171 | * 500: 172 | * description: Internal server error 173 | */ 174 | userPreferencesRoutes.delete( 175 | "/:user_settings_id", 176 | verifyToken, 177 | isUserSettingsFound, 178 | userPreferencesController.deleteUserPreferences 179 | ); 180 | 181 | /** 182 | * @swagger 183 | * /api/v1/user-preferences/location/me: 184 | * get: 185 | * tags: 186 | * - User Preferences 187 | * summary: Fetch the user's current location using Bearer Auth token * 188 | * responses: 189 | * 200: 190 | * description: Successful response 191 | * content: 192 | * application/json: 193 | * schema: 194 | * $ref: '/api/docs/UserPreferencesSchema.yml#/components/schemas/GetUserLocationRs' 195 | * 404: 196 | * description: User location not found 197 | * 401: 198 | * description: Unauthorized 199 | * 400: 200 | * description: Bad request 201 | * 500: 202 | * description: Internal server error 203 | */ 204 | userPreferencesRoutes.get( 205 | '/location/me', 206 | verifyToken, 207 | userPreferencesController.getUserLocation 208 | ) 209 | 210 | export default userPreferencesRoutes; 211 | -------------------------------------------------------------------------------- /src/services/admin/report.service.ts: -------------------------------------------------------------------------------- 1 | import Bottleneck from "bottleneck"; 2 | 3 | import SanctionedRegion from "../../models/misc/SanctionedRegion"; 4 | import { RestrictedArea } from "../../models/enums/restrictedArea"; 5 | import { getSellersWithinSanctionedRegion } from "../seller.service"; 6 | import { reverseLocationDetails } from "../../helpers/location"; 7 | import { ISanctionedRegion, ISeller, SanctionedSeller } from "../../types"; 8 | import logger from "../../config/loggingConfig"; 9 | 10 | const requestLimiter = new Bottleneck({ minTime: 1000 }); 11 | 12 | export const reportSanctionedSellers = async (): Promise => { 13 | const sanctionedSellers: SanctionedSeller[] = []; 14 | 15 | try { 16 | // fetch sanctioned regions from Mongo DB collection 17 | const sanctionedRegions = await getAllSanctionedRegions(); 18 | if (!sanctionedRegions.length) { 19 | return []; 20 | } 21 | 22 | // determine potential sanctioned sellers 23 | for (const region of sanctionedRegions) { 24 | // fetch affected sellers within the current sanctioned region 25 | const sellersInRegion = await getSellersWithinSanctionedRegion(region); 26 | 27 | // geocode and validate each affected seller using Nominatim API 28 | const results = await Promise.all( 29 | sellersInRegion.map((seller) => processSellerGeocoding(seller, region.location)) 30 | ); 31 | sanctionedSellers.push(...results.filter((result): result is SanctionedSeller => result !== null)); 32 | } 33 | 34 | if (sanctionedSellers.length > 0) { 35 | logger.info(`Total number of sanctioned sellers: ${sanctionedSellers.length}`); 36 | logger.error(`Sanctioned Sellers Report | ${sanctionedSellers.length} found: ${JSON.stringify(sanctionedSellers, null, 2)}`); 37 | } else { 38 | logger.info('No sellers found in any sanctioned regions'); 39 | } 40 | } catch (error) { 41 | // Capture any errors and send to Sentry 42 | logger.error('An error occurred while generating the Sanctioned Sellers Report:', error); 43 | throw new Error('Failed to generate Sanctioned Sellers Report; please try again later.'); 44 | } 45 | return sanctionedSellers; 46 | }; 47 | 48 | // Fetch all sanctioned regions 49 | export const getAllSanctionedRegions = async (): Promise => { 50 | try { 51 | const regions = await SanctionedRegion.find(); 52 | if (!regions || regions.length === 0) { 53 | logger.warn('No sanctioned regions found'); 54 | return []; 55 | } 56 | logger.info(`Fetched ${regions.length} sanctioned regions`); 57 | return regions; 58 | } catch (error) { 59 | logger.error('Failed to fetch sanctioned regions:', error); 60 | throw new Error('Failed to get sanctioned regions; please try again later'); 61 | } 62 | }; 63 | 64 | // Function to handle geocoding for a single seller 65 | export const processSellerGeocoding = async ( 66 | seller: ISeller, 67 | sanctionedRegion: RestrictedArea 68 | ): Promise => { 69 | const { seller_id, name, address, sell_map_center, pre_restriction_seller_type } = seller; 70 | const [longitude, latitude] = sell_map_center.coordinates; 71 | 72 | try { 73 | const response = await requestLimiter.wrap(reverseLocationDetails)(latitude, longitude); 74 | 75 | if (response.data.error) { 76 | logger.error(`Geocoding error for seller ${seller_id}`, { 77 | coordinates: [latitude, longitude], 78 | error: response.data.error, 79 | }); 80 | return null; 81 | } 82 | 83 | const locationName = response.data.display_name; 84 | if (locationName.includes(sanctionedRegion)) { 85 | logger.info('Sanctioned Seller found', { seller_id, name, address, coordinates: [latitude, longitude], sanctioned_location: locationName }); 86 | return { 87 | seller_id, 88 | name, 89 | address, 90 | sell_map_center, 91 | sanctioned_location: locationName, 92 | pre_restriction_seller_type 93 | }; 94 | } 95 | } catch (error) { 96 | logger.error(`Geocoding failed for seller ${seller_id}`, { coordinates: [latitude, longitude], error }); 97 | } 98 | return null; 99 | }; -------------------------------------------------------------------------------- /src/services/admin/restriction.service.ts: -------------------------------------------------------------------------------- 1 | import SanctionedRegion from "../../models/misc/SanctionedRegion"; 2 | 3 | export const validateSellerLocation = async (longitude: number, latitude: number) => { 4 | const sellCenter = { 5 | type: 'Point' as const, 6 | coordinates: [longitude, latitude], 7 | }; 8 | 9 | const isSanctionedLocation = await SanctionedRegion.findOne({ 10 | boundary: { 11 | $geoIntersects: { 12 | $geometry: sellCenter 13 | } 14 | } 15 | }).exec(); 16 | 17 | return isSanctionedLocation; 18 | }; 19 | -------------------------------------------------------------------------------- /src/services/admin/toggle.service.ts: -------------------------------------------------------------------------------- 1 | import Toggle from "../../models/misc/Toggle"; 2 | import { IToggle } from "../../types"; 3 | 4 | import logger from "../../config/loggingConfig"; 5 | 6 | export const getToggles = async (): Promise => { 7 | try { 8 | const toggles = await Toggle.find().sort({ createdAt: -1 }).exec(); 9 | logger.info(`Successfully retrieved ${toggles.length} toggle(s)`); 10 | return toggles; 11 | } catch (error) { 12 | logger.error('Failed to retrieve toggles:', error); 13 | throw new Error('Failed to get toggles; please try again later'); 14 | } 15 | }; 16 | 17 | export const getToggleByName = async (name: string): Promise => { 18 | try { 19 | const toggle = await Toggle.findOne({ name }).exec(); 20 | return toggle ? toggle as IToggle : null; 21 | } catch (error) { 22 | logger.error(`Failed to retrieve toggle with identifier ${ name }:`, error); 23 | throw new Error('Failed to get toggle; please try again later'); 24 | } 25 | }; 26 | 27 | export const addToggle = async (toggleData: IToggle): Promise => { 28 | try { 29 | // Check if a toggle with the same name already exists 30 | const existingToggle = await Toggle.findOne({ name: toggleData.name }).exec(); 31 | if (existingToggle) { 32 | throw new Error(`A toggle with the identifier ${toggleData.name} already exists.`); 33 | } 34 | 35 | // Create the new toggle instance 36 | const newToggle = new Toggle({ 37 | ...toggleData 38 | }); 39 | const savedToggle = await newToggle.save(); 40 | return savedToggle as IToggle; 41 | } catch (error: any) { 42 | if (error.message.includes('already exists')) { 43 | throw error; 44 | } 45 | logger.error('Failed to add toggle:', error); 46 | throw new Error('Failed to add toggle; please try again later'); 47 | } 48 | }; 49 | 50 | export const updateToggle = async ( 51 | name: string, 52 | enabled: boolean, 53 | description?: string 54 | ): Promise => { 55 | try { 56 | const updateData: any = { enabled }; 57 | 58 | // Only update the description if it's provided and not an empty string 59 | if (description !== undefined && description !== '') { 60 | updateData.description = description; 61 | } 62 | 63 | // Find and update the toggle by name 64 | const updatedToggle = await Toggle.findOneAndUpdate( 65 | { name }, 66 | { $set: updateData }, 67 | { new: true } 68 | ).exec(); 69 | 70 | if (!updatedToggle) { 71 | throw new Error(`A toggle with the identifier ${name} does not exist.`); 72 | } 73 | 74 | logger.info('Toggle successfully updated in the database:', updatedToggle); 75 | return updatedToggle as IToggle; 76 | } catch (error: any) { 77 | if (error.message.includes('does not exist')) { 78 | throw error; 79 | } 80 | logger.error('Failed to update toggle:', error); 81 | throw new Error('Failed to update toggle; please try again later'); 82 | } 83 | }; 84 | 85 | export const deleteToggleByName = async (name: string): Promise => { 86 | try { 87 | const deletedToggle = await Toggle.findOneAndDelete({ name }).exec(); 88 | 89 | if (!deletedToggle) { 90 | logger.warn(`A toggle with the identifier ${name} does not exist.`); 91 | return null; 92 | } 93 | logger.info('Toggle successfully deleted in the database:', deletedToggle); 94 | return deletedToggle as IToggle; 95 | } catch (error) { 96 | logger.error(`Failed to delete toggle with identifier ${ name }:`, error); 97 | throw new Error('Failed to delete toggle; please try again later'); 98 | } 99 | }; -------------------------------------------------------------------------------- /src/services/mapCenter.service.ts: -------------------------------------------------------------------------------- 1 | import { IMapCenter } from "../types"; 2 | 3 | import Seller from "../models/Seller"; 4 | import UserSettings from "../models/UserSettings"; 5 | import logger from "../config/loggingConfig"; 6 | 7 | export const getMapCenterById = async (map_center_id: string, type: string): Promise => { 8 | try { 9 | if (type === 'sell') { 10 | let seller = await Seller.findOne({ seller_id: map_center_id }).exec(); 11 | return seller? seller.sell_map_center as IMapCenter : null; 12 | } else if (type === 'search') { 13 | let userSettings = await UserSettings.findOne({ user_settings_id: map_center_id }).exec(); 14 | return userSettings? userSettings.search_map_center as IMapCenter : null; 15 | } else { 16 | return null; 17 | } 18 | } catch (error) { 19 | logger.error(`Failed to retrieve Map Center for mapCenterID ${ map_center_id }:`, error); 20 | throw new Error('Failed to retrieve Map Center; please try again later'); 21 | } 22 | }; 23 | 24 | export const createOrUpdateMapCenter = async ( 25 | map_center_id: string, 26 | latitude: number, 27 | longitude: number, 28 | type: 'search' | 'sell' 29 | ): Promise => { 30 | try { 31 | const setCenter: IMapCenter = { 32 | type: 'Point', 33 | coordinates: [longitude, latitude], 34 | } 35 | if (type === 'search') { 36 | await UserSettings.findOneAndUpdate( 37 | { user_settings_id: map_center_id }, 38 | { search_map_center: setCenter }, 39 | { new: true } 40 | ).exec(); 41 | 42 | } else if (type === 'sell') { 43 | const existingSeller = await Seller.findOneAndUpdate( 44 | { seller_id: map_center_id }, 45 | { sell_map_center: setCenter }, 46 | { new: true } 47 | ).exec(); 48 | if (!existingSeller) { 49 | await Seller.create({ 50 | seller_id: map_center_id, 51 | sell_map_center: setCenter, 52 | }) 53 | } 54 | } 55 | return setCenter; 56 | } catch (error) { 57 | logger.error(`Failed to create or update Map Center for ${ type }:`, error); 58 | throw new Error('Failed to create or update Map Center; please try again later'); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/services/misc/image.service.ts: -------------------------------------------------------------------------------- 1 | import cloudinary from '../../utils/cloudinary'; 2 | import logger from '../../config/loggingConfig'; 3 | 4 | export const uploadImage = async (publicId: string, file: Express.Multer.File, folder: string) => { 5 | try { 6 | const result = await cloudinary.uploader.upload(file.path, { 7 | folder: folder, 8 | public_id: publicId, 9 | resource_type: 'image', 10 | overwrite: true 11 | }); 12 | logger.info('Image has been uploaded successfully'); 13 | return result.secure_url; 14 | } catch (error) { 15 | logger.error('Failed to upload image:', error); 16 | throw new Error('Failed to upload image; please try again later'); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/services/reviewFeedback.service.ts: -------------------------------------------------------------------------------- 1 | import { getUser } from "./user.service"; 2 | import ReviewFeedback from "../models/ReviewFeedback"; 3 | import UserSettings from "../models/UserSettings"; 4 | import { IReviewFeedback, IUser, IReviewFeedbackOutput, CompleteFeedback } from "../types"; 5 | 6 | import logger from "../config/loggingConfig"; 7 | import User from "../models/User"; 8 | 9 | /** 10 | The value is set depending on the number of zero(0) ratings in the ReviewFeedback table where this user is review-receiver. 11 | IF user has less than or equal to 5% zero ratings THEN set it to 100. 12 | IF user has 5.01%-10% zero ratings THEN set it to 80. 13 | IF user has 10.01%-20% zero ratings THEN set it to 50. 14 | IF user has more than 20.01% zero ratings THEN set it to 0. 15 | When the User Registration screen is first used (before the users User record has been created) 16 | then the value of “100” is displayed and saved to the DB. 17 | **/ 18 | const computeRatings = async (user_settings_id: string) => { 19 | try { 20 | // Fetch all reviews for the user 21 | const reviewFeedbackCount = await ReviewFeedback.countDocuments({ review_receiver_id: user_settings_id }).exec(); 22 | if (reviewFeedbackCount === 0) { 23 | // Default value when there are no reviews 24 | await UserSettings.findOneAndUpdate({ user_settings_id }, { trust_meter_rating: 100 }).exec(); 25 | return 100; 26 | } 27 | // Calculate the total number of reviews and the number of zero ratings 28 | const totalReviews = reviewFeedbackCount 29 | const zeroRatingsCount = await ReviewFeedback.countDocuments({ review_receiver_id: user_settings_id, rating: 0 }).exec(); 30 | 31 | // Calculate the percentage of zero ratings 32 | const zeroRatingsPercentage = (zeroRatingsCount / totalReviews) * 100; 33 | 34 | // Determine the value based on the percentage of zero ratings 35 | let value; 36 | switch (true) { 37 | case (zeroRatingsPercentage <= 5): 38 | value = 100; 39 | break; 40 | case (zeroRatingsPercentage > 5 && zeroRatingsPercentage <= 10): 41 | value = 80; 42 | break; 43 | case (zeroRatingsPercentage > 10 && zeroRatingsPercentage <= 20): 44 | value = 50; 45 | break; 46 | default: 47 | value = 0; 48 | } 49 | 50 | // Update the user's rating value in the database 51 | await UserSettings.findOneAndUpdate({ user_settings_id }, { trust_meter_rating: value }); 52 | return value; 53 | } catch (error) { 54 | logger.error(`Failed to compute ratings for userSettingsID ${ user_settings_id }:`, error); 55 | throw new Error('Failed to compute ratings; please try again later'); 56 | } 57 | }; 58 | 59 | export const getReviewFeedback = async ( 60 | review_receiver_id: string, 61 | searchQuery?: string 62 | ): Promise => { 63 | try { 64 | //condition to search by username 65 | if (searchQuery && searchQuery.trim()) { 66 | const user = await User.findOne({ 67 | pi_username: searchQuery 68 | }); 69 | if (!user) { 70 | return null; 71 | } 72 | review_receiver_id = user.pi_uid; 73 | } 74 | 75 | const receivedFeedbackList = await ReviewFeedback.find({ 76 | review_receiver_id: review_receiver_id 77 | }).sort({ review_date: -1 }).exec(); 78 | 79 | const givenFeedbackList = await ReviewFeedback.find({ 80 | review_giver_id: review_receiver_id 81 | }).sort({ review_date: -1 }).exec(); 82 | 83 | const updatedReceivedFeedbackList = await Promise.all( 84 | receivedFeedbackList.map(async (reviewFeedback) => { 85 | // Retrieve user details for both giver and receiver 86 | const reviewer = await getUser(reviewFeedback.review_giver_id); 87 | const receiver = await getUser(reviewFeedback.review_receiver_id); 88 | 89 | const giverName = reviewer ? reviewer.user_name : ''; 90 | const receiverName = receiver ? receiver.user_name : ''; 91 | 92 | // Return the updated review feedback object 93 | return { ...reviewFeedback.toObject(), giver: giverName, receiver: receiverName }; 94 | }) 95 | ); 96 | 97 | const updatedGivenFeedbackList = await Promise.all( 98 | givenFeedbackList.map(async (reviewFeedback) => { 99 | // Retrieve user details for both giver and receiver 100 | const reviewer = await getUser(reviewFeedback.review_giver_id); 101 | const receiver = await getUser(reviewFeedback.review_receiver_id); 102 | 103 | const giverName = reviewer ? reviewer.user_name : ''; 104 | const receiverName = receiver ? receiver.user_name : ''; 105 | 106 | // Return the updated review feedback object 107 | return { ...reviewFeedback.toObject(), giver: giverName, receiver: receiverName }; 108 | }) 109 | ); 110 | return { 111 | givenReviews: updatedGivenFeedbackList, 112 | receivedReviews: updatedReceivedFeedbackList 113 | } as unknown as CompleteFeedback; 114 | 115 | } catch (error) { 116 | logger.error(`Failed to retrieve reviews for reviewReceiverID ${review_receiver_id}:`, error); 117 | throw new Error('Failed to retrieve reviews; please try again later'); 118 | } 119 | }; 120 | 121 | export const getReviewFeedbackById = async (review_id: string): Promise<{ 122 | review: IReviewFeedbackOutput | null; 123 | replies: IReviewFeedbackOutput[]; 124 | } | null> => { 125 | try { 126 | // Find the main review by ID 127 | const reviewFeedback = await ReviewFeedback.findById(review_id).exec(); 128 | 129 | if (!reviewFeedback) { 130 | logger.warn(`No review found with ID: ${review_id}`); 131 | return null; 132 | } 133 | 134 | // Fetch replies to the main review 135 | const replies = await ReviewFeedback.find({ reply_to_review_id: review_id }).exec(); 136 | 137 | // Fetch giver and receiver names for each reply asynchronously 138 | const updatedReplyList = await Promise.all( 139 | replies.map(async (reply) => { 140 | const [reviewer, receiver] = await Promise.all([ 141 | getUser(reply.review_giver_id), 142 | getUser(reply.review_receiver_id), 143 | ]); 144 | 145 | const giverName = reviewer?.user_name || 'Unknown'; 146 | const receiverName = receiver?.user_name || 'Unknown'; 147 | 148 | // Return updated reply object 149 | return { ...reply.toObject(), giver: giverName, receiver: receiverName }; 150 | }) 151 | ); 152 | 153 | // Fetch giver and receiver names for the main review 154 | const [reviewer, receiver] = await Promise.all([ 155 | getUser(reviewFeedback.review_giver_id), 156 | getUser(reviewFeedback.review_receiver_id), 157 | ]); 158 | 159 | const giverName = reviewer?.user_name || 'Unknown'; 160 | const receiverName = receiver?.user_name || 'Unknown'; 161 | 162 | // Create the main review object with giver and receiver names 163 | const mainReview = { ...reviewFeedback.toObject(), giver: giverName, receiver: receiverName }; 164 | 165 | return { 166 | review: mainReview as unknown as IReviewFeedbackOutput, 167 | replies: updatedReplyList as unknown as IReviewFeedbackOutput[], 168 | }; 169 | } catch (error) { 170 | logger.error(`Failed to retrieve review for reviewID ${review_id}:`, error); 171 | throw new Error('Failed to retrieve review; please try again later'); 172 | } 173 | }; 174 | 175 | export const addReviewFeedback = async (authUser: IUser, formData: any, image: string): Promise => { 176 | try { 177 | const reviewFeedbackData: Partial = { 178 | review_receiver_id: formData.review_receiver_id || '', 179 | review_giver_id: authUser.pi_uid, 180 | reply_to_review_id: formData.reply_to_review_id || null, 181 | rating: formData.rating || '', 182 | comment: formData.comment || '', 183 | image: image || '', 184 | review_date: new Date() 185 | }; 186 | const newReviewFeedback = new ReviewFeedback(reviewFeedbackData); 187 | const savedReviewFeedback = await newReviewFeedback.save(); 188 | 189 | const computedValue = await computeRatings(savedReviewFeedback.review_receiver_id); 190 | logger.info(`Computed review rating: ${computedValue}`); 191 | 192 | return savedReviewFeedback as IReviewFeedback; 193 | } catch (error) { 194 | logger.error('Failed to add review:', error); 195 | throw new Error('Failed to add review; please try again later'); 196 | } 197 | }; -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import User from "../models/User"; 2 | import Seller from "../models/Seller"; 3 | import UserSettings from "../models/UserSettings"; 4 | import { ISeller, IUser, IUserSettings } from "../types"; 5 | import { getLocationByIP } from "./userSettings.service"; 6 | import logger from "../config/loggingConfig"; 7 | 8 | const getCoordinatesWithRetry = async ( 9 | retries: number = 3, 10 | delay: number = 1000 // Delay in milliseconds 11 | ): Promise<{ lat: number; lng: number } | null> => { 12 | let attempt = 0; 13 | while (attempt < retries) { 14 | const coordinates = await getLocationByIP(); 15 | if (coordinates) { 16 | return coordinates; 17 | } 18 | attempt++; 19 | logger.warn(`Retrying IP location fetch (${attempt}/${retries})...`); 20 | await new Promise((resolve) => setTimeout(resolve, delay)); 21 | } 22 | logger.warn('Failed to fetch IP coordinates after maximum retries'); 23 | return null; 24 | }; 25 | 26 | export const authenticate = async (currentUser: IUser): Promise => { 27 | try { 28 | const user = await User.findOne({ 29 | pi_uid: currentUser.pi_uid, 30 | pi_username: currentUser.pi_username 31 | }).setOptions({ 32 | readPreference: 'primary' 33 | }).exec(); 34 | 35 | if (user) { 36 | return user; 37 | } else { 38 | const newUser = await User.create({ 39 | pi_uid: currentUser.pi_uid, 40 | pi_username: currentUser.pi_username, 41 | user_name: currentUser.user_name 42 | }); 43 | const IP_coordinates = await getCoordinatesWithRetry(3, 1000); 44 | 45 | IP_coordinates ? 46 | await UserSettings.create({ 47 | user_settings_id: currentUser.pi_uid, 48 | user_name: currentUser.user_name, 49 | search_map_center: { type: 'Point', coordinates: [IP_coordinates.lng, IP_coordinates.lat] } 50 | }) : 51 | await UserSettings.create({ 52 | user_settings_id: currentUser.pi_uid, 53 | user_name: currentUser.user_name, 54 | }) 55 | 56 | return newUser; 57 | } 58 | } catch (error) { 59 | logger.error('Failed to authenticate user:', error); 60 | throw new Error('Failed during user authentication; please try again later'); 61 | } 62 | }; 63 | 64 | export const getUser = async (pi_uid: string): Promise => { 65 | try { 66 | const user = await User.findOne({ pi_uid }).exec(); 67 | return user ? user as IUser : null; 68 | } catch (error) { 69 | logger.error(`Failed to retrieve user for piUID ${ pi_uid }:`, error); 70 | throw new Error('Failed to retrieve user; please try again later'); 71 | } 72 | }; 73 | 74 | export const deleteUser = async (pi_uid: string | undefined): Promise<{ user: IUser | null, sellers: ISeller[], userSetting: IUserSettings }> => { 75 | try { 76 | // delete any association with Seller 77 | const deletedSellers = await Seller.find({ seller_id: pi_uid }).exec(); 78 | await Seller.deleteMany({ seller_id: pi_uid }).exec(); 79 | 80 | // delete any association with User Settings 81 | const deletedUserSettings = await UserSettings.findOneAndDelete({ user_settings_id: pi_uid }).exec(); 82 | 83 | // delete the user 84 | const deletedUser = await User.findOneAndDelete({ pi_uid }).exec(); 85 | return { 86 | user: deletedUser ? deletedUser as IUser : null, 87 | sellers: deletedSellers as ISeller[], 88 | userSetting: deletedUserSettings as IUserSettings 89 | } 90 | } catch (error) { 91 | logger.error(`Failed to delete user or user association for piUID ${ pi_uid }:`, error); 92 | throw new Error('Failed to delete user or user association; please try again later'); 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /src/services/userSettings.service.ts: -------------------------------------------------------------------------------- 1 | import UserSettings from "../models/UserSettings"; 2 | import User from "../models/User"; 3 | import { DeviceLocationType } from "../models/enums/deviceLocationType"; 4 | import { IUser, IUserSettings } from "../types"; 5 | 6 | import logger from "../config/loggingConfig"; 7 | 8 | export const getUserSettingsById = async (user_settings_id: string): Promise => { 9 | try { 10 | const userSettings = await UserSettings.findOne({ user_settings_id }).exec(); 11 | return userSettings; 12 | } catch (error) { 13 | logger.error(`Failed to retrieve user settings for userSettingsID ${ user_settings_id }:`, error); 14 | throw new Error('Failed to get user settings; please try again later'); 15 | } 16 | }; 17 | 18 | export const addOrUpdateUserSettings = async ( 19 | authUser: IUser, 20 | formData: any, 21 | image: string 22 | ): Promise => { 23 | 24 | try { 25 | // Reinstate user_name update logic 26 | if (formData.user_name?.trim() === "") { 27 | formData.user_name = authUser.pi_username; 28 | 29 | await User.findOneAndUpdate( 30 | { pi_uid: authUser.pi_uid }, 31 | { user_name: formData.user_name }, 32 | { new: true } 33 | ).exec(); 34 | } 35 | 36 | const existingUserSettings = await UserSettings.findOne({ 37 | user_settings_id: authUser.pi_uid, 38 | }).exec(); 39 | 40 | const updateData: any = {}; 41 | 42 | updateData.user_name = formData.user_name; 43 | 44 | // Handle image if provided 45 | if (image && image.trim() !== '') { 46 | updateData.image = image.trim(); 47 | } 48 | 49 | if (formData.email || formData.email?.trim() === '') { 50 | updateData.email = formData.email.trim(); 51 | } 52 | 53 | if (formData.phone_number || formData.phone_number?.trim() === '') { 54 | updateData.phone_number = formData.phone_number.trim(); 55 | } 56 | 57 | if (formData.findme) { 58 | updateData.findme = formData.findme; 59 | } 60 | 61 | if (formData.search_filters) { 62 | updateData.search_filters = JSON.parse(formData.search_filters); 63 | } 64 | 65 | if (existingUserSettings) { 66 | // Update existing settings 67 | const updatedUserSettings = await UserSettings.findOneAndUpdate( 68 | { user_settings_id: authUser.pi_uid }, 69 | { $set: updateData }, 70 | { new: true } 71 | ).exec(); 72 | 73 | return updatedUserSettings as IUserSettings; 74 | } else { 75 | // Create new settings 76 | const newUserSettings = new UserSettings({ 77 | ...updateData, 78 | user_settings_id: authUser.pi_uid, 79 | user_name: authUser.user_name || authUser.pi_username, 80 | trust_meter_rating: 100, 81 | }); 82 | 83 | const savedUserSettings = await newUserSettings.save(); 84 | return savedUserSettings as IUserSettings; 85 | } 86 | } catch (error) { 87 | logger.error('Failed to add or update user settings:', error); 88 | throw new Error('Failed to add or update user settings; please try again later'); 89 | } 90 | }; 91 | 92 | // Delete existing user settings 93 | export const deleteUserSettings = async (user_settings_id: string): Promise => { 94 | try { 95 | const deletedUserSettings = await UserSettings.findOneAndDelete({ user_settings_id: user_settings_id }).exec(); 96 | return deletedUserSettings ? deletedUserSettings as IUserSettings : null; 97 | } catch (error) { 98 | logger.error(`Failed to delete user settings for userSettingsID ${ user_settings_id }:`, error); 99 | throw new Error('Failed to delete user settings; please try again later'); 100 | } 101 | }; 102 | 103 | // Get device location, first trying GPS and then falling back to IP-based geolocation 104 | export const getDeviceLocation = async (): Promise<{ lat: number; lng: number } | null> => { 105 | if (navigator.geolocation) { 106 | try { 107 | const position = await new Promise((resolve, reject) => 108 | navigator.geolocation.getCurrentPosition(resolve, reject, { 109 | enableHighAccuracy: true, 110 | timeout: 5000, 111 | maximumAge: 0, 112 | }) 113 | ); 114 | return { lat: position.coords.latitude, lng: position.coords.longitude }; 115 | } catch (error) { 116 | logger.warn("GPS location error:", (error as GeolocationPositionError).message); 117 | // Fall back to IP-based geolocation 118 | } 119 | } 120 | logger.warn("Unable to get device location by GPS"); 121 | return null; 122 | }; 123 | 124 | // function to get location by IP address 125 | export const getLocationByIP = async (): Promise<{ lat: number; lng: number } | null> => { 126 | try { 127 | const response = await fetch('https://ipapi.co/json/'); 128 | const data = await response.json(); 129 | 130 | if (data.latitude && data.longitude) { 131 | return { lat: data.latitude, lng: data.longitude }; 132 | } 133 | logger.warn("New user search center from IP is null") 134 | return null 135 | } catch (error: any) { 136 | logger.warn('Failed to retrieve location by IP: ' + error.message) 137 | return null 138 | } 139 | }; 140 | 141 | // Function to check user search center and return appropriate location 142 | export const userLocation = async (uid: string): Promise<{ lat: number; lng: number } | null> => { 143 | const userSettings = await UserSettings.findOne({ user_settings_id: uid }).exec(); 144 | 145 | if (!userSettings) { 146 | logger.warn("User settings not found"); 147 | return null; 148 | } 149 | 150 | if (userSettings.findme === DeviceLocationType.Automatic) { 151 | try { 152 | let location = await getDeviceLocation(); 153 | logger.warn(`[GPS] from auto findme ${location}`); 154 | // set to search center if GPS not available 155 | if (!location && userSettings.search_map_center?.coordinates){ 156 | const searchCenter = userSettings.search_map_center.coordinates; 157 | location = { lng: searchCenter[0], lat: searchCenter[1] }; 158 | logger.warn(`[Search-Center] from auto findme ${location}`) 159 | } 160 | logger.warn(`[No] from auto findme ${location}`) 161 | return location; 162 | 163 | } catch (error) { 164 | logger.error("Failed to retrieve device location:", error); 165 | return null; 166 | } 167 | } 168 | 169 | if (userSettings.findme === DeviceLocationType.GPS) { 170 | try { 171 | const location = await getDeviceLocation(); 172 | logger.info("User location from GPS:", location); 173 | return location; 174 | 175 | } catch (error) { 176 | logger.error("Failed to retrieve device location from GPS:", error); 177 | return null; 178 | } 179 | } 180 | 181 | if (userSettings.findme === DeviceLocationType.SearchCenter && userSettings.search_map_center?.coordinates) { 182 | const searchCenter = userSettings.search_map_center.coordinates; 183 | const location = { lng: searchCenter[0], lat: searchCenter[1] }; 184 | logger.info("User location from search center:", location); 185 | return location as { lat: number; lng: number }; 186 | } 187 | 188 | logger.warn("Location not found"); 189 | return null; 190 | }; 191 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Document, Types } from "mongoose"; 2 | import { DeviceLocationType } from "./models/enums/deviceLocationType"; 3 | import { RatingScale } from "./models/enums/ratingScale"; 4 | import { SellerType } from "./models/enums/sellerType"; 5 | import { FulfillmentType } from "./models/enums/fulfillmentType"; 6 | import { StockLevelType } from "./models/enums/stockLevelType"; 7 | import { TrustMeterScale } from "./models/enums/trustMeterScale"; 8 | import { RestrictedArea } from "./models/enums/restrictedArea"; 9 | 10 | export interface IUser extends Document { 11 | pi_uid: string; 12 | pi_username: string; 13 | user_name: string; 14 | } 15 | 16 | export interface IUserSettings extends Document { 17 | user_settings_id: string; 18 | user_name: string; 19 | email?: string | null; 20 | phone_number?: string | null; 21 | image?: string; 22 | findme: DeviceLocationType; 23 | trust_meter_rating: TrustMeterScale; 24 | search_map_center?: { 25 | type: 'Point'; 26 | coordinates: [number, number]; 27 | }; 28 | search_filters?: { 29 | include_active_sellers: Boolean; 30 | include_inactive_sellers: Boolean; 31 | include_test_sellers: Boolean; 32 | include_trust_level_100: Boolean; 33 | include_trust_level_80: Boolean; 34 | include_trust_level_50: Boolean; 35 | include_trust_level_0: Boolean; 36 | }; 37 | } 38 | 39 | export interface ISeller extends Document { 40 | seller_id: string; 41 | name: string; 42 | seller_type: SellerType; 43 | description: string; 44 | image?: string; 45 | address?: string; 46 | average_rating: Types.Decimal128; 47 | sell_map_center: { 48 | type: 'Point'; 49 | coordinates: [number, number]; 50 | }; 51 | order_online_enabled_pref: boolean; 52 | fulfillment_method: FulfillmentType; 53 | fulfillment_description?: string; 54 | pre_restriction_seller_type?: SellerType | null; 55 | isPreRestricted: boolean; 56 | } 57 | 58 | export interface ISellerItem extends Document { 59 | _id: string; 60 | seller_id: string; 61 | name: string; 62 | description: string; 63 | price: Types.Decimal128; 64 | stock_level: StockLevelType; 65 | image?: string; 66 | duration: number; 67 | expired_by: Date; 68 | createdAt: Date; 69 | updatedAt: Date; 70 | } 71 | 72 | export interface IReviewFeedback extends Document { 73 | _id: string; 74 | review_receiver_id: string; 75 | review_giver_id: string; 76 | reply_to_review_id: string | null; 77 | rating: RatingScale; 78 | comment?: string; 79 | image?: string; 80 | review_date: Date; 81 | } 82 | 83 | export interface ISanctionedRegion extends Document { 84 | location: RestrictedArea; 85 | boundary: { 86 | type: 'Polygon'; 87 | coordinates: [[[number, number]]]; 88 | }; 89 | } 90 | 91 | export interface CompleteFeedback { 92 | givenReviews: IReviewFeedbackOutput[]; 93 | receivedReviews: IReviewFeedbackOutput[]; 94 | } 95 | 96 | export interface IMapCenter { 97 | type: 'Point'; 98 | coordinates: [number, number]; 99 | } 100 | 101 | // Select specific fields from IUserSettings 102 | export type PartialUserSettings = Pick; 103 | 104 | // Combined interface representing a seller with selected user settings 105 | export interface ISellerWithSettings extends ISeller, PartialUserSettings { 106 | } 107 | 108 | export type PartialReview = { 109 | giver: string; 110 | receiver: string; 111 | } 112 | 113 | export interface IReviewFeedbackOutput extends IReviewFeedback, PartialReview {} 114 | 115 | export type SanctionedSeller = Pick & { 116 | sanctioned_location: string, 117 | pre_restriction_seller_type?: SellerType | null 118 | }; 119 | 120 | export type SanctionedSellerStatus = { 121 | seller_id: string; 122 | pre_restriction_seller_type: SellerType | null; 123 | isSanctionedRegion: boolean; 124 | } 125 | 126 | export interface IToggle extends Document { 127 | name: string; 128 | enabled: boolean; 129 | description?: string; 130 | createdAt: Date; 131 | updatedAt: Date; 132 | } -------------------------------------------------------------------------------- /src/utils/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cookieParser from 'cookie-parser'; 3 | import cors from "cors" 4 | import dotenv from "dotenv"; 5 | import path from "path"; 6 | 7 | import docRouter from "../config/swagger"; 8 | import requestLogger from "../middlewares/logger"; 9 | 10 | import appRouter from "../routes"; 11 | import homeRoutes from "../routes/home.routes"; 12 | import userRoutes from "../routes/user.routes"; 13 | import userPreferencesRoutes from "../routes/userPreferences.routes"; 14 | import sellerRoutes from "../routes/seller.routes"; 15 | import reviewFeedbackRoutes from "../routes/reviewFeedback.routes"; 16 | import mapCenterRoutes from "../routes/mapCenter.routes"; 17 | import reportRoutes from "../routes/report.routes"; 18 | import toggleRoutes from "../routes/toggle.routes"; 19 | import restrictionRoutes from "../routes/restriction.routes"; 20 | 21 | dotenv.config(); 22 | 23 | const app = express(); 24 | 25 | app.use(express.urlencoded({ extended: true })); 26 | app.use(express.json()); 27 | app.use(requestLogger); 28 | 29 | app.use(cors({ 30 | origin: process.env.CORS_ORIGIN_URL, 31 | credentials: true 32 | })); 33 | app.use(cookieParser()); 34 | 35 | // serve static files for Swagger documentation 36 | app.use('/api/docs', express.static(path.join(__dirname, '../config/docs'))); 37 | 38 | // Swagger OpenAPI documentation 39 | app.use("/api/docs", docRouter); 40 | 41 | app.use("/api/v1", appRouter); 42 | app.use("/api/v1/users", userRoutes); 43 | app.use("/api/v1/user-preferences", userPreferencesRoutes); 44 | app.use("/api/v1/sellers", sellerRoutes); 45 | app.use("/api/v1/review-feedback", reviewFeedbackRoutes); 46 | app.use("/api/v1/map-center", mapCenterRoutes); 47 | app.use("/api/v1/reports", reportRoutes); 48 | app.use("/api/v1/toggles", toggleRoutes); 49 | app.use("/api/v1/restrictions", restrictionRoutes); 50 | 51 | app.use("/", homeRoutes); 52 | 53 | export default app; 54 | -------------------------------------------------------------------------------- /src/utils/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from 'cloudinary'; 2 | import { env } from './env'; 3 | 4 | cloudinary.config({ 5 | cloud_name: env.CLOUDINARY_CLOUD_NAME, 6 | api_key: env.CLOUDINARY_API_KEY, 7 | api_secret: env.CLOUDINARY_API_SECRET, 8 | }); 9 | 10 | export default cloudinary; 11 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | // Load environment variables from .env file 4 | dotenv.config(); 5 | 6 | export const env = { 7 | PORT: process.env.PORT || 8001, 8 | NODE_ENV: process.env.NODE_ENV || 'development', 9 | JWT_SECRET: process.env.JWT_SECRET || 'default_secret', 10 | PI_API_KEY: process.env.PI_API_KEY || '', 11 | PLATFORM_API_URL: process.env.PLATFORM_API_URL || '', 12 | ADMIN_API_USERNAME: process.env.ADMIN_API_USERNAME || '', 13 | ADMIN_API_PASSWORD: process.env.ADMIN_API_PASSWORD || '', 14 | UPLOAD_PATH: process.env.UPLOAD_PATH || '', 15 | MONGODB_URL: process.env.MONGODB_URL || '', 16 | MONGODB_MIN_POOL_SIZE: Number(process.env.MONGODB_MIN_POOL_SIZE) || 1, 17 | MONGODB_MAX_POOL_SIZE: Number(process.env.MONGODB_MAX_POOL_SIZE) || 5, 18 | SENTRY_DSN: process.env.SENTRY_DSN || '', 19 | CLOUDINARY_CLOUD_NAME: process.env.CLOUDINARY_CLOUD_NAME || '', 20 | CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY || '', 21 | CLOUDINARY_API_SECRET: process.env.CLOUDINARY_API_SECRET || '', 22 | DEVELOPMENT_URL: process.env.DEVELOPMENT_URL || '', 23 | PRODUCTION_URL: process.env.PRODUCTION_URL || '', 24 | CORS_ORIGIN_URL: process.env.CORS_ORIGIN_URL || '' 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/multer.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | 5 | import { env } from "./env"; 6 | 7 | const isProduction = process.env.NODE_ENV === 'production'; 8 | 9 | const uploadPath = isProduction ? path.join('/tmp', env.UPLOAD_PATH) : path.join(__dirname, env.UPLOAD_PATH); 10 | 11 | // define the storage configuration but delay directory creation until needed 12 | const storage = multer.diskStorage({ 13 | destination: (req, file, cb) => { 14 | // ensure the directory exists at runtime 15 | if (!fs.existsSync(uploadPath)) { 16 | fs.mkdirSync(uploadPath, { recursive: true }); 17 | } 18 | 19 | cb(null, uploadPath); 20 | }, 21 | filename: (req, file, cb) => { 22 | cb(null, `${req.currentUser?.pi_uid}${path.extname(file.originalname)}`); 23 | } 24 | }); 25 | 26 | const fileFilter = ( 27 | req: Express.Request, 28 | file: Express.Multer.File, 29 | cb: multer.FileFilterCallback 30 | ): void => { 31 | const extension = path.extname(file.originalname).toLowerCase(); 32 | if (!(extension === ".jpg" || extension === ".jpeg" || extension === ".png")) { 33 | const error: any = { 34 | code: "INVALID_FILE_TYPE", 35 | message: "Wrong format | Please upload an image with one of the following formats: .jpg, .jpeg, or .png.", 36 | }; 37 | cb(new Error(error.message)); 38 | return; 39 | } 40 | cb(null, true); 41 | }; 42 | 43 | const upload = multer({ 44 | storage, 45 | fileFilter, 46 | }); 47 | 48 | export default upload; 49 | -------------------------------------------------------------------------------- /test/controllers/admin/reportsController.spec.ts: -------------------------------------------------------------------------------- 1 | import { getSanctionedSellersReport } from '../../../src/controllers/admin/reportsController'; 2 | import * as reportService from '../../../src/services/admin/report.service'; 3 | 4 | jest.mock('../../../src/services/admin/report.service', () => ({ 5 | reportSanctionedSellers: jest.fn(), 6 | })); 7 | 8 | describe('ReportsController', () => { 9 | let req: any; 10 | let res: any; 11 | 12 | beforeEach(() => { 13 | req = {}; 14 | res = { 15 | status: jest.fn().mockReturnThis(), 16 | json: jest.fn(), 17 | }; 18 | }); 19 | 20 | describe('getSanctionedSellersReport function', () => { 21 | it('should get report for sanctioned sellers and return successful message', async () => { 22 | const expectedSanctionedSellers = [ 23 | { seller_id: '0f0f0f-0f0f-0f0f', name: 'Test Seller Sanctioned 6', address: 'Sanctioned Region Cuba', sanctioned_location: 'Cuba' }, 24 | { seller_id: '0g0g0g-0g0g-0g0g', name: 'Test Seller Sanctioned 7', address: 'Sanctioned Region Iran', sanctioned_location: 'Iran' }, 25 | ]; 26 | 27 | (reportService.reportSanctionedSellers as jest.Mock).mockResolvedValue(expectedSanctionedSellers); 28 | 29 | await getSanctionedSellersReport(req, res); 30 | expect(reportService.reportSanctionedSellers).toHaveBeenCalled(); 31 | expect(res.status).toHaveBeenCalledWith(200); 32 | expect(res.json).toHaveBeenCalledWith({ 33 | message: `${expectedSanctionedSellers.length} Sanctioned seller(s) retrieved successfully`, 34 | sanctionedSellers: expectedSanctionedSellers, 35 | }); 36 | }); 37 | 38 | it('should return appropriate [500] if get sanctioned sellers report fails', async () => { 39 | const mockError = new Error('An error occurred while generating Sanctioned Sellers Report; please try again later'); 40 | 41 | (reportService.reportSanctionedSellers as jest.Mock).mockRejectedValue(mockError); 42 | 43 | await getSanctionedSellersReport(req, res); 44 | 45 | expect(reportService.reportSanctionedSellers).toHaveBeenCalled(); 46 | expect(res.status).toHaveBeenCalledWith(500); 47 | expect(res.json).toHaveBeenCalledWith({ message: mockError.message }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/controllers/admin/restrictionController.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkSanctionStatus } from '../../../src/controllers/admin/restrictionController'; 2 | import * as restrictionService from '../../../src/services/admin/restriction.service'; 3 | import { RestrictedArea } from '../../../src/models/enums/restrictedArea'; 4 | 5 | jest.mock('../../../src/services/admin/restriction.service', () => ({ 6 | validateSellerLocation: jest.fn(), 7 | })); 8 | 9 | describe('RestrictionController', () => { 10 | let req: any; 11 | let res: any; 12 | 13 | beforeEach(() => { 14 | req = { body: {} }; 15 | res = { 16 | status: jest.fn().mockReturnThis(), 17 | json: jest.fn(), 18 | }; 19 | }); 20 | 21 | describe('checkSanctionedStatus function', () => { 22 | const mockSanctionedRegion = { 23 | location: RestrictedArea.NORTH_KOREA, 24 | boundary: { 25 | type: 'Polygon', 26 | coordinates: [[ 27 | [123.5, 37.5], 28 | [131.2, 37.5], 29 | [131.2, 43.0], 30 | [123.5, 43.0], 31 | [123.5, 37.5], 32 | ]], 33 | }, 34 | }; 35 | 36 | it('should return [200] and isSanctioned true if seller is in a sanctioned zone', async () => { 37 | req.body = { latitude: 123.5, longitude: 40.5 }; 38 | (restrictionService.validateSellerLocation as jest.Mock).mockResolvedValue(mockSanctionedRegion); 39 | 40 | await checkSanctionStatus(req, res); 41 | expect(restrictionService.validateSellerLocation).toHaveBeenCalled(); 42 | expect(res.status).toHaveBeenCalledWith(200); 43 | expect(res.json).toHaveBeenCalledWith({ 44 | message: 'Sell center is set within a sanctioned zone', 45 | isSanctioned: true, 46 | }); 47 | }); 48 | 49 | it('should return [200] and isSanctioned false if seller is not in a sanctioned zone', async () => { 50 | req.body = { latitude: 23.5, longitude: 40.5 }; 51 | (restrictionService.validateSellerLocation as jest.Mock).mockResolvedValue(null); 52 | 53 | await checkSanctionStatus(req, res); 54 | expect(restrictionService.validateSellerLocation).toHaveBeenCalled(); 55 | expect(res.status).toHaveBeenCalledWith(200); 56 | expect(res.json).toHaveBeenCalledWith({ 57 | message: 'Sell center is set within a unsanctioned zone', 58 | isSanctioned: false, 59 | }); 60 | }); 61 | 62 | it('should return [400] if latitude or longitude is missing or invalid', async () => { 63 | req.body = { latitude: "malformed", longitude: null }; 64 | 65 | await checkSanctionStatus(req, res); 66 | 67 | expect(res.status).toHaveBeenCalledWith(400); 68 | expect(res.json).toHaveBeenCalledWith({ error: "Unexpected coordinates provided" }); 69 | }); 70 | 71 | it('should return appropriate [500] if check sanction status fails', async () => { 72 | const mockError = new Error('An error occurred while checking sanction status; please try again later'); 73 | 74 | req.body = { latitude: 23.5, longitude: 40.5 }; 75 | (restrictionService.validateSellerLocation as jest.Mock).mockRejectedValue(mockError); 76 | 77 | await checkSanctionStatus(req, res); 78 | expect(restrictionService.validateSellerLocation).toHaveBeenCalled(); 79 | expect(res.status).toHaveBeenCalledWith(500); 80 | expect(res.json).toHaveBeenCalledWith({ message: mockError.message }); 81 | }); 82 | }); 83 | }); -------------------------------------------------------------------------------- /test/controllers/mapCenterController.spec.ts: -------------------------------------------------------------------------------- 1 | import { saveMapCenter } from '../../src/controllers/mapCenterController'; 2 | import * as mapCenterService from '../../src/services/mapCenter.service'; 3 | 4 | jest.mock('../../src/services/mapCenter.service', () => ({ 5 | createOrUpdateMapCenter: jest.fn(), 6 | getMapCenterById: jest.fn(), 7 | })); 8 | 9 | describe('MapCenterController', () => { 10 | let req: any; 11 | let res: any; 12 | 13 | beforeEach(() => { 14 | req = { 15 | currentUser: { 16 | pi_uid: '0a0a0a-0a0a-0a0a' 17 | }, 18 | body: { 19 | longitude: 45.123, 20 | latitude: 23.456, 21 | type: 'search' 22 | }, 23 | params: { 24 | type: 'search' 25 | } 26 | }; 27 | 28 | res = { 29 | status: jest.fn().mockReturnThis(), 30 | json: jest.fn(), 31 | }; 32 | }); 33 | 34 | describe('saveMapCenter', () => { 35 | it('should save map center successfully', async () => { 36 | const mockMapCenter = { map_center_id: '0a0a0a-0a0a-0a0a', latitude: 23.456, longitude: 45.123 }; 37 | (mapCenterService.createOrUpdateMapCenter as jest.Mock).mockResolvedValue(mockMapCenter); 38 | 39 | await saveMapCenter(req, res); 40 | 41 | expect(mapCenterService.createOrUpdateMapCenter).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a', 23.456, 45.123, 'search'); 42 | expect(res.status).toHaveBeenCalledWith(200); 43 | expect(res.json).toHaveBeenCalledWith({ uid: '0a0a0a-0a0a-0a0a', map_center: mockMapCenter }); 44 | }); 45 | 46 | it('should return appropriate [500] if saving map center fails', async () => { 47 | const mockError = new Error('An error occurred while saving the Map Center; please try again later'); 48 | 49 | (mapCenterService.createOrUpdateMapCenter as jest.Mock).mockRejectedValue(mockError); 50 | 51 | await saveMapCenter(req, res); 52 | 53 | expect(mapCenterService.createOrUpdateMapCenter).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a', 23.456, 45.123, 'search'); 54 | expect(res.status).toHaveBeenCalledWith(500); 55 | expect(res.json).toHaveBeenCalledWith({ message: mockError.message }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/controllers/reviewFeedbackController.spec.ts: -------------------------------------------------------------------------------- 1 | import { getReviews } from '../../src/controllers/reviewFeedbackController'; 2 | import * as reviewFeedbackService from '../../src/services/reviewFeedback.service'; 3 | 4 | jest.mock('../../src/services/reviewFeedback.service', () => ({ 5 | getReviewFeedback: jest.fn() 6 | })); 7 | 8 | describe('reviewFeedbackController', () => { 9 | let req: any; 10 | let res: any; 11 | 12 | const mockReviews = { 13 | givenReviews: [ 14 | { 15 | _id: '64f5a0f2a86d1f9f3b7e4e81', 16 | review_receiver_id: '0b0b0b-0b0b-0b0b', 17 | review_giver_id: '0a0a0a-0a0a-0a0a', 18 | reply_to_review_id: null, 19 | giver: 'Test_A', 20 | receiver: 'Test_B', 21 | comment: '0a0a0a-0a0a-0a0a Test Review Comment', 22 | rating: 5, 23 | image: 'http://example.com/image.jpg', 24 | review_date: '2024-10-14T00:00:00.000Z', 25 | }, 26 | ], 27 | receivedReviews: [ 28 | { 29 | _id: '64f5a0f2a86d1f9f3b7e4e82', 30 | review_receiver_id: '0a0a0a-0a0a-0a0a', 31 | review_giver_id: '0c0c0c-0c0c-0c0c', 32 | reply_to_review_id: null, 33 | giver: 'Test_C', 34 | receiver: 'Test_A', 35 | comment: '0c0c0c-0c0c-0c0c Test Review Comment', 36 | rating: 3, 37 | image: 'http://example.com/image.jpg', 38 | review_date: '2024-10-15T00:00:00.000Z', 39 | } 40 | ], 41 | }; 42 | 43 | beforeEach(() => { 44 | req = { 45 | params: { review_receiver_id: '0a0a0a-0a0a-0a0a' }, 46 | query: { searchQuery: 'Test_C' } 47 | }; 48 | 49 | res = { 50 | status: jest.fn().mockReturnThis(), 51 | json: jest.fn(), 52 | }; 53 | }); 54 | 55 | describe('getReviews', () => { 56 | it('should get associated reviews successfully if search query is not provided', async () => { 57 | req.query = {}; // no search query 58 | 59 | (reviewFeedbackService.getReviewFeedback as jest.Mock).mockResolvedValue(mockReviews); 60 | 61 | await getReviews(req, res); 62 | 63 | expect(reviewFeedbackService.getReviewFeedback).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a', undefined); 64 | expect(res.status).toHaveBeenCalledWith(200); 65 | expect(res.json).toHaveBeenCalledWith({ 66 | givenReviews: expect.any(Array), 67 | receivedReviews: expect.any(Array), 68 | }); 69 | }); 70 | 71 | it('should get associated reviews successfully if search query is provided', async () => { 72 | (reviewFeedbackService.getReviewFeedback as jest.Mock).mockResolvedValue(mockReviews); 73 | 74 | await getReviews(req, res); 75 | 76 | expect(reviewFeedbackService.getReviewFeedback).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a', 'Test_C'); 77 | expect(res.status).toHaveBeenCalledWith(200); 78 | expect(res.json).toHaveBeenCalledWith({ 79 | givenReviews: expect.any(Array), 80 | receivedReviews: expect.any(Array), 81 | }); 82 | }); 83 | 84 | it('should return appropriate [500] if retrieving reviews fails', async () => { 85 | req.query = {}; // no search query 86 | 87 | const mockError = new Error('An error occurred while getting reviews; please try again later'); 88 | 89 | (reviewFeedbackService.getReviewFeedback as jest.Mock).mockRejectedValue(mockError); 90 | 91 | await getReviews(req, res); 92 | 93 | expect(reviewFeedbackService.getReviewFeedback).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a', undefined); 94 | expect(res.status).toHaveBeenCalledWith(500); 95 | expect(res.json).toHaveBeenCalledWith({ message: mockError.message }); 96 | }); 97 | }); 98 | }); -------------------------------------------------------------------------------- /test/controllers/userController.spec.ts: -------------------------------------------------------------------------------- 1 | import { deleteUser } from '../../src/controllers/userController'; 2 | import * as userService from '../../src/services/user.service'; 3 | 4 | jest.mock('../../src/services/user.service', () => ({ 5 | deleteUser: jest.fn(), 6 | })); 7 | 8 | describe('UserController', () => { 9 | let req: any; 10 | let res: any; 11 | 12 | beforeEach(() => { 13 | req = { 14 | currentUser: { 15 | pi_uid: '0a0a0a-0a0a-0a0a', 16 | }, 17 | }; 18 | 19 | res = { 20 | status: jest.fn().mockReturnThis(), 21 | json: jest.fn(), 22 | }; 23 | }); 24 | 25 | describe('deleteUser function', () => { 26 | it('should delete user and return successful message', async () => { 27 | const expectedDeletedData = { 28 | user: { pi_uid: '0a0a0a-0a0a-0a0a' }, 29 | sellers: [{ seller_id: '0a0a0a-0a0a-0a0a' }], 30 | userSetting: { user_settings_id: '0a0a0a-0a0a-0a0a' }, 31 | }; 32 | 33 | (userService.deleteUser as jest.Mock).mockResolvedValue(expectedDeletedData); 34 | 35 | await deleteUser(req, res); 36 | 37 | expect(userService.deleteUser).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a'); 38 | expect(res.status).toHaveBeenCalledWith(200); 39 | expect(res.json).toHaveBeenCalledWith({ 40 | message: 'User deleted successfully', 41 | deletedData: expectedDeletedData, 42 | }); 43 | }); 44 | 45 | it('should return appropriate [500] if delete user fails', async () => { 46 | const mockError = new Error('An error occurred while deleting user; please try again later'); 47 | 48 | (userService.deleteUser as jest.Mock).mockRejectedValue(mockError); 49 | 50 | await deleteUser(req, res); 51 | 52 | expect(userService.deleteUser).toHaveBeenCalledWith('0a0a0a-0a0a-0a0a'); 53 | expect(res.status).toHaveBeenCalledWith(500); 54 | expect(res.json).toHaveBeenCalledWith({ message: mockError.message }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/cron/utils/geoUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createBulkPreRestrictionOperation, 3 | createGeoQueries 4 | } from "../../../src/cron/utils/geoUtils"; 5 | import { SellerType } from "../../../src/models/enums/sellerType"; 6 | import { ISanctionedRegion, ISeller } from "../../../src/types"; 7 | 8 | describe("createBulkPreRestrictionOperation function", () => { 9 | it("should create update operations for sellers in restricted regions", () => { 10 | const sellersInRestrictedRegions: Partial[] = [ 11 | { seller_id: "0a0a0a-0a0a-0a0a", seller_type: SellerType.Test }, 12 | { seller_id: "0b0b0b-0b0b-0b0b", seller_type: SellerType.Restricted }, 13 | ]; 14 | 15 | const result = createBulkPreRestrictionOperation(sellersInRestrictedRegions as ISeller[]); 16 | 17 | expect(result).toEqual([ 18 | { 19 | updateOne: { 20 | filter: { seller_id: "0a0a0a-0a0a-0a0a" }, 21 | update: { $set: { isPreRestricted: true, pre_restriction_seller_type: SellerType.Test } }, 22 | }, 23 | }, 24 | { 25 | updateOne: { 26 | filter: { seller_id: "0b0b0b-0b0b-0b0b" }, 27 | update: { $set: { isPreRestricted: true } }, 28 | }, 29 | }, 30 | ]); 31 | }); 32 | 33 | it("should return an empty array if there are no sellers", () => { 34 | const result = createBulkPreRestrictionOperation([]); 35 | 36 | expect(result).toEqual([]); 37 | }); 38 | }); 39 | 40 | describe("createGeoQueries function", () => { 41 | it("should create geo queries for sanctioned regions", () => { 42 | const sanctionedRegions: Partial [] = [ 43 | { 44 | boundary: { 45 | type: "Polygon", 46 | coordinates: [ 47 | [ 48 | [0, 0], 49 | [1, 1], 50 | [1, 0], 51 | [0, 0], 52 | ], 53 | ] as unknown as [[[number, number]]], 54 | }, 55 | location: {} as any, 56 | }, 57 | ]; 58 | 59 | const result = createGeoQueries(sanctionedRegions as ISanctionedRegion[]); 60 | 61 | expect(result).toEqual([ 62 | { 63 | sell_map_center: { 64 | $geoWithin: { 65 | $geometry: sanctionedRegions[0].boundary, 66 | }, 67 | }, 68 | }, 69 | ]); 70 | }); 71 | 72 | it("should return an empty array if no regions are passed", () => { 73 | const result = createGeoQueries([]); 74 | 75 | expect(result).toEqual([]); 76 | }); 77 | }); -------------------------------------------------------------------------------- /test/helpers/location.spec.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { reverseLocationDetails } from '../../src/helpers/location'; 3 | 4 | jest.mock('axios'); 5 | 6 | describe('reverseLocationDetails function', () => { 7 | it('should return location details from Nominatim API', async () => { 8 | // mock axios.get to return a resolved promise with the expected data 9 | (axios.get as jest.Mock).mockResolvedValue({ 10 | data: { 11 | display_name: 'Havana, Cuba' 12 | } 13 | }); 14 | 15 | const result = await reverseLocationDetails(23.1136, -82.3666); 16 | 17 | expect(axios.get).toHaveBeenCalledWith('https://nominatim.openstreetmap.org/reverse', { 18 | headers: { 19 | 'User-Agent': 'mapofpi/1.0 (mapofpi@gmail.com)', 20 | }, 21 | params: { 22 | lat: 23.1136, 23 | lon: -82.3666, 24 | zoom: 6, 25 | format: 'jsonv2', 26 | 'accept-language': 'en', 27 | }, 28 | }); 29 | 30 | expect(result).toEqual({ 31 | data: { 32 | display_name: 'Havana, Cuba' 33 | } 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | 4 | import * as mockData from './mockData.json'; 5 | import User from '../src/models/User'; 6 | import UserSettings from '../src/models/UserSettings'; 7 | import Seller from '../src/models/Seller'; 8 | import SellerItem from '../src/models/SellerItem'; 9 | import ReviewFeedback from '../src/models/ReviewFeedback'; 10 | import SanctionedRegion from '../src/models/misc/SanctionedRegion'; 11 | import Toggle from '../src/models/misc/Toggle'; 12 | 13 | // mock the Winston logger 14 | jest.mock('../src/config/loggingConfig', () => ({ 15 | debug: jest.fn(), 16 | info: jest.fn(), 17 | warn: jest.fn(), 18 | error: jest.fn(), 19 | })); 20 | 21 | // allow ample time to start running tests 22 | jest.setTimeout(100000); 23 | 24 | // MongoDB memory server setup 25 | let mongoServer: MongoMemoryServer; 26 | 27 | beforeAll(async () => { 28 | try { 29 | mongoServer = await MongoMemoryServer.create(); 30 | const uri = mongoServer.getUri(); 31 | await mongoose.connect(uri, { dbName: 'mapofpi-test-db' }); 32 | 33 | // Load the mock data into Map of PI DB collections 34 | await User.insertMany(mockData.users); 35 | await UserSettings.createIndexes(); 36 | await UserSettings.insertMany(mockData.userSettings); 37 | // Ensure indexes are created for the schema models before running tests 38 | await Seller.createIndexes(); 39 | await Seller.insertMany(mockData.sellers); 40 | await SellerItem.createIndexes(); 41 | await SellerItem.insertMany(mockData.sellerItems); 42 | await ReviewFeedback.insertMany(mockData.reviews); 43 | await SanctionedRegion.insertMany(mockData.sanctionedRegion); 44 | await Toggle.insertMany(mockData.toggle); 45 | } catch (error) { 46 | console.error('Failed to start MongoMemoryServer', error); 47 | throw error; 48 | } 49 | }); 50 | 51 | afterAll(async () => { 52 | await mongoose.disconnect(); 53 | await mongoServer.stop(); 54 | }); 55 | -------------------------------------------------------------------------------- /test/middlewares/isToggle.spec.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { isToggle } from "../../src/middlewares/isToggle"; 3 | import Toggle from "../../src/models/misc/Toggle"; 4 | 5 | describe("isToggle function", () => { 6 | let req: Partial; 7 | let res: Partial; 8 | let next: NextFunction; 9 | 10 | beforeEach(() => { 11 | req = {}; 12 | res = { 13 | status: jest.fn().mockReturnThis(), 14 | json: jest.fn(), 15 | }; 16 | next = jest.fn(); 17 | }); 18 | 19 | it("should pass middleware if expected toggle is enabled", async () => { 20 | const middleware = isToggle("testToggle_1"); 21 | await middleware(req as Request, res as Response, next); 22 | 23 | expect(next).toHaveBeenCalled(); 24 | expect(res.status).not.toHaveBeenCalled(); 25 | }); 26 | 27 | it("should return 403 if expected toggle is disabled", async () => { 28 | const middleware = isToggle("testToggle"); 29 | await middleware(req as Request, res as Response, next); 30 | 31 | expect(res.status).toHaveBeenCalledWith(403); 32 | expect(res.json).toHaveBeenCalledWith({ 33 | message: "Feature is currently disabled", 34 | }); 35 | expect(next).not.toHaveBeenCalled(); 36 | }); 37 | 38 | it("should return 403 if expected toggle is not found", async () => { 39 | const middleware = isToggle("testToggle_nonExisting"); 40 | await middleware(req as Request, res as Response, next); 41 | 42 | expect(res.status).toHaveBeenCalledWith(403); 43 | expect(res.json).toHaveBeenCalledWith({ 44 | message: "Feature is currently disabled", 45 | }); 46 | expect(next).not.toHaveBeenCalled(); 47 | }); 48 | 49 | it("should return 500 if an exception occurs", async () => { 50 | const toggleName = "testToggle_nonExisting"; 51 | const mockError = new Error(`Failed to fetch toggle ${ toggleName }; please try again later`); 52 | 53 | const findOneSpy = jest.spyOn(Toggle, 'findOne').mockRejectedValue(mockError); 54 | 55 | const middleware = isToggle(toggleName); 56 | await middleware(req as Request, res as Response, next); 57 | 58 | expect(res.status).toHaveBeenCalledWith(500); 59 | expect(res.json).toHaveBeenCalledWith({ 60 | message: 'Failed to determine feature state; please try again later', 61 | }); 62 | expect(next).not.toHaveBeenCalled(); 63 | 64 | // Restore original method to avoid affecting other tests 65 | findOneSpy.mockRestore(); 66 | }); 67 | }); -------------------------------------------------------------------------------- /test/middlewares/verifyToken.spec.ts: -------------------------------------------------------------------------------- 1 | import { verifyAdminToken } from "../../src/middlewares/verifyToken"; 2 | 3 | describe("verifyAdminToken function", () => { 4 | let req: any; 5 | let res: any; 6 | let mockNext: jest.Mock; 7 | 8 | process.env.ADMIN_API_USERNAME = "validUsername"; 9 | process.env.ADMIN_API_PASSWORD = "validPassword"; 10 | 11 | beforeEach(() => { 12 | req = { 13 | headers: {}, 14 | }; 15 | 16 | res = { 17 | status: jest.fn().mockReturnThis(), 18 | json: jest.fn(), 19 | }; 20 | 21 | mockNext = jest.fn(); 22 | }); 23 | 24 | it("should return 401 if no admin credentials are provided", () => { 25 | verifyAdminToken(req, res, mockNext); 26 | 27 | expect(res.status).toHaveBeenCalledWith(401); 28 | expect(res.json).toHaveBeenCalledWith({ 29 | message: "Unauthorized", 30 | }); 31 | expect(mockNext).not.toHaveBeenCalled(); 32 | }); 33 | 34 | it("should return 401 if incorrect admin credentials are provided", () => { 35 | req.headers.authorization = `Basic ${Buffer.from("invalidUsername:invalidPassword").toString("base64")}`; 36 | 37 | verifyAdminToken(req, res, mockNext); 38 | 39 | expect(res.status).toHaveBeenCalledWith(401); 40 | expect(res.json).toHaveBeenCalledWith({ 41 | message: "Unauthorized", 42 | }); 43 | expect(mockNext).not.toHaveBeenCalled(); 44 | }); 45 | 46 | it("should pass middleware if admin credentials are valid", () => { 47 | req.headers.authorization = `Basic ${Buffer.from("validUsername:validPassword").toString("base64")}`; 48 | 49 | verifyAdminToken(req, res, mockNext); 50 | 51 | expect(mockNext).toHaveBeenCalled(); 52 | expect(res.status).not.toHaveBeenCalled(); 53 | expect(res.json).not.toHaveBeenCalled(); 54 | }); 55 | }); -------------------------------------------------------------------------------- /test/routes/home.routes.spec.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../../src/utils/app"; 3 | 4 | describe("Successful request", () => { 5 | it('should return a 200 status and a message', async () => { 6 | const response = await request(app).get('/'); 7 | expect(response.status).toBe(200); 8 | expect(response.body).toEqual({ message: 'Server is running' }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/services/admin/report.service.spec.ts: -------------------------------------------------------------------------------- 1 | import Seller from '../../../src/models/Seller'; 2 | import { reverseLocationDetails } from '../../../src/helpers/location'; 3 | import { RestrictedAreaBoundaries } from '../../../src/models/enums/restrictedArea'; 4 | import { reportSanctionedSellers } from '../../../src/services/admin/report.service'; 5 | 6 | jest.mock('../../../src/helpers/location', () => ({ 7 | reverseLocationDetails: jest.fn() 8 | })); 9 | 10 | describe('reportSanctionedSellers function', () => { 11 | it('should build sanctioned sellers report for affected sellers', async () => { 12 | (reverseLocationDetails as jest.Mock) 13 | .mockResolvedValueOnce({ data: { display_name: 'Cuba' } }) 14 | .mockResolvedValueOnce({ data: { display_name: 'Iran' } }) 15 | .mockResolvedValueOnce({ data: { display_name: 'North Korea' } }) 16 | .mockResolvedValueOnce({ data: { display_name: 'Syria' } }) 17 | .mockResolvedValueOnce({ data: { display_name: 'Republic of Crimea' } }) 18 | .mockResolvedValueOnce({ data: { display_name: 'Donetsk Oblast' } }) 19 | .mockResolvedValueOnce({ data: { display_name: 'Luhansk Oblast' } }) 20 | .mockResolvedValue({ data: { display_name: 'Russia' } }); 21 | 22 | const sanctionedSellers = await reportSanctionedSellers(); 23 | 24 | // calculate the total expected count of sellers in all restricted areas 25 | const totalExpectedCount = await Promise.all( 26 | Object.values(RestrictedAreaBoundaries).map((region) => 27 | Seller.countDocuments({ 28 | sell_map_center: { 29 | $geoWithin: { 30 | $geometry: region, 31 | }, 32 | }, 33 | }) 34 | ) 35 | ).then((counts) => counts.reduce((sum, count) => sum + count, 0)); 36 | 37 | expect(reverseLocationDetails).toHaveBeenCalledTimes(totalExpectedCount); 38 | expect(reverseLocationDetails).toHaveBeenCalledWith(expect.any(Number), expect.any(Number)); 39 | expect(sanctionedSellers).toHaveLength(totalExpectedCount); 40 | }); 41 | }); -------------------------------------------------------------------------------- /test/services/admin/restriction.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { validateSellerLocation } from '../../../src/services/admin/restriction.service'; 2 | import SanctionedRegion from '../../../src/models/misc/SanctionedRegion'; 3 | import { RestrictedArea } from '../../../src/models/enums/restrictedArea'; 4 | 5 | jest.mock('../../../src/models/misc/SanctionedRegion'); 6 | 7 | describe('validateSellerLocation function', () => { 8 | const longitude = 123.5; 9 | const latitude = 40.5; 10 | 11 | const mockSanctionedRegion = { 12 | location: RestrictedArea.NORTH_KOREA, 13 | boundary: { 14 | type: 'Polygon', 15 | coordinates: [[ 16 | [123.5, 37.5], 17 | [131.2, 37.5], 18 | [131.2, 43.0], 19 | [123.5, 43.0], 20 | [123.5, 37.5], 21 | ]], 22 | }, 23 | }; 24 | 25 | it('should return a sanctioned region given the coordinates is found', async () => { 26 | (SanctionedRegion.findOne as jest.Mock).mockReturnValue({ 27 | exec: jest.fn().mockResolvedValue(mockSanctionedRegion), 28 | }); 29 | 30 | const result = await validateSellerLocation(longitude, latitude); 31 | 32 | expect(SanctionedRegion.findOne).toHaveBeenCalledWith({ 33 | boundary: { 34 | $geoIntersects: { 35 | $geometry: { 36 | type: 'Point', 37 | coordinates: [longitude, latitude], 38 | }, 39 | }, 40 | }, 41 | }); 42 | 43 | expect(result).toEqual(mockSanctionedRegion); 44 | }); 45 | 46 | it('should return null if no sanctioned region given the coordinates is found', async () => { 47 | (SanctionedRegion.findOne as jest.Mock).mockReturnValue({ 48 | exec: jest.fn().mockResolvedValue(null), 49 | }); 50 | 51 | const result = await validateSellerLocation(longitude, latitude); 52 | expect(result).toBeNull(); 53 | }); 54 | }); -------------------------------------------------------------------------------- /test/services/admin/toggle.service.spec.ts: -------------------------------------------------------------------------------- 1 | import Toggle from '../../../src/models/misc/Toggle'; 2 | import { 3 | getToggles, 4 | getToggleByName, 5 | addToggle, 6 | updateToggle, 7 | deleteToggleByName 8 | } from '../../../src/services/admin/toggle.service'; 9 | import { IToggle } from '../../../src/types'; 10 | 11 | describe('getToggles function', () => { 12 | it('should fetch all existing toggles', async () => { 13 | const toggleData = await getToggles(); 14 | expect(toggleData).toHaveLength(2); 15 | }); 16 | 17 | it('should throw an error when an exception occurs', async () => { 18 | // Mock the Toggle model to throw an error 19 | jest.spyOn(Toggle, 'find').mockImplementationOnce(() => { 20 | throw new Error('Mock database error'); 21 | }); 22 | 23 | await expect(getToggles()).rejects.toThrow( 24 | 'Failed to get toggles; please try again later' 25 | ); 26 | }); 27 | }); 28 | 29 | describe('getToggleByName function', () => { 30 | const expectedToggle = { 31 | name: "testToggle", 32 | enabled: false, 33 | description: "Toggle for testing" 34 | } 35 | 36 | it('should fetch the corresponding toggle', async () => { 37 | const toggleData = await getToggleByName("testToggle"); 38 | expect(toggleData).toEqual(expect.objectContaining(expectedToggle)); 39 | }); 40 | 41 | it('should return null if the corresponding toggle does not exist', async () => { 42 | const toggleData = await getToggleByName("testUnknownToggle"); 43 | expect(toggleData).toBeNull(); 44 | }); 45 | 46 | it('should throw an error when an exception occurs', async () => { 47 | // Mock the Toggle model to throw an error 48 | jest.spyOn(Toggle, 'findOne').mockImplementationOnce(() => { 49 | throw new Error('Mock database error'); 50 | }); 51 | 52 | await expect(getToggleByName("testToggle")).rejects.toThrow( 53 | 'Failed to get toggle; please try again later' 54 | ); 55 | }); 56 | }); 57 | 58 | describe('addToggle function', () => { 59 | const existingToggle = { 60 | name: "testToggle", 61 | enabled: false, 62 | description: "Toggle for testing" 63 | } as IToggle; 64 | 65 | const newToggle = { 66 | name: "testToggle_2", 67 | enabled: true, 68 | description: "Toggle for testing_2" 69 | } as IToggle; 70 | 71 | it('should successfully add the new toggle', async () => { 72 | const toggleData = await addToggle(newToggle); 73 | expect(toggleData).toEqual(expect.objectContaining(newToggle)); 74 | }); 75 | 76 | it('should throw an error if a toggle with the same name already exists', async () => { 77 | await expect(addToggle(existingToggle)).rejects.toThrow( 78 | `A toggle with the identifier ${existingToggle.name} already exists.` 79 | ); 80 | }); 81 | 82 | it('should throw an error when an exception occurs', async () => { 83 | // Mock the Toggle model to throw an error 84 | jest.spyOn(Toggle, 'findOne').mockImplementationOnce(() => { 85 | throw new Error('Mock database error'); 86 | }); 87 | 88 | await expect(addToggle({ 89 | name: "testToggle_3", 90 | enabled: true, 91 | description: "Toggle for testing_3" 92 | } as IToggle)).rejects.toThrow('Failed to add toggle; please try again later'); 93 | }); 94 | }); 95 | 96 | describe('updateToggle function', () => { 97 | const updatedToggle = { 98 | name: "testToggle", 99 | enabled: true, 100 | description: "Toggle for testing updated" 101 | } as IToggle; 102 | 103 | it('should successfully update the toggle when it exists', async () => { 104 | const toggleData = await updateToggle("testToggle", true, "Toggle for testing updated"); 105 | expect(toggleData).toEqual(expect.objectContaining(updatedToggle)); 106 | }); 107 | 108 | it('should successfully update the existing toggle when description is not provided', async () => { 109 | const expectedToggle = { 110 | name: "testToggle", 111 | enabled: false 112 | } as IToggle; 113 | 114 | const toggleData = await updateToggle("testToggle", false); 115 | expect(toggleData).toEqual(expect.objectContaining(expectedToggle)); 116 | }); 117 | 118 | it('should throw an error if the corresponding toggle does not exist', async () => { 119 | await expect(updateToggle("testUnknownToggle", false, "")).rejects.toThrow( 120 | `A toggle with the identifier testUnknownToggle does not exist.` 121 | ); 122 | }); 123 | 124 | it('should throw an error when an exception occurs', async () => { 125 | // Mock the Toggle model to throw an error 126 | jest.spyOn(Toggle, 'findOneAndUpdate').mockImplementationOnce(() => { 127 | throw new Error('Mock database error'); 128 | }); 129 | 130 | await expect(updateToggle("testToggle", false, "")).rejects.toThrow('Failed to update toggle; please try again later'); 131 | }); 132 | }); 133 | 134 | describe('deleteToggleByName function', () => { 135 | const deletedToggle = { 136 | name: "testToggle_1", 137 | enabled: true, 138 | description: "Toggle for testing_1" 139 | } 140 | 141 | it('should delete the corresponding toggle', async () => { 142 | const toggleData = await deleteToggleByName("testToggle_1"); 143 | expect(toggleData).toEqual(expect.objectContaining(deletedToggle)); 144 | }); 145 | 146 | it('should return null if the corresponding toggle does not exist', async () => { 147 | const toggleData = await deleteToggleByName("testUnknownToggle"); 148 | expect(toggleData).toBeNull(); 149 | }); 150 | 151 | it('should throw an error when an exception occurs', async () => { 152 | // Mock the Toggle model to throw an error 153 | jest.spyOn(Toggle, 'findOneAndDelete').mockImplementationOnce(() => { 154 | throw new Error('Mock database error'); 155 | }); 156 | 157 | await expect(deleteToggleByName("testToggle_1")).rejects.toThrow( 158 | 'Failed to delete toggle; please try again later' 159 | ); 160 | }); 161 | }); -------------------------------------------------------------------------------- /test/services/mapCenter.service.spec.ts: -------------------------------------------------------------------------------- 1 | import Seller from '../../src/models/Seller'; 2 | import UserSettings from '../../src/models/UserSettings'; 3 | import { getMapCenterById, createOrUpdateMapCenter } from '../../src/services/mapCenter.service'; 4 | 5 | describe('getMapCenterById function', () => { 6 | it('should fetch the sell map center for the given seller ID', async () => { 7 | const sellerData = await Seller.findOne({ seller_id: '0a0a0a-0a0a-0a0a' }); 8 | 9 | const result = await getMapCenterById('0a0a0a-0a0a-0a0a', 'sell'); 10 | 11 | expect(result).toBeDefined(); 12 | // assert that the result matches the expected sell map center 13 | expect(result).toEqual(expect.objectContaining(sellerData!.sell_map_center)); 14 | }); 15 | 16 | it('should fetch the search map center for the given user settings ID', async () => { 17 | const userSettingsData = await UserSettings.findOne({ user_settings_id: '0b0b0b-0b0b-0b0b' }); 18 | 19 | const result = await getMapCenterById('0b0b0b-0b0b-0b0b', 'search'); 20 | 21 | expect(result).toBeDefined(); 22 | // assert that the result matches the expected search map center 23 | expect(result).toEqual(expect.objectContaining(userSettingsData!.search_map_center)); 24 | }); 25 | 26 | it('should return null for a non-existent map center ID', async () => { 27 | const randomType = ['sell', 'search'][Math.floor(Math.random() * 2)]; 28 | 29 | const result = await getMapCenterById('0x0x0x-0x0x-0x0x', randomType); 30 | expect(result).toBeNull(); 31 | }); 32 | }); 33 | 34 | describe('createOrUpdateMapCenter function', () => { 35 | it('should update the appropriate search_map_center instance for the given user settings ID', async () => { 36 | const updatedSearchMapCenter = { 37 | map_center_id: '0b0b0b-0b0b-0b0b', 38 | latitude: 42.8781, 39 | longitude: -88.6298 40 | }; 41 | 42 | const result = await createOrUpdateMapCenter( 43 | '0b0b0b-0b0b-0b0b', 44 | updatedSearchMapCenter.latitude, 45 | updatedSearchMapCenter.longitude, 46 | 'search' 47 | ); 48 | 49 | expect(result).toBeDefined(); 50 | // assert that the search_map_center has been updated with new coordinates 51 | expect(result?.coordinates).toEqual([updatedSearchMapCenter.longitude, updatedSearchMapCenter.latitude]); 52 | }); 53 | 54 | it('should update the appropriate sell_map_center instance for the given seller ID', async () => { 55 | const updatedSellMapCenter = { 56 | map_center_id: '0a0a0a-0a0a-0a0a', 57 | longitude: -88.6298, 58 | latitude: 42.8781 59 | }; 60 | 61 | const result = await createOrUpdateMapCenter( 62 | '0a0a0a-0a0a-0a0a', 63 | updatedSellMapCenter.latitude, 64 | updatedSellMapCenter.longitude, 65 | 'sell' 66 | ); 67 | 68 | expect(result).toBeDefined(); 69 | // assert that the sell_map_center has been updated with new coordinates 70 | expect(result?.coordinates).toEqual([updatedSellMapCenter.longitude, updatedSellMapCenter.latitude]); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/services/userSettings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { addOrUpdateUserSettings } from '../../src/services/userSettings.service'; 2 | import User from '../../src/models/User'; 3 | import { DeviceLocationType } from '../../src/models/enums/deviceLocationType'; 4 | import { IUser, IUserSettings } from '../../src/types'; 5 | 6 | const formData = { 7 | user_name: 'test-user-1-updated', 8 | email: 'example-new@test.com', 9 | phone_number: '123-456-7890', 10 | image: 'http://example.com/image_new.jpg', 11 | findme: DeviceLocationType.GPS, 12 | search_map_center: { type: 'Point', coordinates: [-83.856077, 50.848447] } 13 | } 14 | 15 | describe('addOrUpdateUserSettings function', () => { 16 | it('should add new user settings when user_name is not empty', async () => { 17 | const userData = await User.findOne({ pi_username: 'TestUser1' }) as IUser; 18 | 19 | const userSettingsData = await addOrUpdateUserSettings(userData, formData, formData.image ?? ''); 20 | 21 | expect(userSettingsData).toEqual(expect.objectContaining({ 22 | user_settings_id: userData.pi_uid, 23 | user_name: formData.user_name, 24 | email: formData.email, 25 | phone_number: formData.phone_number, 26 | image: formData.image, 27 | findme: formData.findme, 28 | search_map_center: formData.search_map_center 29 | })); 30 | }); 31 | 32 | it('should add new user settings when user_name is empty', async () => { 33 | const userData = await User.findOne({ pi_username: 'TestUser1' }) as IUser; 34 | 35 | const userSettingsData = await addOrUpdateUserSettings( 36 | userData, { 37 | ...formData, user_name: "" 38 | } as IUserSettings, formData.image ?? ''); 39 | 40 | expect(userSettingsData).toEqual(expect.objectContaining({ 41 | user_settings_id: userData.pi_uid, 42 | user_name: userData.pi_username, 43 | email: formData.email, 44 | phone_number: formData.phone_number, 45 | image: formData.image, 46 | findme: formData.findme, 47 | search_map_center: formData.search_map_center 48 | })); 49 | }); 50 | 51 | it('should update existing user settings', async () => { 52 | const userData = await User.findOne({ pi_username: 'TestUser1' }) as IUser; 53 | 54 | const updatedUserSettingsData = { 55 | user_name: formData.user_name, 56 | email: formData.email, 57 | phone_number: formData.phone_number, 58 | image: formData.image, 59 | findme: formData.findme, 60 | search_map_center: formData.search_map_center 61 | } as IUserSettings; 62 | 63 | const userSettingsData = await addOrUpdateUserSettings(userData, updatedUserSettingsData, updatedUserSettingsData.image ?? ''); 64 | 65 | expect(userSettingsData).toEqual(expect.objectContaining({ 66 | user_settings_id: userData.pi_uid, 67 | user_name: updatedUserSettingsData.user_name, 68 | email: updatedUserSettingsData.email, 69 | phone_number: updatedUserSettingsData.phone_number, 70 | image: updatedUserSettingsData.image, 71 | findme: updatedUserSettingsData.findme, 72 | search_map_center: updatedUserSettingsData.search_map_center 73 | })); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/index.ts", 6 | "use": "@vercel/node", 7 | "config": { 8 | "includeFiles": [ 9 | "node_modules/swagger-ui-dist/**" 10 | ] 11 | } 12 | } 13 | ], 14 | "routes": [ 15 | { 16 | "src": "/api/docs/swagger-ui/(.*)", 17 | "dest": "/node_modules/swagger-ui-dist/$1" 18 | }, 19 | { 20 | "src": "/(.*)", 21 | "dest": "/src/index.ts" 22 | } 23 | ] 24 | } 25 | --------------------------------------------------------------------------------