├── .env.example ├── .eslintrc.js ├── .github └── workflows │ ├── run-e2e-tests.yml │ └── run-unit-tests.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .swcrc ├── README.md ├── assets └── swagger.png ├── docker-compose.yml ├── jest-e2e.json ├── jest-unit.json ├── nest-cli.json ├── package.json ├── prisma ├── migrations │ ├── 20240223004456_init │ │ └── migration.sql │ ├── 20240223173924_added_more_tables_and_relations │ │ └── migration.sql │ ├── 20240223204255_added_more_data │ │ └── migration.sql │ ├── 20240223214043_removed_qtd_of_items │ │ └── migration.sql │ ├── 20240306195210_added_payment_options │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── src ├── app.module.ts ├── application │ └── ecommerce │ │ ├── ecommerce.module.ts │ │ ├── ports │ │ ├── order-product.repositoy.ts │ │ ├── order.repositoy.ts │ │ ├── payment.repositoy.ts │ │ ├── product.repositoy.ts │ │ └── user.repositoy.ts │ │ └── use-case │ │ ├── checkout-complete.ts │ │ ├── checkout-url.ts │ │ ├── create-order.ts │ │ ├── create-product.ts │ │ ├── create-user.ts │ │ ├── get-order.ts │ │ ├── get-orders.ts │ │ ├── get-product.ts │ │ └── get-user.ts ├── core │ └── entities │ │ ├── entity.ts │ │ ├── unique-entity-id.ts │ │ └── value-object.ts ├── domain │ └── ecommerce │ │ ├── order-product.ts │ │ ├── order.ts │ │ ├── product.ts │ │ └── user.ts ├── infra │ ├── env │ │ ├── env.module.ts │ │ ├── env.service.ts │ │ ├── env.service.unit-spec.ts │ │ ├── env.ts │ │ └── index.ts │ ├── http │ │ ├── app.controller.ts │ │ ├── checkout.controller.ts │ │ ├── dto │ │ │ ├── create-order-product.dto.ts │ │ │ ├── create-order.dto.ts │ │ │ ├── create-product.dto.ts │ │ │ └── create-user.dto.ts │ │ ├── http.module.ts │ │ ├── order.controller.ts │ │ ├── product.controller.ts │ │ ├── user.controller-e2e-spec.ts │ │ └── user.controller.ts │ ├── payment │ │ ├── payment.module.ts │ │ └── stripe │ │ │ ├── stripe-payment.repositoy.ts │ │ │ ├── stripe.module.ts │ │ │ └── stripe.service.ts │ └── persistence │ │ ├── cache │ │ ├── cache.module.ts │ │ └── interceptor │ │ │ └── http-cache.interceptor.ts │ │ ├── mongoose │ │ ├── entities │ │ │ ├── order-product.entity.ts │ │ │ ├── order.entity.ts │ │ │ ├── product.entity.ts │ │ │ └── user.entity.ts │ │ ├── mapper │ │ │ ├── mongoose-order-details-mapper.ts │ │ │ ├── mongoose-order-mapper.ts │ │ │ ├── mongoose-order-product-mapper.ts │ │ │ ├── mongoose-product-mapper.ts │ │ │ ├── mongoose-user-details-mapper.ts │ │ │ └── mongoose-user-mapper.ts │ │ ├── mongoose.module.ts │ │ └── repositories │ │ │ ├── mongoose-order-product.repositoy.ts │ │ │ ├── mongoose-order.repositoy.ts │ │ │ ├── mongoose-product.repositoy.ts │ │ │ └── mongoose-user.repositoy.ts │ │ ├── persistence.module.ts │ │ └── prisma │ │ ├── mapper │ │ ├── prisma-order-details-mapper.ts │ │ ├── prisma-order-mapper.ts │ │ ├── prisma-order-product-mapper.ts │ │ ├── prisma-product-mapper.ts │ │ ├── prisma-user-details-mapper.ts │ │ └── prisma-user-mapper.ts │ │ ├── prisma.module.ts │ │ ├── prisma.service.ts │ │ └── repositories │ │ ├── prisma-order-product.repositoy.ts │ │ ├── prisma-order.repositoy.ts │ │ ├── prisma-product.repositoy.ts │ │ └── prisma-user.repositoy.ts └── main.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://docker:docker@localhost:5432/project?schema=public" 2 | MONGODB_URL="mongodb://localhost:27017/project" 3 | 4 | CACHE_TTL=5 5 | CACHE_MAX=10 6 | 7 | STRIPE_API_KEY=test 8 | STRIPE_WEBHOOK_SECRET=test 9 | CHECKOUT_SUCCESS_URL=http://localhost:3000/api 10 | 11 | PORT=3000 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin', 'prettier'], 9 | extends: ['plugin:@typescript-eslint/recommended'], 10 | root: true, 11 | env: { 12 | node: true, 13 | jest: true, 14 | }, 15 | ignorePatterns: ['.eslintrc.js'], 16 | rules: { 17 | '@typescript-eslint/interface-name-prefix': 'off', 18 | '@typescript-eslint/explicit-function-return-type': 'off', 19 | '@typescript-eslint/explicit-module-boundary-types': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/run-e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run E2E Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run-e2e-tests: 7 | name: Run E2E Tests 8 | runs-on: ubuntu-latest 9 | 10 | services: 11 | mongo: 12 | image: mongo 13 | ports: 14 | - 27017:27017 15 | env: 16 | MONGODB_DATABASE: project 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v2 24 | with: 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }} 26 | path: node_modules 27 | 28 | - name: Use Node 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: 19.0.1 32 | 33 | - run: npm i --force 34 | 35 | - run: npm run test:e2e 36 | -------------------------------------------------------------------------------- /.github/workflows/run-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | unit-test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 12 | 13 | - name: Cache dependencies 14 | uses: actions/cache@v2 15 | with: 16 | key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }} 17 | path: node_modules 18 | 19 | - name: Use Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 19.0.1 23 | 24 | - name: Install Dependencies 25 | run: npm i --force 26 | 27 | - name: Unit testing 28 | run: | 29 | npm run test:unit 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "parser": { 6 | "syntax": "typescript", 7 | "decorators": true, 8 | "dynamicImport": true 9 | }, 10 | "transform": { 11 | "legacyDecorator": true, 12 | "decoratorMetadata": true 13 | }, 14 | "paths": { 15 | "@app/core": [ 16 | "src/core" 17 | ], 18 | "@app/core/*": [ 19 | "src/core/*" 20 | ], 21 | "@app/application": [ 22 | "src/application" 23 | ], 24 | "@app/application/*": [ 25 | "src/application/*" 26 | ], 27 | "@app/domain": [ 28 | "src/domain" 29 | ], 30 | "@app/domain/*": [ 31 | "src/domain/*" 32 | ], 33 | "@app/infra": [ 34 | "src/infra" 35 | ], 36 | "@app/infra/*": [ 37 | "src/infra/*" 38 | ] 39 | }, 40 | "baseUrl": "./" 41 | }, 42 | "minify": false 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS e-commerce 2 | 3 | Example to exercise clean architecture with different databases + cache layer + DDD. 4 | 5 | [![Run E2E Tests](https://github.com/henriqueweiand/nestjs-ecommerce/actions/workflows/run-e2e-tests.yml/badge.svg)](https://github.com/henriqueweiand/nestjs-ecommerce/actions/workflows/run-e2e-tests.yml) 6 | [![Run Unit Tests](https://github.com/henriqueweiand/nestjs-ecommerce/actions/workflows/run-unit-tests.yml/badge.svg)](https://github.com/henriqueweiand/nestjs-ecommerce/actions/workflows/run-unit-tests.yml) 7 | 8 | blog post about the project -> https://medium.com/nestjs-ninja/mastering-nestjs-unleashing-the-power-of-clean-architecture-and-ddd-in-e-commerce-development-97850131fd87 9 | 10 | ## Pre requirements 11 | 12 | - To use the full project, you will need to have a Stripe dev account; 13 | - Mongo or Postgres dabatase; 14 | 15 | ## Running locally 16 | 17 | 1. Instal the dependecies 18 | 2. copy .env.example to .env' 19 | 3. run `docker-compose up -d`, it will create a Mongo instance 20 | 4. run `yarn start:dev` 21 | 5. Access `http://localhost:3000/api` 22 | 23 | The default database is set Mongo, but it can be changed inside `app.module.ts` 24 | 25 | ## API Documentation 26 | 27 | Running the solution, access `http://localhost:3000/api` 28 | 29 | ![Preview](https://github.com/henriqueweiand/nestjs-ecommerce/blob/master/assets/swagger.png) 30 | 31 | ## To-do 32 | 33 | - [x] Product 34 | - [x] User 35 | - [x] Order 36 | - [x] Add Mongo 37 | - [x] Add Postgres 38 | - [x] Add way to switch database easly 39 | - [x] Cache layer 40 | - [x] A few Unit tests 41 | - [x] A few e2e tests 42 | - [x] Stripe integration 43 | -------------------------------------------------------------------------------- /assets/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henriqueweiand/nestjs-ecommerce/f05f1a91e4a49c94ac4b63f9b832ce06e8e7dade/assets/swagger.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: bitnami/postgresql 5 | ports: 6 | - 5432:5432 7 | environment: 8 | - POSTGRESQL_USERNAME=docker 9 | - POSTGRESQL_PASSWORD=docker 10 | - POSTGRESQL_DATABASE=project 11 | 12 | mongo: 13 | image: mongo 14 | restart: always 15 | ports: 16 | - 27017:27017 17 | environment: 18 | MONGODB_DATABASE: project 19 | -------------------------------------------------------------------------------- /jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s?$": [ 12 | "@swc/jest" 13 | ] 14 | }, 15 | "moduleNameMapper": { 16 | "@app/core/(.*)": "/src/core/$1", 17 | "@app/core": "/src/core", 18 | "@app/application/(.*)": "/src/application/$1", 19 | "@app/application": "/src/application", 20 | "@app/domain/(.*)": "/src/domain/$1", 21 | "@app/domain": "/src/domain", 22 | "@app/infra/(.*)": "/src/infra/$1", 23 | "@app/infra": "/src/infra", 24 | "@app/(.*)": "/src/$1", 25 | "@app/": "/src/" 26 | } 27 | } -------------------------------------------------------------------------------- /jest-unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".unit-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": [ 12 | "@swc/jest" 13 | ] 14 | }, 15 | "moduleNameMapper": { 16 | "@app/core/(.*)": "/src/core/$1", 17 | "@app/core": "/src/core", 18 | "@app/application/(.*)": "/src/application/$1", 19 | "@app/application": "/src/application", 20 | "@app/domain/(.*)": "/src/domain/$1", 21 | "@app/domain": "/src/domain", 22 | "@app/infra/(.*)": "/src/infra/$1", 23 | "@app/infra": "/src/infra", 24 | "@app/(.*)": "/src/$1", 25 | "@app/": "/src/" 26 | } 27 | } -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "builder": "swc", 8 | "plugins": [ 9 | "@nestjs/swagger/plugin" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-ecommerce", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:unit": "jest --config ./jest-unit.json", 21 | "test:e2e": "jest --config ./jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/cache-manager": "2.2.1", 25 | "@nestjs/common": "^10.0.0", 26 | "@nestjs/config": "3.2.0", 27 | "@nestjs/core": "^10.0.0", 28 | "@nestjs/mongoose": "10.0.4", 29 | "@nestjs/platform-express": "^10.0.0", 30 | "@nestjs/serve-static": "4.0.1", 31 | "@nestjs/swagger": "7.3.0", 32 | "@prisma/client": "5.10.2", 33 | "cache-manager": "5.4.0", 34 | "class-transformer": "0.5.1", 35 | "class-validator": "0.14.1", 36 | "mongoose": "8.2.0", 37 | "reflect-metadata": "^0.2.0", 38 | "rxjs": "^7.8.1", 39 | "stripe": "14.19.0", 40 | "uuid": "9.0.1", 41 | "@nestjs/devtools-integration": "0.1.6", 42 | "zod": "3.22.4" 43 | }, 44 | "devDependencies": { 45 | "@nestjs/cli": "^10.0.0", 46 | "@nestjs/schematics": "^10.0.0", 47 | "@nestjs/testing": "^10.0.0", 48 | "@swc/cli": "0.3.9", 49 | "@swc/core": "1.4.2", 50 | "@swc/jest": "0.2.36", 51 | "@types/express": "^4.17.17", 52 | "@types/jest": "^29.5.2", 53 | "@types/node": "^20.3.1", 54 | "@types/supertest": "^6.0.0", 55 | "@typescript-eslint/eslint-plugin": "^6.0.0", 56 | "@typescript-eslint/parser": "^6.0.0", 57 | "eslint": "^8.42.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "jest": "29.7.0", 61 | "prettier": "^3.0.0", 62 | "prisma": "5.10.2", 63 | "source-map-support": "^0.5.21", 64 | "supertest": "^6.3.3", 65 | "ts-jest": "^29.1.0", 66 | "ts-loader": "^9.4.3", 67 | "ts-node": "^10.9.1", 68 | "tsconfig-paths": "^4.2.0", 69 | "typescript": "^5.1.3" 70 | }, 71 | "jest": { 72 | "moduleFileExtensions": [ 73 | "js", 74 | "json", 75 | "ts" 76 | ], 77 | "rootDir": "src", 78 | "testRegex": ".*\\.spec\\.ts$", 79 | "transform": { 80 | "^.+\\.(t|j)s$": "ts-jest" 81 | }, 82 | "collectCoverageFrom": [ 83 | "**/*.(t|j)s" 84 | ], 85 | "coverageDirectory": "../coverage", 86 | "testEnvironment": "node" 87 | } 88 | } -------------------------------------------------------------------------------- /prisma/migrations/20240223004456_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "product" ( 3 | "id" TEXT NOT NULL, 4 | "title" TEXT NOT NULL, 5 | 6 | CONSTRAINT "product_pkey" PRIMARY KEY ("id") 7 | ); 8 | -------------------------------------------------------------------------------- /prisma/migrations/20240223173924_added_more_tables_and_relations/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "product" ADD COLUMN "price" INTEGER NOT NULL DEFAULT 0, 3 | ADD COLUMN "quantityAvailable" INTEGER NOT NULL DEFAULT 0; 4 | 5 | -- CreateTable 6 | CREATE TABLE "user" ( 7 | "id" TEXT NOT NULL, 8 | "name" TEXT NOT NULL, 9 | 10 | CONSTRAINT "user_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "order" ( 15 | "id" TEXT NOT NULL, 16 | "userId" TEXT NOT NULL, 17 | "total" INTEGER NOT NULL, 18 | 19 | CONSTRAINT "order_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "order_product" ( 24 | "id" TEXT NOT NULL, 25 | "productId" TEXT NOT NULL, 26 | "order_id" TEXT NOT NULL, 27 | "price" INTEGER NOT NULL, 28 | 29 | CONSTRAINT "order_product_pkey" PRIMARY KEY ("id") 30 | ); 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "order" ADD CONSTRAINT "order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "order_product" ADD CONSTRAINT "order_product_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 37 | 38 | -- AddForeignKey 39 | ALTER TABLE "order_product" ADD CONSTRAINT "order_product_productId_fkey" FOREIGN KEY ("productId") REFERENCES "product"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 40 | -------------------------------------------------------------------------------- /prisma/migrations/20240223204255_added_more_data/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `order_id` on the `order_product` table. All the data in the column will be lost. 5 | - Added the required column `orderId` to the `order_product` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "order_product" DROP CONSTRAINT "order_product_order_id_fkey"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "order_product" DROP COLUMN "order_id", 13 | ADD COLUMN "orderId" TEXT NOT NULL; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "order_product" ADD CONSTRAINT "order_product_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "order"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20240223214043_removed_qtd_of_items/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `quantityAvailable` on the `product` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "product" DROP COLUMN "quantityAvailable"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240306195210_added_payment_options/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `userId` on the `order` table. All the data in the column will be lost. 5 | - Added the required column `user` to the `order` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- CreateEnum 9 | CREATE TYPE "OrderStatus" AS ENUM ('paid', 'open', 'canceled'); 10 | 11 | -- CreateEnum 12 | CREATE TYPE "PaymentMethod" AS ENUM ('stripe', 'paddle', 'paypal', 'other'); 13 | 14 | -- DropForeignKey 15 | ALTER TABLE "order" DROP CONSTRAINT "order_userId_fkey"; 16 | 17 | -- AlterTable 18 | ALTER TABLE "order" DROP COLUMN "userId", 19 | ADD COLUMN "paymentId" TEXT, 20 | ADD COLUMN "paymentMethod" "PaymentMethod", 21 | ADD COLUMN "status" "OrderStatus" NOT NULL DEFAULT 'open', 22 | ADD COLUMN "user" TEXT NOT NULL; 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "order" ADD CONSTRAINT "order_user_fkey" FOREIGN KEY ("user") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | enum OrderStatus { 8 | paid @map("paid") 9 | open @map("open") 10 | canceled @map("canceled") 11 | } 12 | 13 | enum PaymentMethod { 14 | stripe @map("stripe") 15 | paddle @map("paddle") 16 | paypal @map("paypal") 17 | other @map("other") 18 | } 19 | 20 | generator client { 21 | provider = "prisma-client-js" 22 | } 23 | 24 | datasource db { 25 | provider = "postgresql" 26 | url = env("DATABASE_URL") 27 | } 28 | 29 | model Product { 30 | id String @id @default(uuid()) 31 | title String 32 | price Int @default(0) 33 | OrderProduct OrderProduct[] 34 | 35 | @@map("product") 36 | } 37 | 38 | model User { 39 | id String @id @default(uuid()) 40 | name String 41 | 42 | orders Order[] 43 | 44 | @@map("user") 45 | } 46 | 47 | model Order { 48 | id String @id @default(uuid()) 49 | user String 50 | status OrderStatus @default(open) 51 | paymentMethod PaymentMethod? 52 | paymentId String? 53 | total Int 54 | 55 | User User @relation(fields: [user], references: [id]) 56 | orderProduct OrderProduct[] 57 | 58 | @@map("order") 59 | } 60 | 61 | model OrderProduct { 62 | id String @id @default(uuid()) 63 | productId String 64 | orderId String @map("orderId") 65 | price Int 66 | 67 | order Order @relation(fields: [orderId], references: [id]) 68 | product Product? @relation(fields: [productId], references: [id]) 69 | 70 | @@map("order_product") 71 | } 72 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DevtoolsModule } from '@nestjs/devtools-integration'; 3 | import { EcommerceModule } from './application/ecommerce/ecommerce.module'; 4 | import { PersistenceModule } from './infra/persistence/persistence.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | DevtoolsModule.register({ 9 | http: process.env.NODE_ENV !== 'production', 10 | }), 11 | PersistenceModule.register({ 12 | type: 'mongoose', 13 | global: true, 14 | }), 15 | EcommerceModule 16 | ], 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /src/application/ecommerce/ecommerce.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@app/infra/http/http.module'; 2 | import { Module } from '@nestjs/common'; 3 | 4 | @Module({ 5 | imports: [HttpModule], 6 | controllers: [], 7 | providers: [], 8 | }) 9 | export class EcommerceModule { } 10 | -------------------------------------------------------------------------------- /src/application/ecommerce/ports/order-product.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { OrderProduct } from "@app/domain/ecommerce/order-product"; 2 | 3 | export abstract class OrderProductRepository { 4 | abstract create(data: OrderProduct): Promise; 5 | abstract createMany(data: OrderProduct[]): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/application/ecommerce/ports/order.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { Order } from "@app/domain/ecommerce/order"; 2 | 3 | export abstract class OrderRepository { 4 | abstract findMany(): Promise; 5 | abstract findById(id: string): Promise; 6 | abstract create(data: Order): Promise; 7 | abstract update(id: string, data: Order): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/application/ecommerce/ports/payment.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { Order } from "@app/domain/ecommerce/order"; 2 | 3 | export abstract class PaymentRepository { 4 | abstract generateUrl(orderId: string): Promise; 5 | abstract complete(paymentInput: any): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/application/ecommerce/ports/product.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "@app/domain/ecommerce/product"; 2 | 3 | export abstract class ProductRepository { 4 | abstract findMany(): Promise; 5 | abstract create(data: Product): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/application/ecommerce/ports/user.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@app/domain/ecommerce/User"; 2 | 3 | export abstract class UserRepository { 4 | abstract findMany(): Promise; 5 | abstract create(data: User): Promise; 6 | abstract appendOrder(userId: string, orderId: string): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/checkout-complete.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, RawBodyRequest } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { PaymentRepository } from '../ports/payment.repositoy'; 4 | import { Order } from '@app/domain/ecommerce/order'; 5 | 6 | type CheckoutCompleteUseCaseCommand = { 7 | headers: Request['headers'], 8 | req: RawBodyRequest 9 | } 10 | 11 | @Injectable() 12 | export class CheckoutCompleteUseCase { 13 | constructor( 14 | private paymentRepository: PaymentRepository, 15 | ) { } 16 | 17 | /* 18 | * This method is not abstracted, but it is a good example of how to handle webhooks in a controller. 19 | */ 20 | async execute({ headers, req }: CheckoutCompleteUseCaseCommand): Promise { 21 | const signature = headers['stripe-signature']; 22 | 23 | if (!signature) { 24 | throw new Error('Invalid signature'); 25 | } 26 | 27 | const order = await this.paymentRepository.complete({ 28 | signature, 29 | req: req.rawBody, 30 | }); 31 | 32 | return order; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/checkout-url.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PaymentRepository } from '../ports/payment.repositoy'; 3 | 4 | @Injectable() 5 | export class CheckoutUrlUseCase { 6 | constructor( 7 | private paymentRepository: PaymentRepository 8 | ) { } 9 | 10 | async execute(orderId: string): Promise { 11 | const URL = await this.paymentRepository.generateUrl(orderId); 12 | 13 | return URL; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/create-order.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '@app/domain/ecommerce/order'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { OrderRepository } from '../ports/order.repositoy'; 4 | import { OrderProduct } from '@app/domain/ecommerce/order-product'; 5 | 6 | interface CreateOrderUseCaseCommand { 7 | user: string, 8 | orderProduct: Pick[] 9 | } 10 | 11 | @Injectable() 12 | export class CreateOrderUseCase { 13 | constructor( 14 | private orderRepository: OrderRepository, 15 | ) { } 16 | 17 | async execute({ 18 | user, 19 | orderProduct 20 | }: CreateOrderUseCaseCommand): Promise { 21 | let total = 0; 22 | const order = new Order({ 23 | user, 24 | }) 25 | 26 | const createdOrderProduct = orderProduct.map((product) => { 27 | total += product.price; 28 | 29 | return new OrderProduct({ 30 | product: product.product, 31 | price: product.price, 32 | }); 33 | }); 34 | 35 | order.total = total; 36 | order.orderProduct = createdOrderProduct; 37 | 38 | const createdOrder = await this.orderRepository.create(order) 39 | const response = await this.orderRepository.findById(createdOrder.id); 40 | 41 | return response; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/create-product.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '@app/domain/ecommerce/product'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { ProductRepository } from '../ports/product.repositoy'; 4 | 5 | interface CreateProductUseCaseCommand { 6 | title: string, 7 | price: number 8 | } 9 | 10 | @Injectable() 11 | export class CreateProductUseCase { 12 | constructor(private productRepository: ProductRepository) { } 13 | 14 | async execute({ 15 | title, 16 | price 17 | }: CreateProductUseCaseCommand): Promise { 18 | 19 | const product = new Product({ 20 | title, 21 | price 22 | }) 23 | 24 | const response = await this.productRepository.create(product) 25 | 26 | return response; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/create-user.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@app/domain/ecommerce/user'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { UserRepository } from '../ports/user.repositoy'; 4 | 5 | interface CreateUserUseCaseCommand { 6 | name: string, 7 | } 8 | 9 | @Injectable() 10 | export class CreateUserUseCase { 11 | constructor(private userRepository: UserRepository) { } 12 | 13 | async execute({ 14 | name 15 | }: CreateUserUseCaseCommand): Promise { 16 | 17 | const user = new User({ 18 | name 19 | }) 20 | 21 | const response = await this.userRepository.create(user) 22 | 23 | return response; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/get-order.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OrderRepository } from '../ports/order.repositoy'; 3 | 4 | @Injectable() 5 | export class GetOrderUseCase { 6 | constructor(private orderRepository: OrderRepository) { } 7 | 8 | async execute(id: string): Promise { 9 | const response = await this.orderRepository.findById(id) 10 | 11 | return response; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/get-orders.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OrderRepository } from '../ports/order.repositoy'; 3 | 4 | interface GetOrderUseCaseCommand { } 5 | 6 | @Injectable() 7 | export class GetOrdersUseCase { 8 | constructor(private orderRepository: OrderRepository) { } 9 | 10 | async execute({ }: GetOrderUseCaseCommand): Promise { 11 | const response = await this.orderRepository.findMany() 12 | 13 | return response; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/get-product.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ProductRepository } from '../ports/product.repositoy'; 3 | 4 | interface GetProductUseCaseCommand { } 5 | 6 | @Injectable() 7 | export class GetProductUseCase { 8 | constructor(private productRepository: ProductRepository) { } 9 | 10 | async execute({ }: GetProductUseCaseCommand): Promise { 11 | const response = await this.productRepository.findMany() 12 | 13 | return response; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/application/ecommerce/use-case/get-user.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UserRepository } from '../ports/user.repositoy'; 3 | 4 | interface GetUserUseCaseCommand { } 5 | 6 | @Injectable() 7 | export class GetUserUseCase { 8 | constructor(private userRepository: UserRepository) { } 9 | 10 | async execute({ }: GetUserUseCaseCommand): Promise { 11 | const response = await this.userRepository.findMany() 12 | 13 | return response; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/core/entities/entity.ts: -------------------------------------------------------------------------------- 1 | 2 | export abstract class Entity { 3 | protected props: Props 4 | 5 | protected constructor(props: Props) { 6 | this.props = props 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/core/entities/unique-entity-id.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | export class UniqueEntityID { 4 | private value: string 5 | 6 | toString() { 7 | return this.value 8 | } 9 | 10 | toValue() { 11 | return this.value 12 | } 13 | 14 | constructor(value?: string) { 15 | this.value = value ?? randomUUID() 16 | } 17 | 18 | public equals(id: UniqueEntityID) { 19 | return id.toValue() === this.value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/core/entities/value-object.ts: -------------------------------------------------------------------------------- 1 | export abstract class ValueObject { 2 | protected props: Props 3 | 4 | protected constructor(props: Props) { 5 | this.props = props 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/ecommerce/order-product.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "@app/core/entities/entity"; 2 | 3 | export interface OrderProductProps { 4 | id?: string 5 | orderId?: string 6 | product: string 7 | price: number 8 | } 9 | 10 | export class OrderProduct extends Entity { 11 | constructor(props: OrderProductProps) { 12 | super(props); 13 | } 14 | 15 | get id(): string { 16 | return this.props.id; 17 | } 18 | 19 | get product(): string { 20 | return this.props.product; 21 | } 22 | 23 | get orderId(): string { 24 | return this.props.orderId; 25 | } 26 | 27 | get price(): number { 28 | return this.props.price; 29 | } 30 | 31 | get currentState(): OrderProductProps { 32 | return this.props; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/domain/ecommerce/order.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "@app/core/entities/entity"; 2 | import { OrderProduct } from "./order-product"; 3 | 4 | export interface OrderProps { 5 | id?: string; 6 | user: string 7 | total?: number 8 | status?: "paid" | "open" | "canceled" 9 | paymentId?: string, 10 | paymentMethod?: "stripe" | "paddle" | "paypal" | "other", // It is only working with stripe for now 11 | orderProduct?: OrderProduct[] 12 | } 13 | 14 | export class Order extends Entity { 15 | constructor(props: OrderProps) { 16 | props.total = props.total ?? 0; 17 | props.status = props.status ?? "open"; 18 | 19 | super(props); 20 | } 21 | 22 | get id(): string { 23 | return this.props.id; 24 | } 25 | 26 | get user(): string { 27 | return this.props.user; 28 | } 29 | 30 | get total(): number { 31 | return this.props.total; 32 | } 33 | 34 | get orderProduct(): OrderProduct[] { 35 | return this.props.orderProduct 36 | } 37 | 38 | get status(): "paid" | "open" | "canceled" { 39 | return this.props.status; 40 | } 41 | 42 | get paymentId(): string { 43 | return this.props.paymentId; 44 | } 45 | 46 | get paymentMethod(): "stripe" | "paddle" | "paypal" | "other" { 47 | return this.props.paymentMethod; 48 | } 49 | 50 | get currentState(): OrderProps { 51 | return this.props; 52 | } 53 | 54 | set status(status: "paid" | "open" | "canceled") { 55 | this.props.status = status; 56 | } 57 | 58 | set paymentId(paymentId: string) { 59 | this.props.paymentId = paymentId; 60 | } 61 | 62 | set paymentMethod(paymentMethod: "stripe" | "paddle" | "paypal" | "other") { 63 | this.props.paymentMethod = paymentMethod 64 | } 65 | 66 | set orderProduct(orderProduct: OrderProduct[]) { 67 | this.props.orderProduct = orderProduct 68 | } 69 | 70 | set total(total: number) { 71 | this.props.total = total 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/domain/ecommerce/product.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "@app/core/entities/entity"; 2 | 3 | export interface ProductProps { 4 | id?: string 5 | title: string 6 | price: number 7 | } 8 | 9 | export class Product extends Entity { 10 | constructor(props: ProductProps) { 11 | super(props); 12 | } 13 | 14 | get id(): string { 15 | return this.props.id; 16 | } 17 | 18 | get title(): string { 19 | return this.props.title; 20 | } 21 | 22 | get price(): number { 23 | return this.props.price; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/domain/ecommerce/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "@app/core/entities/entity"; 2 | import { Order } from "./order"; 3 | 4 | export interface UserProps { 5 | id?: string; 6 | name: string; 7 | orders?: Order[] 8 | } 9 | 10 | export class User extends Entity { 11 | constructor(props: UserProps) { 12 | super(props); 13 | } 14 | 15 | get id(): string { 16 | return this.props.id; 17 | } 18 | 19 | get name(): string { 20 | return this.props.name; 21 | } 22 | 23 | get order(): string[] { 24 | return this.order; 25 | } 26 | 27 | get currentState(): UserProps { 28 | return this.props; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/infra/env/env.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { EnvService } from './env.service' 3 | import { envSchema } from './env' 4 | import { ConfigModule } from '@nestjs/config' 5 | 6 | @Module({ 7 | imports: [ 8 | ConfigModule.forRoot({ 9 | envFilePath: process.env.NODE_ENV === 'test' ? '.env.example' : '.env', 10 | validate: (env) => envSchema.parse(env), 11 | isGlobal: false, 12 | }), 13 | ], 14 | providers: [EnvService], 15 | exports: [EnvService], 16 | }) 17 | export class EnvModule { } 18 | -------------------------------------------------------------------------------- /src/infra/env/env.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { ConfigService } from '@nestjs/config' 3 | import { Env } from './env' 4 | 5 | @Injectable() 6 | export class EnvService { 7 | constructor(private configService: ConfigService) {} 8 | 9 | get(key: T) { 10 | return this.configService.get(key, { infer: true }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/infra/env/env.service.unit-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { EnvService } from './env.service'; 4 | 5 | describe('EnvService', () => { 6 | let service: EnvService; 7 | let configService: ConfigService; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | providers: [ 12 | EnvService, 13 | { 14 | provide: ConfigService, 15 | useValue: { 16 | get: jest.fn(), 17 | }, 18 | }, 19 | ], 20 | }).compile(); 21 | 22 | service = module.get(EnvService); 23 | configService = module.get(ConfigService); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(service).toBeDefined(); 28 | }); 29 | 30 | describe('exchangeApiKey', () => { 31 | it('should return the exchangeApiKey value', () => { 32 | const PORT = 3000; 33 | jest.spyOn(configService, 'get').mockReturnValue(PORT); 34 | 35 | expect(service.get('PORT')).toEqual(PORT); 36 | expect(configService.get).toHaveBeenCalledWith('PORT', { infer: true }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/infra/env/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const envSchema = z.object({ 4 | DATABASE_URL: z.string().url(), 5 | MONGODB_URL: z.string().url(), 6 | PORT: z.coerce.number().optional().default(3000), 7 | CACHE_TTL: z.coerce.number().optional().default(5), 8 | CACHE_MAX: z.coerce.number().optional().default(10), 9 | STRIPE_API_KEY: z.coerce.string(), 10 | CHECKOUT_SUCCESS_URL: z.coerce.string(), 11 | STRIPE_WEBHOOK_SECRET: z.coerce.string(), 12 | }) 13 | 14 | export type Env = z.infer 15 | -------------------------------------------------------------------------------- /src/infra/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env'; 2 | export * from './env.module'; 3 | export * from './env.service'; -------------------------------------------------------------------------------- /src/infra/http/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get 4 | } from '@nestjs/common'; 5 | import { ApiTags } from '@nestjs/swagger'; 6 | 7 | @Controller('/') 8 | @ApiTags('App') 9 | export class AppController { 10 | @Get('') 11 | getAll() { 12 | return 'ok'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/infra/http/checkout.controller.ts: -------------------------------------------------------------------------------- 1 | import { CheckoutUrlUseCase } from '@app/application/ecommerce/use-case/checkout-url'; 2 | import { CacheKey } from '@nestjs/cache-manager'; 3 | import { 4 | Controller, 5 | Get, 6 | Headers, 7 | Param, 8 | Post, 9 | RawBodyRequest, 10 | Req, 11 | UseInterceptors 12 | } from '@nestjs/common'; 13 | import { Request } from 'express'; 14 | import { CheckoutCompleteUseCase } from '@app/application/ecommerce/use-case/checkout-complete'; 15 | import { ApiTags } from '@nestjs/swagger'; 16 | import { HttpCacheInterceptor } from '../persistence/cache/interceptor/http-cache.interceptor'; 17 | 18 | @Controller('/checkout') 19 | @ApiTags('Checkout') 20 | export class CheckoutController { 21 | constructor( 22 | private checkoutUrlUseCase: CheckoutUrlUseCase, 23 | private checkoutCompleteUseCase: CheckoutCompleteUseCase, 24 | ) { } 25 | 26 | @Get(':orderId/url') 27 | @CacheKey(':orderId/url') 28 | @UseInterceptors(HttpCacheInterceptor) 29 | checkoutUrl(@Param('orderId') orderId: string) { 30 | const response = this.checkoutUrlUseCase.execute(orderId); 31 | return response; 32 | } 33 | 34 | @Post('completed') 35 | checkoutComplete( 36 | @Headers() requestHeaders: Request['headers'], 37 | @Req() req: RawBodyRequest 38 | ) { 39 | const response = this.checkoutCompleteUseCase.execute({ 40 | headers: requestHeaders, 41 | req: req 42 | }); 43 | return response; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/infra/http/dto/create-order-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsNotEmpty, 4 | IsNumber, 5 | IsString 6 | } from 'class-validator'; 7 | 8 | export class CreateOrderProductDto { 9 | @ApiProperty() 10 | @IsString() 11 | @IsNotEmpty() 12 | product: string; 13 | 14 | @ApiProperty() 15 | @IsNumber() 16 | @IsNotEmpty() 17 | price: number; 18 | } -------------------------------------------------------------------------------- /src/infra/http/dto/create-order.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsNotEmpty, 4 | IsString 5 | } from 'class-validator'; 6 | import { CreateOrderProductDto } from './create-order-product.dto'; 7 | 8 | export class CreateOrderDto { 9 | @ApiProperty() 10 | @IsString() 11 | @IsNotEmpty() 12 | user: string; 13 | 14 | @ApiProperty({ 15 | required: true, 16 | type: CreateOrderProductDto, 17 | nullable: false, 18 | isArray: true, 19 | }) 20 | @IsNotEmpty() 21 | orderProduct: CreateOrderProductDto[] 22 | } -------------------------------------------------------------------------------- /src/infra/http/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsNotEmpty, 4 | IsNumber, 5 | IsString 6 | } from 'class-validator'; 7 | 8 | export class CreateProductDto { 9 | @ApiProperty() 10 | @IsString() 11 | @IsNotEmpty() 12 | title: string; 13 | 14 | @ApiProperty() 15 | @IsNumber() 16 | @IsNotEmpty() 17 | price: number; 18 | } -------------------------------------------------------------------------------- /src/infra/http/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsNotEmpty, 4 | IsString 5 | } from 'class-validator'; 6 | 7 | export class CreateUserDto { 8 | @ApiProperty() 9 | @IsString() 10 | @IsNotEmpty() 11 | name: string; 12 | } -------------------------------------------------------------------------------- /src/infra/http/http.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CacheManagerModule } from '@app/infra/persistence/cache/cache.module'; 3 | import { OrderController } from './order.controller'; 4 | import { ProductController } from './product.controller'; 5 | import { UserController } from './user.controller'; 6 | 7 | import { CreateOrderUseCase } from '@app/application/ecommerce/use-case/create-order'; 8 | import { CreateProductUseCase } from '@app/application/ecommerce/use-case/create-product'; 9 | import { CreateUserUseCase } from '@app/application/ecommerce/use-case/create-user'; 10 | import { GetOrderUseCase } from '@app/application/ecommerce/use-case/get-order'; 11 | import { GetProductUseCase } from '@app/application/ecommerce/use-case/get-product'; 12 | import { GetUserUseCase } from '@app/application/ecommerce/use-case/get-user'; 13 | import { CheckoutUrlUseCase } from '@app/application/ecommerce/use-case/checkout-url'; 14 | import { CheckoutController } from './checkout.controller'; 15 | import { PaymentModule } from '../payment/payment.module'; 16 | import { GetOrdersUseCase } from '@app/application/ecommerce/use-case/get-orders'; 17 | import { CheckoutCompleteUseCase } from '@app/application/ecommerce/use-case/checkout-complete'; 18 | import { AppController } from './app.controller'; 19 | 20 | @Module({ 21 | imports: [PaymentModule, CacheManagerModule], 22 | controllers: [AppController, ProductController, UserController, OrderController, CheckoutController], 23 | providers: [CreateProductUseCase, GetProductUseCase, CreateUserUseCase, GetUserUseCase, GetOrderUseCase, GetOrdersUseCase, CreateOrderUseCase, CheckoutUrlUseCase, CheckoutCompleteUseCase], 24 | exports: [], 25 | }) 26 | export class HttpModule { } 27 | -------------------------------------------------------------------------------- /src/infra/http/order.controller.ts: -------------------------------------------------------------------------------- 1 | import { CreateOrderUseCase } from '@app/application/ecommerce/use-case/create-order'; 2 | import { GetOrderUseCase } from '@app/application/ecommerce/use-case/get-order'; 3 | import { GetOrdersUseCase } from '@app/application/ecommerce/use-case/get-orders'; 4 | import { HttpCacheInterceptor } from '@app/infra/persistence/cache/interceptor/http-cache.interceptor'; 5 | import { CacheKey } from '@nestjs/cache-manager'; 6 | import { 7 | Body, 8 | Controller, 9 | Get, 10 | Param, 11 | Post, 12 | UseInterceptors 13 | } from '@nestjs/common'; 14 | import { ApiTags } from '@nestjs/swagger'; 15 | import { CreateOrderDto } from './dto/create-order.dto'; 16 | 17 | @Controller('/order') 18 | @ApiTags('Order') 19 | export class OrderController { 20 | constructor( 21 | private createOrderUseCase: CreateOrderUseCase, 22 | private getOrderUseCase: GetOrderUseCase, 23 | private getOrdersUseCase: GetOrdersUseCase, 24 | ) { } 25 | 26 | @Get(':id') 27 | @CacheKey('order') 28 | @UseInterceptors(HttpCacheInterceptor) 29 | getOne(@Param('id') id: string) { 30 | const response = this.getOrderUseCase.execute(id); 31 | return response; 32 | } 33 | 34 | @Get('') 35 | @CacheKey('orders') 36 | @UseInterceptors(HttpCacheInterceptor) 37 | getAll() { 38 | const response = this.getOrdersUseCase.execute({}); 39 | return response; 40 | } 41 | 42 | @Post('') 43 | create(@Body() createOrderDto: CreateOrderDto) { 44 | const response = this.createOrderUseCase.execute(createOrderDto); 45 | return response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/infra/http/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { CreateProductUseCase } from '@app/application/ecommerce/use-case/create-product'; 2 | import { GetProductUseCase } from '@app/application/ecommerce/use-case/get-product'; 3 | import { CacheKey } from '@nestjs/cache-manager'; 4 | import { 5 | Body, 6 | Controller, 7 | Get, 8 | Post, 9 | UseInterceptors 10 | } from '@nestjs/common'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | import { HttpCacheInterceptor } from '../persistence/cache/interceptor/http-cache.interceptor'; 13 | import { CreateProductDto } from './dto/create-product.dto'; 14 | 15 | @Controller('/product') 16 | @ApiTags('Product') 17 | export class ProductController { 18 | constructor( 19 | private createProductUseCase: CreateProductUseCase, 20 | private getProductUseCase: GetProductUseCase, 21 | ) { } 22 | 23 | @Get('') 24 | @CacheKey('products') 25 | @UseInterceptors(HttpCacheInterceptor) 26 | async getAll() { 27 | const response = this.getProductUseCase.execute({}); 28 | return response; 29 | } 30 | 31 | @Post('') 32 | create(@Body() createProductDto: CreateProductDto) { 33 | const response = this.createProductUseCase.execute(createProductDto); 34 | return response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/infra/http/user.controller-e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserUseCase } from '@app/application/ecommerce/use-case/create-user'; 2 | import { GetUserUseCase } from '@app/application/ecommerce/use-case/get-user'; 3 | import { Test } from '@nestjs/testing'; 4 | import request from 'supertest'; 5 | import { CacheManagerModule } from '../persistence/cache/cache.module'; 6 | import { PersistenceModule } from '../persistence/persistence.module'; 7 | import { CreateUserDto } from './dto/create-user.dto'; 8 | import { UserController } from './user.controller'; 9 | 10 | describe('UsersController', () => { 11 | let httpServer: any; 12 | let app: any; 13 | 14 | beforeAll(async () => { 15 | const moduleRef = await Test.createTestingModule({ 16 | imports: [ 17 | CacheManagerModule, 18 | PersistenceModule.register({ 19 | type: 'mongoose', 20 | global: true, 21 | }), 22 | ], 23 | controllers: [UserController], 24 | providers: [CreateUserUseCase, GetUserUseCase], 25 | }).compile(); 26 | 27 | app = moduleRef.createNestApplication(); 28 | await app.init(); 29 | 30 | httpServer = app.getHttpServer(); 31 | }); 32 | 33 | afterAll(async () => { 34 | await app.close(); 35 | }); 36 | 37 | describe('UserController', () => { 38 | it('should create user', async () => { 39 | const createDto: CreateUserDto = { 40 | name: 'Jonh Doe', 41 | }; 42 | const response = await request(httpServer) 43 | .post('/user') 44 | .send(createDto); 45 | 46 | expect(response.status).toBe(201); 47 | expect(response.body.props.name).toEqual(createDto.name); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/infra/http/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserUseCase } from '@app/application/ecommerce/use-case/create-user'; 2 | import { 3 | Body, 4 | Controller, 5 | Get, 6 | Post, 7 | UseInterceptors 8 | } from '@nestjs/common'; 9 | import { CreateUserDto } from './dto/create-user.dto'; 10 | import { GetUserUseCase } from '@app/application/ecommerce/use-case/get-user'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | import { CacheKey } from '@nestjs/cache-manager'; 13 | import { HttpCacheInterceptor } from '@app/infra/persistence/cache/interceptor/http-cache.interceptor'; 14 | 15 | @Controller('/user') 16 | @ApiTags('User') 17 | export class UserController { 18 | constructor( 19 | private createUserUseCase: CreateUserUseCase, 20 | private getUserUseCase: GetUserUseCase 21 | ) { } 22 | 23 | @Get('') 24 | @CacheKey('users') 25 | @UseInterceptors(HttpCacheInterceptor) 26 | getAll() { 27 | const response = this.getUserUseCase.execute({}); 28 | return response; 29 | } 30 | 31 | @Post('') 32 | create(@Body() createUserDto: CreateUserDto) { 33 | const response = this.createUserUseCase.execute(createUserDto); 34 | return response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/infra/payment/payment.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { StripeModule } from './stripe/stripe.module'; 3 | 4 | @Module({ 5 | imports: [StripeModule], 6 | exports: [StripeModule], 7 | }) 8 | export class PaymentModule { } 9 | -------------------------------------------------------------------------------- /src/infra/payment/stripe/stripe-payment.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { OrderRepository } from "@app/application/ecommerce/ports/order.repositoy"; 2 | import { PaymentRepository } from "@app/application/ecommerce/ports/payment.repositoy"; 3 | import { EnvService } from "@app/infra/env"; 4 | import { Injectable } from "@nestjs/common"; 5 | import Stripe from "stripe"; 6 | import { StripeService } from "./stripe.service"; 7 | import { Order } from "@app/domain/ecommerce/order"; 8 | 9 | @Injectable() 10 | export class StripePaymentRepository implements PaymentRepository { 11 | private stripe: Stripe; 12 | 13 | constructor( 14 | private orderRepository: OrderRepository, 15 | private stripeService: StripeService, 16 | private envService: EnvService 17 | ) { 18 | this.stripe = this.stripeService.getInstance(); 19 | } 20 | 21 | async generateUrl(orderId: string): Promise { 22 | const order = await this.orderRepository.findById(orderId); 23 | const settingsUrl = this.envService.get("CHECKOUT_SUCCESS_URL"); 24 | 25 | if (!order) { 26 | throw new Error("Order not found"); 27 | } 28 | 29 | const stripeSession = await this.stripe.checkout.sessions.create({ 30 | success_url: settingsUrl, 31 | cancel_url: settingsUrl, 32 | payment_method_types: ["card"], 33 | mode: "payment", 34 | billing_address_collection: "auto", 35 | // customer_email: 'test@gmail.com', // this system doesn't have email 36 | line_items: order.orderProduct.map((item) => { 37 | return { 38 | price_data: { 39 | currency: "USD", 40 | product_data: { 41 | name: item.product, 42 | }, 43 | unit_amount: item.price * 100, 44 | }, 45 | quantity: 1, 46 | }; 47 | }), 48 | metadata: { 49 | orderId: order.id, 50 | }, 51 | }); 52 | 53 | return stripeSession.url || ""; 54 | } 55 | 56 | private constructEvent({ 57 | req, 58 | signature, 59 | }: { 60 | req: string | Buffer, 61 | signature: string, 62 | }): Stripe.Event { 63 | let event: Stripe.Event; 64 | 65 | try { 66 | event = this.stripe.webhooks.constructEvent( 67 | req, 68 | signature, 69 | this.envService.get("STRIPE_WEBHOOK_SECRET") 70 | ); 71 | } catch (error) { 72 | throw new Error("Webhook error"); 73 | } 74 | 75 | return event; 76 | } 77 | 78 | async complete(orderData: any): Promise { 79 | const event = this.constructEvent(orderData); 80 | const session = event.data.object as Stripe.Checkout.Session; 81 | 82 | if (event.type === "checkout.session.completed") { 83 | try { 84 | const paymentIntentId = await this.stripe.checkout.sessions.retrieve(session.id); 85 | 86 | const orderId = session?.metadata?.orderId; 87 | 88 | if (!orderId) { 89 | throw new Error("Invalid event: orderId not found"); 90 | } 91 | 92 | const order = await this.orderRepository.findById(orderId); 93 | 94 | if (order.status === "paid") { 95 | throw new Error("Order already paid"); 96 | } 97 | 98 | const updatedOrder = await this.orderRepository.update( 99 | orderId, 100 | new Order({ 101 | ...order.currentState, 102 | status: 'paid', 103 | paymentId: paymentIntentId.id, 104 | paymentMethod: "stripe", 105 | }) 106 | ); 107 | 108 | return updatedOrder; 109 | } catch (error) { 110 | throw new Error("Invalid event: subscription not found"); 111 | } 112 | } 113 | 114 | } 115 | } -------------------------------------------------------------------------------- /src/infra/payment/stripe/stripe.module.ts: -------------------------------------------------------------------------------- 1 | import { PaymentRepository } from '@app/application/ecommerce/ports/payment.repositoy'; 2 | import { Module } from '@nestjs/common'; 3 | 4 | // Non exported 5 | import { StripePaymentRepository } from './stripe-payment.repositoy'; 6 | import { EnvModule } from '@app/infra/env'; 7 | import { StripeService } from './stripe.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | EnvModule 12 | ], 13 | providers: [ 14 | StripeService, 15 | { 16 | provide: PaymentRepository, 17 | useClass: StripePaymentRepository 18 | }, 19 | ], 20 | exports: [PaymentRepository], 21 | }) 22 | export class StripeModule { } 23 | -------------------------------------------------------------------------------- /src/infra/payment/stripe/stripe.service.ts: -------------------------------------------------------------------------------- 1 | import { EnvService } from '@app/infra/env'; 2 | import { Injectable } from '@nestjs/common'; 3 | import Stripe from "stripe"; 4 | 5 | @Injectable() 6 | export class StripeService { 7 | constructor( 8 | private envService: EnvService 9 | ) { } 10 | 11 | getInstance(): Stripe { 12 | const STRIPE_API_KEY = this.envService.get("STRIPE_API_KEY"); 13 | 14 | const stripe = new Stripe(STRIPE_API_KEY, { 15 | apiVersion: "2023-10-16", 16 | typescript: true, 17 | }); 18 | 19 | return stripe 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infra/persistence/cache/cache.module.ts: -------------------------------------------------------------------------------- 1 | import { EnvModule, EnvService } from '@app/infra/env'; 2 | import { CacheModule } from '@nestjs/cache-manager'; 3 | import { Module } from '@nestjs/common'; 4 | 5 | @Module({ 6 | imports: [ 7 | CacheModule.registerAsync({ 8 | isGlobal: false, 9 | imports: [EnvModule], 10 | useFactory: async (configService: EnvService) => ({ 11 | ttl: configService.get('CACHE_TTL'), 12 | max: configService.get('CACHE_MAX'), 13 | }), 14 | inject: [EnvService], 15 | }), 16 | ], 17 | exports: [CacheModule], 18 | }) 19 | export class CacheManagerModule { } 20 | -------------------------------------------------------------------------------- /src/infra/persistence/cache/interceptor/http-cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CacheInterceptor } from '@nestjs/cache-manager'; 2 | import { ExecutionContext, Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class HttpCacheInterceptor extends CacheInterceptor { 6 | trackBy(context: ExecutionContext): string | undefined { 7 | const request = context.switchToHttp().getRequest(); 8 | const { httpAdapter } = this.httpAdapterHost; 9 | 10 | const isGetRequest = httpAdapter.getRequestMethod(request) === 'GET'; 11 | const excludePaths = [ 12 | // Routes to be excluded 13 | ]; 14 | if ( 15 | !isGetRequest || 16 | (isGetRequest && 17 | excludePaths.includes(httpAdapter.getRequestUrl(request))) 18 | ) { 19 | return undefined; 20 | } 21 | return httpAdapter.getRequestUrl(request); 22 | } 23 | } -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/entities/order-product.entity.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import mongoose, { HydratedDocument } from 'mongoose'; 3 | import { Product } from './product.entity'; 4 | 5 | export type OrderProductDocument = HydratedDocument; 6 | 7 | @Schema() 8 | export class OrderProduct { 9 | _id: mongoose.Schema.Types.ObjectId; 10 | 11 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: Product.name }) 12 | product: Product; 13 | 14 | @Prop() 15 | price: number; 16 | } 17 | 18 | export const OrderProductSchema = SchemaFactory.createForClass(OrderProduct); 19 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/entities/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import mongoose, { HydratedDocument } from 'mongoose'; 3 | import { Type } from 'class-transformer'; 4 | import { OrderProduct } from './order-product.entity'; 5 | import { User } from './user.entity'; 6 | 7 | export type OrderDocument = HydratedDocument; 8 | 9 | @Schema() 10 | export class Order { 11 | _id: mongoose.Schema.Types.ObjectId; 12 | 13 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: User.name, }) 14 | @Type(() => User) 15 | user: User; 16 | 17 | @Prop({ 18 | type: [{ type: mongoose.Schema.Types.ObjectId, ref: OrderProduct.name }], 19 | }) 20 | orderProduct: OrderProduct[]; 21 | 22 | @Prop() 23 | total: number; 24 | 25 | @Prop({ default: 'open' }) 26 | status: 'paid' | 'open' | 'canceled'; 27 | 28 | @Prop({ index: true }) 29 | paymentId?: string; 30 | 31 | @Prop({ index: true }) 32 | paymentMethod?: 'stripe' | 'paddle' | 'paypal' | 'other'; 33 | } 34 | 35 | const OrderSchema = SchemaFactory.createForClass(Order); 36 | 37 | export { OrderSchema }; 38 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import mongoose, { HydratedDocument } from 'mongoose'; 3 | import { OrderProduct } from './order-product.entity'; 4 | 5 | export type ProductDocument = HydratedDocument; 6 | 7 | @Schema() 8 | export class Product { 9 | _id: mongoose.Schema.Types.ObjectId; 10 | 11 | @Prop() 12 | title: string; 13 | 14 | @Prop() 15 | price: number; 16 | 17 | @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'OrderProduct' }] }) 18 | orderProduct: OrderProduct[]; 19 | } 20 | 21 | export const ProductSchema = SchemaFactory.createForClass(Product); 22 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import mongoose, { HydratedDocument } from 'mongoose'; 3 | import { Order } from './order.entity'; 4 | 5 | export type UserDocument = HydratedDocument; 6 | 7 | @Schema() 8 | export class User { 9 | _id: mongoose.Schema.Types.ObjectId; 10 | 11 | @Prop() 12 | name: string; 13 | 14 | @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Order' }] }) 15 | orders: Order[]; 16 | } 17 | 18 | const UserSchema = SchemaFactory.createForClass(User); 19 | 20 | export { UserSchema }; 21 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mapper/mongoose-order-details-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '@app/domain/ecommerce/order'; 2 | import { Order as OrderDocument } from '../entities/order.entity'; 3 | import { OrderProduct as OrderProductDocument } from '../entities/order-product.entity'; 4 | import { MongooseOrderProductMapper } from './mongoose-order-product-mapper'; 5 | 6 | type OrderWithOrderProductsDocument = OrderDocument & { orderProduct?: OrderProductDocument[] } 7 | 8 | export class MongooseOrderDetailsMapper { 9 | static toDomain(entity: OrderWithOrderProductsDocument): Order { 10 | const model = new Order({ 11 | id: entity._id.toString(), 12 | user: entity.user.toString(), 13 | total: entity.total, 14 | orderProduct: !!entity.orderProduct ? entity.orderProduct.map((order) => MongooseOrderProductMapper.toDomain(order)) : [], 15 | status: entity.status, 16 | paymentId: entity.paymentId, 17 | paymentMethod: entity.paymentMethod, 18 | }); 19 | return model; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mapper/mongoose-order-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '@app/domain/ecommerce/order'; 2 | import { Order as OrderDocument } from '../entities/order.entity'; 3 | 4 | export class MongooseOrderMapper { 5 | static toDomain(entity: OrderDocument): Order { 6 | const model = new Order({ 7 | id: entity._id.toString(), 8 | user: entity.user.toString(), 9 | total: entity.total, 10 | status: entity.status, 11 | paymentId: entity.paymentId, 12 | paymentMethod: entity.paymentMethod, 13 | }); 14 | return model; 15 | } 16 | 17 | static toMongoose(order: Order) { 18 | return { 19 | total: order.total, 20 | user: order.user, 21 | status: order.status, 22 | paymentId: order?.paymentId, 23 | paymentMethod: order?.paymentMethod, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mapper/mongoose-order-product-mapper.ts: -------------------------------------------------------------------------------- 1 | import { OrderProduct } from '@app/domain/ecommerce/order-product'; 2 | import { OrderProduct as OrderProductDocument } from '../entities/order-product.entity'; 3 | 4 | export class MongooseOrderProductMapper { 5 | static toDomain(entity: OrderProductDocument): OrderProduct { 6 | const model = new OrderProduct({ 7 | id: entity._id.toString(), 8 | product: entity.product.toString(), 9 | price: entity.price, 10 | }); 11 | return model; 12 | } 13 | 14 | static toMongoose( 15 | orderProducts: OrderProduct, 16 | ): any { 17 | return { 18 | price: orderProducts.price, 19 | product: orderProducts.product, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mapper/mongoose-product-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '@app/domain/ecommerce/product'; 2 | import { Product as ProductDocument } from "../entities/product.entity"; 3 | 4 | export class MongooseProductMapper { 5 | static toDomain(entity: ProductDocument): Product { 6 | const model = new Product({ 7 | id: entity._id.toString(), 8 | title: entity.title, 9 | price: entity.price, 10 | }); 11 | return model; 12 | } 13 | 14 | static toMongoose(product: Product) { 15 | return { 16 | title: product.title, 17 | price: product.price, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mapper/mongoose-user-details-mapper.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@app/domain/ecommerce/user'; 2 | import { Order } from '../entities/order.entity'; 3 | import { User as UserDocument } from '../entities/user.entity'; 4 | import { MongooseOrderMapper } from './mongoose-order-mapper'; 5 | 6 | type UserWithOrderDocument = UserDocument & { orders?: Order[] } 7 | 8 | export class MongooseUserDetailsMapper { 9 | static toDomain(entity: UserWithOrderDocument): User { 10 | const model = new User({ 11 | id: entity._id.toString(), 12 | name: entity.name, 13 | orders: !!entity.orders ? entity.orders.map((order) => MongooseOrderMapper.toDomain(order)) : [], 14 | }); 15 | return model; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mapper/mongoose-user-mapper.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@app/domain/ecommerce/user'; 2 | import { User as UserDocument } from '../entities/user.entity'; 3 | 4 | export class MongooseUserMapper { 5 | static toDomain(entity: UserDocument): User { 6 | const model = new User({ 7 | id: entity._id.toString(), 8 | name: entity.name, 9 | }); 10 | return model; 11 | } 12 | 13 | static toMongoose(user: User) { 14 | return { 15 | name: user.name, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/mongoose.module.ts: -------------------------------------------------------------------------------- 1 | import { OrderProductRepository } from '@app/application/ecommerce/ports/order-product.repositoy'; 2 | import { OrderRepository } from '@app/application/ecommerce/ports/order.repositoy'; 3 | import { ProductRepository } from '@app/application/ecommerce/ports/product.repositoy'; 4 | import { UserRepository } from '@app/application/ecommerce/ports/user.repositoy'; 5 | import { EnvModule, EnvService } from '@app/infra/env'; 6 | import { Module } from '@nestjs/common'; 7 | import { MongooseModule as MongooseModuleLib } from '@nestjs/mongoose'; 8 | import { OrderProduct, OrderProductSchema } from './entities/order-product.entity'; 9 | import { Order, OrderSchema } from './entities/order.entity'; 10 | import { Product, ProductSchema } from './entities/product.entity'; 11 | import { User, UserSchema } from './entities/user.entity'; 12 | 13 | // Non exported 14 | import { MongooseOrderProductRepository } from './repositories/mongoose-order-product.repositoy'; 15 | import { MongooseOrderRepository } from './repositories/mongoose-order.repositoy'; 16 | import { MongooseProductRepository } from './repositories/mongoose-product.repositoy'; 17 | import { MongooseUserRepository } from './repositories/mongoose-user.repositoy'; 18 | 19 | @Module({ 20 | imports: [ 21 | MongooseModuleLib.forRootAsync({ 22 | imports: [EnvModule], 23 | useFactory: (envService: EnvService) => ({ 24 | uri: envService.get('MONGODB_URL'), 25 | }), 26 | inject: [EnvService], 27 | }), 28 | MongooseModuleLib.forFeature([ 29 | { name: User.name, schema: UserSchema }, 30 | { name: Product.name, schema: ProductSchema }, 31 | { name: Order.name, schema: OrderSchema }, 32 | { name: OrderProduct.name, schema: OrderProductSchema }, 33 | ]), 34 | ], 35 | providers: [ 36 | { 37 | provide: ProductRepository, 38 | useClass: MongooseProductRepository 39 | }, 40 | { 41 | provide: UserRepository, 42 | useClass: MongooseUserRepository 43 | }, 44 | { 45 | provide: OrderRepository, 46 | useClass: MongooseOrderRepository 47 | }, 48 | { 49 | provide: OrderProductRepository, 50 | useClass: MongooseOrderProductRepository 51 | }, 52 | ], 53 | exports: [ProductRepository, UserRepository, OrderRepository, OrderProductRepository], 54 | }) 55 | export class MongooseModule { } 56 | -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/repositories/mongoose-order-product.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { OrderProductRepository } from "@app/application/ecommerce/ports/order-product.repositoy"; 2 | import { OrderProduct } from "@app/domain/ecommerce/order-product"; 3 | import { OrderProduct as OrderProductMongoose } from "../entities/order-product.entity"; 4 | import { Injectable } from "@nestjs/common"; 5 | import { MongooseOrderProductMapper } from "../mapper/mongoose-order-product-mapper"; 6 | import { Model } from "mongoose"; 7 | import { InjectModel } from "@nestjs/mongoose"; 8 | 9 | @Injectable() 10 | export class MongooseOrderProductRepository implements OrderProductRepository { 11 | constructor( 12 | @InjectModel(OrderProductMongoose.name) private readonly productOrderModel: Model 13 | ) { } 14 | 15 | async create(orderProduct: OrderProduct): Promise { 16 | const data = MongooseOrderProductMapper.toMongoose(orderProduct); 17 | const createdOrderProduct = await this.productOrderModel.create(data); 18 | 19 | return MongooseOrderProductMapper.toDomain(createdOrderProduct); 20 | } 21 | 22 | async createMany(orderProducts: OrderProduct[]): Promise { 23 | if (orderProducts.length === 0) { 24 | return []; 25 | } 26 | 27 | const data = orderProducts.map((product) => MongooseOrderProductMapper.toMongoose(product)); 28 | const createdOrderProductProducts = await this.productOrderModel.create(data); 29 | 30 | return createdOrderProductProducts.map(MongooseOrderProductMapper.toDomain); 31 | } 32 | } -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/repositories/mongoose-order.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { OrderProductRepository } from "@app/application/ecommerce/ports/order-product.repositoy"; 2 | import { OrderRepository } from "@app/application/ecommerce/ports/order.repositoy"; 3 | import { UserRepository } from "@app/application/ecommerce/ports/user.repositoy"; 4 | import { Order } from "@app/domain/ecommerce/order"; 5 | import { Injectable } from "@nestjs/common"; 6 | import { InjectModel } from "@nestjs/mongoose"; 7 | import mongoose, { Model } from "mongoose"; 8 | import { Order as OrderMongoose } from "../entities/order.entity"; 9 | import { MongooseOrderMapper } from "../mapper/mongoose-order-mapper"; 10 | import { MongooseOrderDetailsMapper } from "../mapper/mongoose-order-details-mapper"; 11 | 12 | @Injectable() 13 | export class MongooseOrderRepository implements OrderRepository { 14 | constructor( 15 | @InjectModel(OrderMongoose.name) private readonly orderModel: Model, 16 | private orderProductRepository: OrderProductRepository, 17 | private userRepository: UserRepository 18 | ) { } 19 | 20 | async findMany(): Promise { 21 | const findQuery = await this.orderModel 22 | .find() 23 | .populate(['orderProduct']); 24 | 25 | return findQuery.map((item) => MongooseOrderDetailsMapper.toDomain(item)); 26 | } 27 | 28 | async findById(id: string): Promise { 29 | const findQuery = await this.orderModel 30 | .findById(id) 31 | .populate(['orderProduct']); 32 | 33 | return MongooseOrderDetailsMapper.toDomain(findQuery); 34 | } 35 | 36 | async create(orderInput: Order): Promise { 37 | let orderProductIds = []; 38 | 39 | if (orderInput?.orderProduct.length) { 40 | orderProductIds = await Promise.all( 41 | orderInput.orderProduct.map(async (orderProduct) => { 42 | const orderProductCreated = await this.orderProductRepository.create(orderProduct); 43 | return orderProductCreated.id; 44 | }) 45 | ); 46 | } 47 | 48 | const data = MongooseOrderMapper.toMongoose(orderInput); 49 | const order = new this.orderModel({ 50 | ...data, 51 | user: { 52 | "_id": new mongoose.Types.ObjectId(orderInput.user), 53 | }, 54 | orderProduct: orderProductIds, 55 | }); 56 | 57 | await this.userRepository.appendOrder(orderInput.user, order.id); 58 | 59 | const savedOrder = await order.save(); 60 | 61 | return MongooseOrderMapper.toDomain(savedOrder); 62 | } 63 | 64 | async update(orderId: string, orderInput: Order): Promise { 65 | const preparedData = MongooseOrderMapper.toMongoose(orderInput); 66 | 67 | const order = await this.orderModel 68 | .findOneAndUpdate( 69 | { 70 | _id: orderId, 71 | }, 72 | preparedData, 73 | { 74 | new: true, 75 | }, 76 | ) 77 | .exec(); 78 | 79 | return MongooseOrderMapper.toDomain(order); 80 | } 81 | } -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/repositories/mongoose-product.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from "@app/application/ecommerce/ports/product.repositoy"; 2 | import { Product } from "@app/domain/ecommerce/product"; 3 | import { Product as ProductMongoose } from "../entities/product.entity"; 4 | import { Injectable } from "@nestjs/common"; 5 | import { MongooseProductMapper } from "../mapper/mongoose-product-mapper"; 6 | import { InjectModel } from "@nestjs/mongoose"; 7 | import { Model } from "mongoose"; 8 | 9 | @Injectable() 10 | export class MongooseProductRepository implements ProductRepository { 11 | constructor( 12 | @InjectModel(ProductMongoose.name) private readonly productModel: Model, 13 | ) { } 14 | 15 | async findMany(): Promise { 16 | const products = await this.productModel.find(); 17 | 18 | return products.map((item) => MongooseProductMapper.toDomain(item)); 19 | } 20 | 21 | async create(product: Product): Promise { 22 | const data = MongooseProductMapper.toMongoose(product); 23 | const entity = await this.productModel.create(data); 24 | 25 | return MongooseProductMapper.toDomain(entity); 26 | } 27 | } -------------------------------------------------------------------------------- /src/infra/persistence/mongoose/repositories/mongoose-user.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from "@app/application/ecommerce/ports/user.repositoy"; 2 | import { User } from "@app/domain/ecommerce/user"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { InjectModel } from "@nestjs/mongoose"; 5 | import mongoose, { Model } from "mongoose"; 6 | import { User as UserMongoose } from "../entities/user.entity"; 7 | import { MongooseUserMapper } from "../mapper/mongoose-user-mapper"; 8 | import { MongooseUserDetailsMapper } from "../mapper/mongoose-user-details-mapper"; 9 | 10 | @Injectable() 11 | export class MongooseUserRepository implements UserRepository { 12 | constructor( 13 | @InjectModel(UserMongoose.name) private readonly userModel: Model, 14 | ) { } 15 | 16 | async findMany(): Promise { 17 | const findQuery = await this.userModel 18 | .find() 19 | .populate(['orders']); 20 | 21 | return findQuery.map((item) => MongooseUserDetailsMapper.toDomain(item)); 22 | } 23 | 24 | async create(user: User): Promise { 25 | const data = MongooseUserMapper.toMongoose(user); 26 | const entity = new this.userModel({ ...data }) 27 | await entity.save(); 28 | 29 | return MongooseUserMapper.toDomain(entity); 30 | } 31 | 32 | async appendOrder(id: string, order: string): Promise { 33 | const updatedUser = await this.userModel.findByIdAndUpdate( 34 | id, 35 | { 36 | $push: { orders: new mongoose.Types.ObjectId(order) }, 37 | }, 38 | { new: true } 39 | ); 40 | 41 | return MongooseUserMapper.toDomain(updatedUser); 42 | } 43 | } -------------------------------------------------------------------------------- /src/infra/persistence/persistence.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { MongooseModule } from './mongoose/mongoose.module'; 3 | import { PrismaModule } from './prisma/prisma.module'; 4 | 5 | interface DatabaseOptions { 6 | type: 'prisma' | 'mongoose'; 7 | global?: boolean; 8 | } 9 | 10 | @Module({}) 11 | export class PersistenceModule { 12 | static async register({ global = false, type }: DatabaseOptions): Promise { 13 | return { 14 | global, 15 | module: PersistenceModule, 16 | imports: [type === 'mongoose' ? MongooseModule : PrismaModule], 17 | exports: [type === 'mongoose' ? MongooseModule : PrismaModule], 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/mapper/prisma-order-details-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '@app/domain/ecommerce/order'; 2 | import { OrderProduct, Order as PrismaOrder } from '@prisma/client'; 3 | import { PrismaOrderProductMapper } from './prisma-order-product-mapper'; 4 | 5 | type OrderWithOrderProduct = PrismaOrder & { orderProduct?: OrderProduct[] }; 6 | 7 | export class PrismaOrderDetailsMapper { 8 | static toDomain(entity: OrderWithOrderProduct): Order { 9 | const model = new Order({ 10 | id: entity.id, 11 | user: entity.user, 12 | total: entity.total, 13 | orderProduct: !!entity.orderProduct ? entity.orderProduct.map((item) => PrismaOrderProductMapper.toDomain(item)) : [], 14 | status: entity.status, 15 | paymentId: entity.paymentId, 16 | paymentMethod: entity.paymentMethod, 17 | }); 18 | return model; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/mapper/prisma-order-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '@app/domain/ecommerce/order'; 2 | import { Prisma, Order as PrismaOrder } from '@prisma/client'; 3 | 4 | export class PrismaOrderMapper { 5 | static toDomain(entity: PrismaOrder): Order { 6 | const model = new Order({ 7 | id: entity.id, 8 | user: entity.user, 9 | total: entity.total, 10 | paymentId: entity.paymentId, 11 | paymentMethod: entity.paymentMethod 12 | }); 13 | return model; 14 | } 15 | 16 | static toPrisma(order: Order): Prisma.OrderUncheckedCreateInput { 17 | return { 18 | user: order.user, 19 | total: order.total, 20 | id: order.id, 21 | paymentId: order?.paymentId, 22 | paymentMethod: order?.paymentMethod 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/mapper/prisma-order-product-mapper.ts: -------------------------------------------------------------------------------- 1 | import { OrderProduct } from '@app/domain/ecommerce/order-product' 2 | import { Prisma, OrderProduct as PrismaOrderProduct } from '@prisma/client' 3 | 4 | export class PrismaOrderProductMapper { 5 | static toDomain(entity: PrismaOrderProduct): OrderProduct { 6 | const model = new OrderProduct({ 7 | id: entity.id, 8 | product: entity.productId, 9 | orderId: entity.orderId, 10 | price: entity.price, 11 | }); 12 | return model; 13 | } 14 | 15 | static toPrisma( 16 | orderProducts: OrderProduct, 17 | ): Prisma.OrderProductUncheckedCreateInput { 18 | return { 19 | productId: orderProducts.product, 20 | orderId: orderProducts.orderId, 21 | price: orderProducts.price, 22 | } 23 | } 24 | 25 | static toPrismaCreateMany( 26 | orderProducts: OrderProduct[], 27 | ): Prisma.OrderProductCreateManyInput[] { 28 | return orderProducts.map((product) => ({ 29 | productId: product.product, 30 | orderId: product.orderId, 31 | price: product.price, 32 | })); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/mapper/prisma-product-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '@app/domain/ecommerce/product' 2 | import { Prisma, Product as PrismaProduct } from '@prisma/client' 3 | 4 | export class PrismaProductMapper { 5 | static toDomain(entity: PrismaProduct): Product { 6 | const model = new Product({ 7 | id: entity.id, 8 | title: entity.title, 9 | price: entity.price, 10 | }); 11 | return model; 12 | } 13 | 14 | static toPrisma(product: Product): Prisma.ProductUncheckedCreateInput { 15 | return { 16 | title: product.title, 17 | price: product.price, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/mapper/prisma-user-details-mapper.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@app/domain/ecommerce/user'; 2 | import { Order, User as PrismaUser } from '@prisma/client'; 3 | import { PrismaOrderMapper } from './prisma-order-mapper'; 4 | 5 | type UserWithOrder = PrismaUser & { orders?: Order[] }; 6 | 7 | export class PrismaUserDetailsMapper { 8 | static toDomain(entity: UserWithOrder): User { 9 | const model = new User({ 10 | id: entity.id, 11 | name: entity.name, 12 | orders: !!entity.orders ? entity.orders.map((item) => PrismaOrderMapper.toDomain(item)) : [] 13 | }); 14 | return model; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/mapper/prisma-user-mapper.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@app/domain/ecommerce/user' 2 | import { Prisma, User as PrismaUser } from '@prisma/client' 3 | 4 | export class PrismaUserMapper { 5 | static toDomain(entity: PrismaUser): User { 6 | const model = new User({ 7 | id: entity.id, 8 | name: entity.name, 9 | }); 10 | return model; 11 | } 12 | 13 | static toPrisma(user: User): Prisma.UserUncheckedCreateInput { 14 | return { 15 | name: user.name, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from '@app/application/ecommerce/ports/product.repositoy'; 2 | import { Module } from '@nestjs/common'; 3 | import { PrismaService } from './prisma.service'; 4 | import { EnvModule } from '@app/infra/env'; 5 | import { OrderProductRepository } from '@app/application/ecommerce/ports/order-product.repositoy'; 6 | import { UserRepository } from '@app/application/ecommerce/ports/user.repositoy'; 7 | import { OrderRepository } from '@app/application/ecommerce/ports/order.repositoy'; 8 | 9 | // Non exported 10 | import { PrismaProductRepository } from './repositories/prisma-product.repositoy'; 11 | import { PrismaUserRepository } from './repositories/prisma-user.repositoy'; 12 | import { PrismaOrderRepository } from './repositories/prisma-order.repositoy'; 13 | import { PrismaOrderProductRepository } from './repositories/prisma-order-product.repositoy'; 14 | 15 | @Module({ 16 | imports: [EnvModule], 17 | providers: [ 18 | PrismaService, 19 | { 20 | provide: ProductRepository, 21 | useClass: PrismaProductRepository 22 | }, 23 | { 24 | provide: UserRepository, 25 | useClass: PrismaUserRepository 26 | }, 27 | { 28 | provide: OrderRepository, 29 | useClass: PrismaOrderRepository 30 | }, 31 | { 32 | provide: OrderProductRepository, 33 | useClass: PrismaOrderProductRepository 34 | }, 35 | ], 36 | exports: [PrismaService, ProductRepository, UserRepository, OrderRepository, OrderProductRepository], 37 | }) 38 | export class PrismaModule { } 39 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common' 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | @Injectable() 5 | export class PrismaService 6 | extends PrismaClient 7 | implements OnModuleInit, OnModuleDestroy { 8 | constructor() { 9 | super({ 10 | log: ['warn', 'error'], 11 | }) 12 | } 13 | 14 | onModuleInit() { 15 | return this.$connect() 16 | } 17 | 18 | onModuleDestroy() { 19 | return this.$disconnect() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infra/persistence/prisma/repositories/prisma-order-product.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { OrderProductRepository } from "@app/application/ecommerce/ports/order-product.repositoy"; 2 | import { OrderProduct } from "@app/domain/ecommerce/order-product"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { PrismaOrderProductMapper } from "../mapper/prisma-order-product-mapper"; 5 | import { PrismaService } from "../prisma.service"; 6 | 7 | @Injectable() 8 | export class PrismaOrderProductRepository implements OrderProductRepository { 9 | constructor( 10 | private prisma: PrismaService, 11 | ) { } 12 | 13 | async create(orderProduct: OrderProduct): Promise { 14 | const data = PrismaOrderProductMapper.toPrisma(orderProduct); 15 | const createdOrderProduct = await this.prisma.orderProduct.create({ data }); 16 | 17 | return PrismaOrderProductMapper.toDomain(createdOrderProduct); 18 | } 19 | 20 | async createMany(orderProducts: OrderProduct[]): Promise { 21 | if (orderProducts.length === 0) { 22 | return []; 23 | } 24 | 25 | const data = PrismaOrderProductMapper.toPrismaCreateMany(orderProducts); 26 | 27 | const createdOrderProducts = await this.prisma.$transaction( 28 | data.map((item) => this.prisma.orderProduct.create({ data: item })) 29 | ); 30 | 31 | return createdOrderProducts.map(PrismaOrderProductMapper.toDomain); 32 | } 33 | } -------------------------------------------------------------------------------- /src/infra/persistence/prisma/repositories/prisma-order.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { OrderRepository } from "@app/application/ecommerce/ports/order.repositoy"; 2 | import { Order } from "@app/domain/ecommerce/order"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { PrismaOrderDetailsMapper } from "../mapper/prisma-order-details-mapper"; 5 | import { PrismaOrderMapper } from "../mapper/prisma-order-mapper"; 6 | import { PrismaService } from "../prisma.service"; 7 | 8 | @Injectable() 9 | export class PrismaOrderRepository implements OrderRepository { 10 | constructor( 11 | private prisma: PrismaService, 12 | ) { } 13 | 14 | async findMany(): Promise { 15 | const orders = await this.prisma.order.findMany({ 16 | include: { 17 | orderProduct: true 18 | } 19 | }); 20 | 21 | return orders.map((item) => PrismaOrderDetailsMapper.toDomain(item)); 22 | } 23 | 24 | async findById(id: string): Promise { 25 | const order = await this.prisma.order.findFirst({ 26 | where: { 27 | id 28 | }, 29 | include: { 30 | orderProduct: true 31 | } 32 | }); 33 | 34 | return PrismaOrderDetailsMapper.toDomain(order); 35 | } 36 | 37 | async create(orderInput: Order): Promise { 38 | const data = PrismaOrderMapper.toPrisma(orderInput); 39 | 40 | const orderProducts = orderInput.orderProduct.map(orderProduct => ({ 41 | productId: orderProduct.product, 42 | price: orderProduct.price 43 | })); 44 | 45 | const order = await this.prisma.order.create({ 46 | data: { 47 | ...data, 48 | orderProduct: { 49 | create: orderProducts 50 | } 51 | }, 52 | include: { 53 | orderProduct: true 54 | } 55 | }); 56 | 57 | return PrismaOrderMapper.toDomain(order); 58 | } 59 | 60 | async update(orderId: string, orderInput: Order): Promise { 61 | const data = PrismaOrderMapper.toPrisma(orderInput); 62 | 63 | const order = await this.prisma.order.update({ 64 | where: { 65 | id: orderId 66 | }, 67 | data, 68 | }); 69 | 70 | return PrismaOrderMapper.toDomain(order); 71 | } 72 | } -------------------------------------------------------------------------------- /src/infra/persistence/prisma/repositories/prisma-product.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from "@app/application/ecommerce/ports/product.repositoy"; 2 | import { Product } from "@app/domain/ecommerce/product"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { PrismaProductMapper } from "../mapper/prisma-product-mapper"; 5 | import { PrismaService } from "../prisma.service"; 6 | 7 | @Injectable() 8 | export class PrismaProductRepository implements ProductRepository { 9 | constructor(private prisma: PrismaService) { } 10 | 11 | async findMany(): Promise { 12 | const products = await this.prisma.product.findMany(); 13 | 14 | return products.map((item) => PrismaProductMapper.toDomain(item)); 15 | } 16 | 17 | async create(product: Product): Promise { 18 | const data = PrismaProductMapper.toPrisma(product); 19 | const entity = await this.prisma.product.create({ data }); 20 | 21 | return PrismaProductMapper.toDomain(entity); 22 | } 23 | } -------------------------------------------------------------------------------- /src/infra/persistence/prisma/repositories/prisma-user.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from "@app/application/ecommerce/ports/user.repositoy"; 2 | import { User } from "@app/domain/ecommerce/user"; 3 | import { Injectable } from "@nestjs/common"; 4 | import { PrismaUserMapper } from "../mapper/prisma-user-mapper"; 5 | import { PrismaService } from "../prisma.service"; 6 | import { PrismaUserDetailsMapper } from "../mapper/prisma-user-details-mapper"; 7 | 8 | @Injectable() 9 | export class PrismaUserRepository implements UserRepository { 10 | constructor(private prisma: PrismaService) { } 11 | 12 | async findMany(): Promise { 13 | const users = await this.prisma.user.findMany({ 14 | include: { 15 | orders: true 16 | } 17 | }); 18 | 19 | return users.map((item) => PrismaUserDetailsMapper.toDomain(item)); 20 | } 21 | 22 | async create(user: User): Promise { 23 | const data = PrismaUserMapper.toPrisma(user); 24 | const entity = await this.prisma.user.create({ data }); 25 | 26 | return PrismaUserMapper.toDomain(entity); 27 | } 28 | 29 | async appendOrder(userId: string, orderId: string): Promise { 30 | const user = await this.prisma.user.update({ 31 | where: { id: userId }, 32 | data: { 33 | orders: { 34 | connect: { id: orderId } 35 | } 36 | } 37 | }); 38 | 39 | return PrismaUserMapper.toDomain(user); 40 | } 41 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { EnvService } from './infra/env/env.service'; 4 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 5 | import { ValidationPipe } from '@nestjs/common'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule, { 9 | snapshot: true, 10 | rawBody: true, 11 | }) 12 | 13 | const configService = app.get(EnvService) 14 | const port = configService.get('PORT') 15 | 16 | function getSwaggerServerUrl() { 17 | switch (process.env.NODE_ENV) { 18 | case 'production': 19 | return 'https://nestjs-ecommerce-alpha.vercel.app'; 20 | default: 21 | return `http://localhost:${port}`; 22 | } 23 | } 24 | 25 | app.useGlobalPipes(new ValidationPipe({ 26 | transform: true, 27 | whitelist: true, 28 | })); 29 | 30 | const config = new DocumentBuilder() 31 | .setTitle('API') 32 | .setVersion('0.1') 33 | .addServer(getSwaggerServerUrl()) 34 | .build(); 35 | const document = SwaggerModule.createDocument(app, config); 36 | SwaggerModule.setup('api', app, document); 37 | 38 | await app.listen(port); 39 | } 40 | bootstrap(); 41 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2022", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": ".", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "paths": { 21 | "@app/core": [ 22 | "src/core" 23 | ], 24 | "@app/core/*": [ 25 | "src/core/*" 26 | ], 27 | "@app/application": [ 28 | "src/application" 29 | ], 30 | "@app/application/*": [ 31 | "src/application/*" 32 | ], 33 | "@app/domain": [ 34 | "src/domain" 35 | ], 36 | "@app/domain/*": [ 37 | "src/domain/*" 38 | ], 39 | "@app/infra": [ 40 | "src/infra" 41 | ], 42 | "@app/infra/*": [ 43 | "src/infra/*" 44 | ] 45 | } 46 | } 47 | } --------------------------------------------------------------------------------