├── .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 | [](https://github.com/pi-apps/PiOS/blob/main/pi-commerce.md)
6 | 
7 | 
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 | |
|
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 |
--------------------------------------------------------------------------------