├── .nvmrc ├── tools ├── postgres │ └── init.sql ├── tsconfig.json └── nest-mono-new │ ├── README.md │ ├── index.ts │ └── helper.ts ├── gateway ├── src │ ├── common │ │ ├── index.ts │ │ └── enums │ │ │ ├── index.ts │ │ │ └── version.enum.ts │ ├── funds │ │ ├── funds.module.ts │ │ ├── query │ │ │ ├── query.controller.ts │ │ │ └── query.module.ts │ │ └── command │ │ │ ├── command.module.ts │ │ │ └── command.controller.ts │ ├── account │ │ ├── account.module.ts │ │ ├── query │ │ │ ├── query.module.ts │ │ │ └── query.controller.ts │ │ └── command │ │ │ ├── command.module.ts │ │ │ └── command.controller.ts │ ├── app.module.ts │ └── main.ts ├── tsconfig.build.json ├── .env ├── nest-cli.json ├── README.md ├── tsconfig.json ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts └── package.json ├── shared └── sdk │ ├── README.md │ ├── src │ ├── constants │ │ ├── index.ts │ │ └── kafka.constants.ts │ ├── filters │ │ ├── index.ts │ │ └── http-exception.filter.ts │ ├── services │ │ ├── funds │ │ │ ├── common │ │ │ │ ├── index.ts │ │ │ │ └── entity │ │ │ │ │ ├── index.ts │ │ │ │ │ └── funds.entity.ts │ │ │ ├── receive │ │ │ │ ├── index.ts │ │ │ │ ├── funds-received.event.ts │ │ │ │ └── receive-funds.command.ts │ │ │ ├── deposit │ │ │ │ ├── index.ts │ │ │ │ ├── deposit-funds.dto.ts │ │ │ │ ├── funds-deposited.event.ts │ │ │ │ └── deposit-funds.command.ts │ │ │ ├── transfer │ │ │ │ ├── index.ts │ │ │ │ ├── transfer-funds.dto.ts │ │ │ │ ├── funds-transferred.event.ts │ │ │ │ └── transfer-funds.command.ts │ │ │ ├── withdraw │ │ │ │ ├── index.ts │ │ │ │ ├── withdraw-funds.dto.ts │ │ │ │ ├── funds-withdrawn.event.ts │ │ │ │ └── withdraw-funds.command.ts │ │ │ └── index.ts │ │ ├── account │ │ │ ├── common │ │ │ │ ├── entity │ │ │ │ │ ├── index.ts │ │ │ │ │ └── account.entity.ts │ │ │ │ ├── enums │ │ │ │ │ ├── index.ts │ │ │ │ │ └── account-type.enum.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── open-account │ │ │ │ ├── index.ts │ │ │ │ ├── open-account.dto.ts │ │ │ │ ├── account-opened.event.ts │ │ │ │ └── open-account.command.ts │ │ │ └── close-account │ │ │ │ ├── index.ts │ │ │ │ ├── close-account.dto.ts │ │ │ │ ├── close-account.command.ts │ │ │ │ └── account-closed.event.ts │ │ └── index.ts │ ├── index.ts │ ├── proto │ │ ├── funds-query.proto │ │ ├── account-command.proto │ │ ├── account-query.proto │ │ └── funds-command.proto │ └── pb │ │ ├── funds-query.pb.ts │ │ ├── account-command.pb.ts │ │ ├── account-query.pb.ts │ │ └── funds-command.pb.ts │ ├── tsconfig.json │ ├── package.json │ └── tools │ └── proto-gen.ts ├── pnpm-workspace.yaml ├── .gitignore ├── .prettierignore ├── .github └── static │ └── flowchart.png ├── .env ├── services ├── funds │ ├── query │ │ ├── tsconfig.build.json │ │ ├── .env │ │ ├── src │ │ │ ├── lookup │ │ │ │ ├── get-balance │ │ │ │ │ ├── controller │ │ │ │ │ │ ├── get-balance.dto.ts │ │ │ │ │ │ └── get-balance.controller.ts │ │ │ │ │ ├── query │ │ │ │ │ │ ├── get-balancet.query.ts │ │ │ │ │ │ └── get-balance.handler.ts │ │ │ │ │ └── get-balance.module.ts │ │ │ │ └── lookup.module.ts │ │ │ ├── common │ │ │ │ ├── entity │ │ │ │ │ └── funds.entity.ts │ │ │ │ └── services │ │ │ │ │ ├── kafka.service.ts │ │ │ │ │ └── typeorm.service.ts │ │ │ ├── consumer │ │ │ │ ├── consumer.module.ts │ │ │ │ ├── funds-received │ │ │ │ │ ├── consumer │ │ │ │ │ │ └── funds-received.consumer.ts │ │ │ │ │ ├── funds-received.module.ts │ │ │ │ │ └── event │ │ │ │ │ │ └── funds-received.handler.ts │ │ │ │ ├── funds-withdrawn │ │ │ │ │ ├── consumer │ │ │ │ │ │ └── funds-withdrawn.consumer.ts │ │ │ │ │ ├── funds-withdrawn.module.ts │ │ │ │ │ └── event │ │ │ │ │ │ └── funds-withdrawn.handler.ts │ │ │ │ ├── funds-deposited │ │ │ │ │ ├── consumer │ │ │ │ │ │ └── funds-deposited.consumer.ts │ │ │ │ │ ├── funds-deposited.module.ts │ │ │ │ │ └── event │ │ │ │ │ │ └── funds-deposited.handler.ts │ │ │ │ └── funds-transfered │ │ │ │ │ ├── consumer │ │ │ │ │ └── funds-transferred.consumer.ts │ │ │ │ │ ├── funds-transferred.module.ts │ │ │ │ │ └── event │ │ │ │ │ └── funds-transferred.handler.ts │ │ │ ├── app.module.ts │ │ │ └── main.ts │ │ ├── nest-cli.json │ │ ├── tsconfig.json │ │ ├── test │ │ │ ├── jest-e2e.json │ │ │ └── app.e2e-spec.ts │ │ ├── package.json │ │ └── README.md │ └── command │ │ ├── tsconfig.build.json │ │ ├── nest-cli.json │ │ ├── tsconfig.json │ │ ├── .env │ │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ │ ├── src │ │ ├── receive-funds │ │ │ ├── receive-funds.module.ts │ │ │ ├── events │ │ │ │ └── funds-received.handler.ts │ │ │ └── commands │ │ │ │ └── receive-funds.handler.ts │ │ ├── transfer-funds │ │ │ ├── sagas │ │ │ │ └── transfer-funds.saga.ts │ │ │ ├── events │ │ │ │ └── funds-transferred.handler.ts │ │ │ ├── controllers │ │ │ │ └── transfer-funds.controller.ts │ │ │ ├── transfer-funds.module.ts │ │ │ └── commands │ │ │ │ └── transfer-funds.handler.ts │ │ ├── common │ │ │ ├── options │ │ │ │ └── grpc-client.option.ts │ │ │ ├── producer │ │ │ │ └── funds-event.producer.ts │ │ │ └── aggregates │ │ │ │ └── funds.aggregate.ts │ │ ├── deposit-funds │ │ │ ├── events │ │ │ │ └── funds-deposited.handler.ts │ │ │ ├── controllers │ │ │ │ └── deposit-funds.controller.ts │ │ │ ├── deposit-funds.module.ts │ │ │ └── commands │ │ │ │ └── deposit-funds.handler.ts │ │ ├── withdraw-funds │ │ │ ├── events │ │ │ │ └── funds-withdrawn.handler.ts │ │ │ ├── controllers │ │ │ │ └── withdraw-funds.controller.ts │ │ │ ├── withdraw-funds.module.ts │ │ │ └── commands │ │ │ │ └── withdraw-funds.handler.ts │ │ ├── app.module.ts │ │ └── main.ts │ │ ├── package.json │ │ └── README.md └── account │ ├── command │ ├── tsconfig.build.json │ ├── nest-cli.json │ ├── tsconfig.json │ ├── .env │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── src │ │ ├── common │ │ │ ├── aggregates │ │ │ │ └── account.aggregate.ts │ │ │ └── producer │ │ │ │ └── account-event.producer.ts │ │ ├── open-account │ │ │ ├── events │ │ │ │ └── account-opened.handler.ts │ │ │ ├── aggregates │ │ │ │ └── open-account.aggregate.ts │ │ │ ├── controllers │ │ │ │ └── open-account.controller.ts │ │ │ ├── commands │ │ │ │ └── open-account.handler.ts │ │ │ ├── open-account.module.ts │ │ │ └── sagas │ │ │ │ └── open-account.saga.ts │ │ ├── close-account │ │ │ ├── events │ │ │ │ └── account-closed.handler.ts │ │ │ ├── close-account.module.ts │ │ │ ├── controllers │ │ │ │ └── close-account.controller.ts │ │ │ ├── aggregates │ │ │ │ └── close-account.aggregate.ts │ │ │ └── commands │ │ │ │ └── close-account.handler.ts │ │ ├── app.module.ts │ │ └── main.ts │ ├── package.json │ └── README.md │ └── query │ ├── tsconfig.build.json │ ├── .env │ ├── src │ ├── lookup │ │ ├── find-account │ │ │ ├── controller │ │ │ │ ├── find-account.dto.ts │ │ │ │ └── find-account.controller.ts │ │ │ ├── query │ │ │ │ ├── find-account.query.ts │ │ │ │ └── find-account.handler.ts │ │ │ └── find-account.module.ts │ │ ├── find-all-accounts │ │ │ ├── controller │ │ │ │ ├── find-all-accounts.dto.ts │ │ │ │ └── find-all-accounts.controller.ts │ │ │ ├── query │ │ │ │ ├── find-all-accounts.query.ts │ │ │ │ └── find-all-accounts.handler.ts │ │ │ └── find-all-accounts.module.ts │ │ └── lookup.module.ts │ ├── consumer │ │ ├── consumer.module.ts │ │ ├── account-opened │ │ │ ├── event │ │ │ │ └── account-opened.handler.ts │ │ │ ├── consumer │ │ │ │ └── account-opened.consumer.ts │ │ │ └── account-opened.module.ts │ │ └── account-closed │ │ │ ├── account-closed.module.ts │ │ │ ├── event │ │ │ └── account-closed.handler.ts │ │ │ └── consumer │ │ │ └── account-closed.consumer.ts │ ├── app.module.ts │ ├── common │ │ ├── services │ │ │ └── typeorm.service.ts │ │ └── entity │ │ │ └── account.entity.ts │ └── main.ts │ ├── nest-cli.json │ ├── tsconfig.json │ ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts │ ├── package.json │ └── README.md ├── .editorconfig ├── .prettierrc ├── tsconfig.json ├── README.md ├── .eslintrc.js ├── package.json └── docker-compose.yml /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.0 -------------------------------------------------------------------------------- /tools/postgres/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE funds; -------------------------------------------------------------------------------- /gateway/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enums'; 2 | -------------------------------------------------------------------------------- /gateway/src/common/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './version.enum'; 2 | -------------------------------------------------------------------------------- /shared/sdk/README.md: -------------------------------------------------------------------------------- 1 | ```sh 2 | $ ts-node tools/proto-gen.ts 3 | ``` 4 | -------------------------------------------------------------------------------- /shared/sdk/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kafka.constants'; 2 | -------------------------------------------------------------------------------- /shared/sdk/src/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-exception.filter'; 2 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity'; 2 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/common/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './funds.entity'; 2 | -------------------------------------------------------------------------------- /gateway/src/common/enums/version.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Version { 2 | One = '1', 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'shared/*' 3 | - 'services/*/*' 4 | - 'gateway' 5 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/common/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.entity'; 2 | -------------------------------------------------------------------------------- /shared/sdk/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account'; 2 | export * from './funds'; 3 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/common/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-type.enum'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .pnpm 5 | .local 6 | .cache 7 | *.tsbuildinfo 8 | *.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test 4 | *.pb.ts 5 | *.tsbuildinfo 6 | *.proto 7 | *.spec.ts -------------------------------------------------------------------------------- /shared/sdk/src/constants/kafka.constants.ts: -------------------------------------------------------------------------------- 1 | export const KAFKA_SERVICE_NAME: string = 'KAFKA_SERVICE'; 2 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity'; 2 | export * from './enums'; 3 | -------------------------------------------------------------------------------- /.github/static/flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellokvn/bank-api/HEAD/.github/static/flowchart.png -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2021" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/receive/index.ts: -------------------------------------------------------------------------------- 1 | export * from './funds-received.event'; 2 | export * from './receive-funds.command'; 3 | -------------------------------------------------------------------------------- /gateway/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './close-account'; 2 | export * from './common'; 3 | export * from './open-account'; 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Postgres 2 | 3 | PSQL_DB_USER=user 4 | PSQL_DB_PASS=password 5 | PSQL_DB_NAME=account 6 | 7 | # MongoDB 8 | 9 | MONGODB_DB_NAME=account -------------------------------------------------------------------------------- /services/funds/query/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /services/account/command/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /services/account/query/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /services/funds/command/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/common/enums/account-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AccountType { 2 | SAVINGS = 'SAVINGS', 3 | CURRENT = 'CURRENT', 4 | } 5 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/deposit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deposit-funds.command'; 2 | export * from './deposit-funds.dto'; 3 | export * from './funds-deposited.event'; 4 | -------------------------------------------------------------------------------- /services/funds/query/.env: -------------------------------------------------------------------------------- 1 | # Internal Service 2 | 3 | GRPC_URL=0.0.0.0:50054 4 | DB_URL=postgres://user:password@0.0.0.0:5432/funds 5 | 6 | # Misc 7 | 8 | KAFKA_URL=localhost:9092 -------------------------------------------------------------------------------- /shared/sdk/src/services/account/open-account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-opened.event'; 2 | export * from './open-account.command'; 3 | export * from './open-account.dto'; 4 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/transfer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './funds-transferred.event'; 2 | export * from './transfer-funds.command'; 3 | export * from './transfer-funds.dto'; 4 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/withdraw/index.ts: -------------------------------------------------------------------------------- 1 | export * from './funds-withdrawn.event'; 2 | export * from './withdraw-funds.command'; 3 | export * from './withdraw-funds.dto'; 4 | -------------------------------------------------------------------------------- /services/account/query/.env: -------------------------------------------------------------------------------- 1 | # Internal Service 2 | 3 | GRPC_URL=0.0.0.0:50052 4 | DB_URL=postgres://user:password@0.0.0.0:5432/account 5 | 6 | # Misc 7 | 8 | KAFKA_URL=localhost:9092 -------------------------------------------------------------------------------- /shared/sdk/src/services/account/close-account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-closed.event'; 2 | export * from './close-account.command'; 3 | export * from './close-account.dto'; 4 | -------------------------------------------------------------------------------- /gateway/.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | 3 | ACCOUNT_COMMAND_GRPC_URL=0.0.0.0:50051 4 | ACCOUNT_QUERY_GRPC_URL=0.0.0.0:50052 5 | 6 | FUNDS_COMMAND_GRPC_URL=0.0.0.0:50053 7 | FUNDS_QUERY_GRPC_URL=0.0.0.0:50054 -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './deposit'; 3 | export * from './receive'; 4 | export * from './transfer'; 5 | export * from './withdraw'; 6 | -------------------------------------------------------------------------------- /tools/nest-mono-new/README.md: -------------------------------------------------------------------------------- 1 | # Add 2 | 3 | # NestJS Generator 4 | 5 | ## Add new NestJS CQRS Applications 6 | 7 | ```sh 8 | $ ts-node tools/nest-mono-new/index.ts funds 9 | ``` 10 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/close-account/close-account.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID } from 'class-validator'; 2 | 3 | export class CloseAccountDto { 4 | @IsUUID() 5 | public id: string; 6 | } 7 | -------------------------------------------------------------------------------- /shared/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "exclude": ["tools", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,ts,json}] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /services/funds/query/src/lookup/get-balance/controller/get-balance.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID } from 'class-validator'; 2 | 3 | export class GetBalanceDto { 4 | @IsUUID() 5 | public id: string; 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"], 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "always", 6 | "tabWidth": 2, 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-account/controller/find-account.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID } from 'class-validator'; 2 | 3 | export class FindAccountDto { 4 | @IsUUID() 5 | public id: string; 6 | } 7 | -------------------------------------------------------------------------------- /gateway/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 | } 8 | } 9 | -------------------------------------------------------------------------------- /gateway/README.md: -------------------------------------------------------------------------------- 1 | # API Gateway 2 | 3 | ## Add new microservice 4 | 5 | ```sh 6 | $ nest g mo funds 7 | $ nest g mo funds/command && nest g co funds/command 8 | $ nest g mo funds/query && nest g co funds/query 9 | ``` 10 | -------------------------------------------------------------------------------- /gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist", 6 | "paths": { 7 | "@app/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/funds/query/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 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/account/command/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 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/account/query/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 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/funds/command/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 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/account/command/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./", 6 | "paths": { 7 | "@app/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/account/query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./", 6 | "paths": { 7 | "@app/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/funds/command/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist", 6 | "paths": { 7 | "@app/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/funds/query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist", 6 | "paths": { 7 | "@app/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gateway/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/funds/command/.env: -------------------------------------------------------------------------------- 1 | # Internal Service 2 | 3 | GRPC_URL=0.0.0.0:50053 4 | DB_URL=mongodb://0.0.0.0:27017/funds 5 | 6 | # External Services 7 | 8 | ACCOUNT_QUERY_GRPC_URL=0.0.0.0:50052 9 | 10 | # Misc 11 | 12 | KAFKA_URL=localhost:9092 -------------------------------------------------------------------------------- /services/account/command/.env: -------------------------------------------------------------------------------- 1 | # Internal Service 2 | 3 | GRPC_URL=0.0.0.0:50051 4 | DB_URL=mongodb://0.0.0.0:27017/account 5 | 6 | # External Services 7 | 8 | FUNDS_COMMAND_GRPC_URL=0.0.0.0:50053 9 | 10 | # Misc 11 | 12 | KAFKA_URL=localhost:9092 -------------------------------------------------------------------------------- /services/funds/query/src/lookup/lookup.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GetBalanceModule } from './get-balance/get-balance.module'; 3 | 4 | @Module({ 5 | imports: [GetBalanceModule], 6 | }) 7 | export class LookupModule {} 8 | -------------------------------------------------------------------------------- /services/funds/query/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/account/command/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-all-accounts/controller/find-all-accounts.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsInt, IsOptional, Min } from 'class-validator'; 2 | 3 | export class FindAllAccountsDto { 4 | @IsInt() 5 | @Min(1) 6 | @IsOptional() 7 | public page = 1; 8 | } 9 | -------------------------------------------------------------------------------- /services/account/query/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/funds/command/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /shared/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './filters'; 3 | export * from './pb/account-command.pb'; 4 | export * from './pb/account-query.pb'; 5 | export * from './pb/funds-command.pb'; 6 | export * from './pb/funds-query.pb'; 7 | export * from './services'; 8 | -------------------------------------------------------------------------------- /gateway/src/funds/funds.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommandModule } from './command/command.module'; 3 | import { QueryModule } from './query/query.module'; 4 | 5 | @Module({ 6 | imports: [CommandModule, QueryModule], 7 | }) 8 | export class FundsModule {} 9 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/close-account/close-account.command.ts: -------------------------------------------------------------------------------- 1 | import { CloseAccountDto } from './close-account.dto'; 2 | 3 | export class CloseAccountCommand { 4 | public id: string; 5 | 6 | constructor(payload: CloseAccountDto) { 7 | this.id = payload.id; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gateway/src/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommandModule } from './command/command.module'; 3 | import { QueryModule } from './query/query.module'; 4 | 5 | @Module({ 6 | imports: [CommandModule, QueryModule], 7 | }) 8 | export class AccountModule {} 9 | -------------------------------------------------------------------------------- /services/funds/query/src/lookup/get-balance/query/get-balancet.query.ts: -------------------------------------------------------------------------------- 1 | import { GetBalanceDto } from '../controller/get-balance.dto'; 2 | 3 | export class GetBalanceQuery { 4 | public id: string; 5 | 6 | constructor(payload: GetBalanceDto) { 7 | this.id = payload.id; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-account/query/find-account.query.ts: -------------------------------------------------------------------------------- 1 | import { FindAccountDto } from '../controller/find-account.dto'; 2 | 3 | export class FindAccountQuery { 4 | public id: string; 5 | 6 | constructor(payload: FindAccountDto) { 7 | this.id = payload.id; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-all-accounts/query/find-all-accounts.query.ts: -------------------------------------------------------------------------------- 1 | import { FindAllAccountsDto } from '../controller/find-all-accounts.dto'; 2 | 3 | export class FindAllAccountsQuery { 4 | public page: number; 5 | 6 | constructor(query: FindAllAccountsDto) { 7 | this.page = query.page; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/consumer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountClosedModule } from './account-closed/account-closed.module'; 3 | import { AccountOpenedModule } from './account-opened/account-opened.module'; 4 | 5 | @Module({ imports: [AccountOpenedModule, AccountClosedModule] }) 6 | export class ConsumerModule {} 7 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/lookup.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FindAccountModule } from './find-account/find-account.module'; 3 | import { FindAllAccountsModule } from './find-all-accounts/find-all-accounts.module'; 4 | 5 | @Module({ 6 | imports: [FindAllAccountsModule, FindAccountModule], 7 | }) 8 | export class LookupModule {} 9 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/deposit/deposit-funds.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsUUID, Min } from 'class-validator'; 2 | import { DepositFundsRequest } from '../../../pb/funds-command.pb'; 3 | 4 | export class DepositFundsDto implements DepositFundsRequest { 5 | @IsUUID() 6 | public id: string; 7 | 8 | @IsNumber({ allowInfinity: false, allowNaN: false }) 9 | @Min(1) 10 | public amount: number; 11 | } 12 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/withdraw/withdraw-funds.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsUUID, Min } from 'class-validator'; 2 | import { WithdrawFundsRequest } from '../../../pb/funds-command.pb'; 3 | 4 | export class WithdrawFundsDto implements WithdrawFundsRequest { 5 | @IsUUID() 6 | public id: string; 7 | 8 | @IsNumber({ allowInfinity: false, allowNaN: false }) 9 | @Min(1) 10 | public amount: number; 11 | } 12 | -------------------------------------------------------------------------------- /shared/sdk/src/proto/funds-query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package funds_query; 4 | 5 | option go_package = "./"; 6 | 7 | service FundsQueryService { 8 | rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {} 9 | } 10 | 11 | // GetBalance 12 | 13 | message GetBalanceRequest { string id = 1; } 14 | 15 | message GetBalanceResponse { 16 | int32 status = 1; 17 | repeated string error = 2; 18 | int32 data = 3; 19 | } -------------------------------------------------------------------------------- /services/funds/query/src/common/entity/funds.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Funds { 5 | @PrimaryColumn('uuid') 6 | public id: string; 7 | 8 | @Column({ default: 0 }) 9 | public balance: number; 10 | 11 | @CreateDateColumn({ name: 'created_date' }) 12 | public createdDate: Date; 13 | 14 | @UpdateDateColumn({ name: 'updated_date' }) 15 | public updatedDate: Date; 16 | } 17 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/receive/funds-received.event.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from 'nestjs-event-sourcing'; 2 | import { ReceiveFundsCommand } from './receive-funds.command'; 3 | 4 | export class FundsReceivedEvent extends BaseEvent { 5 | public amount: number; 6 | 7 | constructor(command?: ReceiveFundsCommand) { 8 | super(); 9 | 10 | if (!command) { 11 | return; 12 | } 13 | 14 | this.id = command.id; 15 | this.amount = command.getAmount(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/common/entity/funds.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Funds { 5 | @PrimaryColumn('uuid') 6 | public id: string; 7 | 8 | @Column({ default: 0 }) 9 | public balance: number; 10 | 11 | @CreateDateColumn({ name: 'created_date' }) 12 | public createdDate: Date; 13 | 14 | @UpdateDateColumn({ name: 'updated_date' }) 15 | public updatedDate: Date; 16 | } 17 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/deposit/funds-deposited.event.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from 'nestjs-event-sourcing'; 2 | import { DepositFundsCommand } from './deposit-funds.command'; 3 | 4 | export class FundsDepositedEvent extends BaseEvent { 5 | public amount: number; 6 | 7 | constructor(command?: DepositFundsCommand) { 8 | super(); 9 | 10 | if (!command) { 11 | return; 12 | } 13 | 14 | this.id = command.id; 15 | this.amount = command.getAmount(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/transfer/transfer-funds.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsUUID, Min } from 'class-validator'; 2 | import { TransferFundsRequest } from '../../../pb/funds-command.pb'; 3 | 4 | export class TransferFundsDto implements TransferFundsRequest { 5 | @IsUUID() 6 | public readonly fromId: string; 7 | 8 | @IsUUID() 9 | public readonly toId: string; 10 | 11 | @IsNumber({ allowInfinity: false, allowNaN: false }) 12 | @Min(1) 13 | public readonly amount: number; 14 | } 15 | -------------------------------------------------------------------------------- /gateway/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { AccountModule } from './account/account.module'; 4 | import { FundsModule } from './funds/funds.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | ConfigModule.forRoot({ 9 | isGlobal: true, 10 | envFilePath: process.env.IS_DOCKER ? '.docker.env' : '.env', 11 | }), 12 | AccountModule, 13 | FundsModule, 14 | ], 15 | }) 16 | export class AppModule {} 17 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/close-account/account-closed.event.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from 'nestjs-event-sourcing'; 2 | import { CloseAccountCommand } from './close-account.command'; 3 | 4 | export const ACCOUNT_CLOSED_EVENT_NAME: string = 'AccountClosedEvent'; 5 | 6 | export class AccountClosedEvent extends BaseEvent { 7 | constructor(command?: CloseAccountCommand) { 8 | super(); 9 | 10 | if (!command) { 11 | return; 12 | } 13 | 14 | this.id = command.id; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/withdraw/funds-withdrawn.event.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from 'nestjs-event-sourcing'; 2 | import { WithdrawFundsCommand } from './withdraw-funds.command'; 3 | 4 | export class FundsWithdrawnEvent extends BaseEvent { 5 | public amount: number; 6 | 7 | constructor(command?: WithdrawFundsCommand) { 8 | super(); 9 | 10 | if (!command) { 11 | return; 12 | } 13 | 14 | this.id = command.id; 15 | this.amount = command.getAmount(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/open-account/open-account.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; 2 | import { AccountType } from '../common/enums'; 3 | 4 | export class OpenAccountDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | public holder: string; 8 | 9 | @IsEmail() 10 | public email: string; 11 | 12 | @IsEnum(AccountType) 13 | public type: AccountType; 14 | 15 | @IsNumber({ allowInfinity: false, allowNaN: false }) 16 | @Min(0) 17 | public openingBalance: number; 18 | } 19 | -------------------------------------------------------------------------------- /shared/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bank/sdk", 3 | "version": "1.0.0", 4 | "description": "SDK", 5 | "private": true, 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "files": [ 9 | "./dist" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "build": "tsc --build", 14 | "build:all": "ts-node tools/proto-gen.ts", 15 | "dev": "watch 'pnpm run build' ./src", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "devDependencies": { 19 | "ts-proto": "^1.147.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/deposit/deposit-funds.command.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from 'nestjs-event-sourcing'; 2 | import { DepositFundsDto } from './deposit-funds.dto'; 3 | 4 | export class DepositFundsCommand extends BaseCommand { 5 | private amount: number; 6 | 7 | constructor(payload: DepositFundsDto) { 8 | super(); 9 | 10 | this.id = payload.id; 11 | this.amount = payload.amount; 12 | } 13 | 14 | public getAmount(): number { 15 | return this.amount; 16 | } 17 | 18 | public setAmount(value: number) { 19 | this.amount = value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/transfer/funds-transferred.event.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from 'nestjs-event-sourcing'; 2 | import { TransferFundsCommand } from './transfer-funds.command'; 3 | 4 | export class FundsTransferredEvent extends BaseEvent { 5 | public targetedId: string; 6 | public amount: number; 7 | 8 | constructor(command?: TransferFundsCommand) { 9 | super(); 10 | 11 | if (!command) { 12 | return; 13 | } 14 | 15 | this.id = command.id; 16 | this.targetedId = command.getTargetedId(); 17 | this.amount = command.getAmount(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/withdraw/withdraw-funds.command.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from 'nestjs-event-sourcing'; 2 | import { WithdrawFundsDto } from './withdraw-funds.dto'; 3 | 4 | export class WithdrawFundsCommand extends BaseCommand { 5 | private amount: number; 6 | 7 | constructor(payload: WithdrawFundsDto) { 8 | super(); 9 | 10 | this.id = payload.id; 11 | this.amount = payload.amount; 12 | } 13 | 14 | public getAmount(): number { 15 | return this.amount; 16 | } 17 | 18 | public setAmount(value: number) { 19 | this.amount = value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/consumer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FundsDepositedModule } from './funds-deposited/funds-deposited.module'; 3 | import { FundsReceivedModule } from './funds-received/funds-received.module'; 4 | import { FundsTransferredModule } from './funds-transfered/funds-transferred.module'; 5 | import { FundsWithdrawnModule } from './funds-withdrawn/funds-withdrawn.module'; 6 | 7 | @Module({ 8 | imports: [FundsDepositedModule, FundsTransferredModule, FundsWithdrawnModule, FundsReceivedModule], 9 | }) 10 | export class ConsumerModule {} 11 | -------------------------------------------------------------------------------- /services/account/command/src/common/aggregates/account.aggregate.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedAggregateRoot } from 'nestjs-event-sourcing'; 2 | 3 | export class AccountAggregate extends ExtendedAggregateRoot { 4 | private isActive: boolean; 5 | private balance: number; 6 | 7 | public getActive(): boolean { 8 | return this.isActive; 9 | } 10 | 11 | public setActive(value: boolean): void { 12 | this.isActive = value; 13 | } 14 | 15 | public getBalance(): number { 16 | return this.balance; 17 | } 18 | 19 | public setBalance(value: number): void { 20 | this.balance = value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/funds/query/src/lookup/get-balance/get-balance.module.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { CqrsModule } from '@nestjs/cqrs'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { GetBalanceController } from './controller/get-balance.controller'; 6 | import { GetBalanceQueryHandler } from './query/get-balance.handler'; 7 | 8 | @Module({ 9 | imports: [CqrsModule, TypeOrmModule.forFeature([Funds])], 10 | controllers: [GetBalanceController], 11 | providers: [GetBalanceQueryHandler], 12 | }) 13 | export class GetBalanceModule {} 14 | -------------------------------------------------------------------------------- /services/funds/command/src/receive-funds/receive-funds.module.ts: -------------------------------------------------------------------------------- 1 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 2 | import { Module } from '@nestjs/common'; 3 | import { CqrsModule } from '@nestjs/cqrs'; 4 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 5 | import { RceiveFundsHandler } from './commands/receive-funds.handler'; 6 | import { FundsReceivedHandler } from './events/funds-received.handler'; 7 | 8 | @Module({ 9 | imports: [CqrsModule], 10 | providers: [RceiveFundsHandler, FundsReceivedHandler, FundsEventProducer, EventSourcingHandler], 11 | }) 12 | export class ReceiveFundsModule {} 13 | -------------------------------------------------------------------------------- /shared/sdk/src/filters/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | @Catch(HttpException) 4 | export class HttpExceptionFilter implements ExceptionFilter { 5 | catch(exception: HttpException) { 6 | const status: HttpStatus = exception.getStatus(); 7 | const error: string | object | any = exception.getResponse(); 8 | const timestamp: string = new Date().toISOString(); 9 | 10 | if (typeof error === 'string') { 11 | return { status, timestamp, error: [error] }; 12 | } 13 | 14 | return { status, timestamp, error: error.message }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-account/find-account.module.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { CqrsModule } from '@nestjs/cqrs'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { FindAccountController } from './controller/find-account.controller'; 6 | import { FindAccountQueryHandler } from './query/find-account.handler'; 7 | 8 | @Module({ 9 | imports: [CqrsModule, TypeOrmModule.forFeature([Account])], 10 | controllers: [FindAccountController], 11 | providers: [FindAccountQueryHandler], 12 | }) 13 | export class FindAccountModule {} 14 | -------------------------------------------------------------------------------- /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": "es2017", 10 | "sourceMap": true, 11 | "incremental": true, 12 | "skipLibCheck": true, 13 | "strictNullChecks": false, 14 | "noImplicitAny": false, 15 | "strictBindCallApply": false, 16 | "forceConsistentCasingInFileNames": false, 17 | "noFallthroughCasesInSwitch": false, 18 | "composite": false, 19 | "typeRoots": ["@types"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bank API with NestJS, CQRS, DDD, Event Sourcing 2 | 3 | ## Description 4 | 5 | This repository is part of my article on Medium: 6 | [Microservices with CQRS and Event Sourcing in TypeScript with NestJS](https://medium.com/gitconnected/microservices-with-cqrs-in-typescript-and-nestjs-5a8af0a56c3a) 7 | 8 | ## Installation 9 | 10 | ```bash 11 | $ nvm use 12 | $ pnpm install 13 | $ pnpm run build 14 | $ docker-compose up 15 | ``` 16 | 17 | ## Open Account Flowchart 18 | 19 | ![flowchart](https://raw.githubusercontent.com/hellokvn/bank-api/master/.github/static/flowchart.png) 20 | 21 | ## Author 22 | 23 | - [Kevin Vogel](https://medium.com/@hellokevinvogel) 24 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-all-accounts/find-all-accounts.module.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { CqrsModule } from '@nestjs/cqrs'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { FindAllAccountsController } from './controller/find-all-accounts.controller'; 6 | import { FindAllAccountsQueryHandler } from './query/find-all-accounts.handler'; 7 | 8 | @Module({ 9 | imports: [CqrsModule, TypeOrmModule.forFeature([Account])], 10 | controllers: [FindAllAccountsController], 11 | providers: [FindAllAccountsQueryHandler], 12 | }) 13 | export class FindAllAccountsModule {} 14 | -------------------------------------------------------------------------------- /services/funds/query/src/lookup/get-balance/query/get-balance.handler.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { ICommandHandler, QueryHandler } from '@nestjs/cqrs'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { GetBalanceQuery } from './get-balancet.query'; 6 | 7 | @QueryHandler(GetBalanceQuery) 8 | export class GetBalanceQueryHandler implements ICommandHandler { 9 | @InjectRepository(Funds) 10 | private readonly repository: Repository; 11 | 12 | public execute({ id }: GetBalanceQuery): Promise { 13 | return this.repository.findOneBy({ id }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/funds/command/src/transfer-funds/sagas/transfer-funds.saga.ts: -------------------------------------------------------------------------------- 1 | import { FundsTransferredEvent, ReceiveFundsCommand } from '@bank/sdk'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { ICommand, ofType, Saga } from '@nestjs/cqrs'; 4 | import { delay, map, Observable } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class TransferFundsSaga { 8 | @Saga() 9 | private onEvent(events$: Observable): Observable { 10 | return events$.pipe( 11 | ofType(FundsTransferredEvent), 12 | delay(1000), 13 | map((event: FundsTransferredEvent) => { 14 | return new ReceiveFundsCommand(event); 15 | }), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-account/query/find-account.handler.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { ICommandHandler, QueryHandler } from '@nestjs/cqrs'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { FindAccountQuery } from './find-account.query'; 6 | 7 | @QueryHandler(FindAccountQuery) 8 | export class FindAccountQueryHandler implements ICommandHandler { 9 | @InjectRepository(Account) 10 | private readonly repository: Repository; 11 | 12 | public execute({ id }: FindAccountQuery): Promise { 13 | return this.repository.findOneBy({ id }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gateway/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/account/query/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/funds/command/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/funds/query/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/account/command/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/funds/command/src/common/options/grpc-client.option.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_PACKAGE_NAME, ACCOUNT_QUERY_SERVICE_NAME } from '@bank/sdk'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { ClientsProviderAsyncOptions, Transport } from '@nestjs/microservices'; 4 | 5 | export const ACCOUNT_QUERY_PROVIDER: ClientsProviderAsyncOptions = { 6 | name: ACCOUNT_QUERY_SERVICE_NAME, 7 | inject: [ConfigService], 8 | useFactory: (config: ConfigService) => ({ 9 | transport: Transport.GRPC, 10 | options: { 11 | url: config.get('ACCOUNT_QUERY_GRPC_URL'), 12 | package: ACCOUNT_QUERY_PACKAGE_NAME, 13 | protoPath: 'node_modules/@bank/sdk/dist/proto/account-query.proto', 14 | }, 15 | }), 16 | }; 17 | -------------------------------------------------------------------------------- /services/account/command/src/open-account/events/account-opened.handler.ts: -------------------------------------------------------------------------------- 1 | import { AccountEventProducer } from '@app/common/producer/account-event.producer'; 2 | import { AccountOpenedEvent, ACCOUNT_OPENED_EVENT_NAME } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | 6 | @EventsHandler(AccountOpenedEvent) 7 | export class AccountOpenedHandler implements IEventHandler { 8 | @Inject(AccountEventProducer) 9 | private readonly eventProducer: AccountEventProducer; 10 | 11 | public handle(event: AccountOpenedEvent): void { 12 | console.log('AccountOpenedHandler/handle'); 13 | this.eventProducer.produce(ACCOUNT_OPENED_EVENT_NAME, event); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/funds/query/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { CqrsModule } from '@nestjs/cqrs'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { TypeOrmConfigService } from './common/services/typeorm.service'; 6 | import { ConsumerModule } from './consumer/consumer.module'; 7 | import { LookupModule } from './lookup/lookup.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.IS_DOCKER ? '.docker.env' : '.env' }), 12 | TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService }), 13 | CqrsModule, 14 | ConsumerModule, 15 | LookupModule, 16 | ], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /services/account/command/src/close-account/events/account-closed.handler.ts: -------------------------------------------------------------------------------- 1 | import { AccountEventProducer } from '@app/common/producer/account-event.producer'; 2 | import { AccountClosedEvent, ACCOUNT_CLOSED_EVENT_NAME } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | 6 | @EventsHandler(AccountClosedEvent) 7 | export class AccountClosedHandler implements IEventHandler { 8 | @Inject(AccountEventProducer) 9 | private readonly eventProducer: AccountEventProducer; 10 | 11 | public async handle(event: AccountClosedEvent) { 12 | console.log('AccountClosedHandler/handle', { event }); 13 | this.eventProducer.produce(ACCOUNT_CLOSED_EVENT_NAME, event); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /services/account/command/src/close-account/close-account.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 4 | import { AccountEventProducer } from '../common/producer/account-event.producer'; 5 | import { CloseAccountHandler } from './commands/close-account.handler'; 6 | import { CloseAccountController } from './controllers/close-account.controller'; 7 | import { AccountClosedHandler } from './events/account-closed.handler'; 8 | 9 | @Module({ 10 | imports: [CqrsModule], 11 | controllers: [CloseAccountController], 12 | providers: [CloseAccountHandler, AccountClosedHandler, AccountEventProducer, EventSourcingHandler], 13 | }) 14 | export class CloseAccountModule {} 15 | -------------------------------------------------------------------------------- /services/account/command/src/open-account/aggregates/open-account.aggregate.ts: -------------------------------------------------------------------------------- 1 | import { AccountAggregate } from '@app/common/aggregates/account.aggregate'; 2 | import { AccountOpenedEvent, OpenAccountCommand } from '@bank/sdk'; 3 | 4 | export class OpenAccountAggregate extends AccountAggregate { 5 | public openAccount(command: OpenAccountCommand): void { 6 | console.log('AccountAggregate/openAccount'); 7 | const event: AccountOpenedEvent = new AccountOpenedEvent(command); 8 | // logic 9 | this.apply(event); 10 | } 11 | 12 | public onAccountOpenedEvent(event: AccountOpenedEvent): void { 13 | console.log('AccountAggregate/onAccountOpenedEvent'); 14 | this.id = event.id; 15 | this.setActive(true); 16 | this.setBalance(event.openingBalance); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-received/consumer/funds-received.consumer.ts: -------------------------------------------------------------------------------- 1 | import { FundsReceivedEvent } from '@bank/sdk'; 2 | import { Controller, Inject } from '@nestjs/common'; 3 | import { EventBus } from '@nestjs/cqrs'; 4 | import { MessagePattern, Payload } from '@nestjs/microservices'; 5 | import { plainToClass } from 'class-transformer'; 6 | import { KafkaMessage } from 'kafkajs'; 7 | 8 | @Controller() 9 | export class FundsReceivedConsumer { 10 | @Inject(EventBus) 11 | private readonly eventBus: EventBus; 12 | 13 | @MessagePattern('FundsReceivedEvent') 14 | private fundsReceived(@Payload() payload: KafkaMessage): void { 15 | const event: FundsReceivedEvent = plainToClass(FundsReceivedEvent, payload); 16 | 17 | this.eventBus.publish(event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/account/query/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { CqrsModule } from '@nestjs/cqrs'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { TypeOrmConfigService } from './common/services/typeorm.service'; 6 | import { ConsumerModule } from './consumer/consumer.module'; 7 | import { LookupModule } from './lookup/lookup.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule.forRoot({ 12 | isGlobal: true, 13 | envFilePath: process.env.IS_DOCKER ? '.docker.env' : '.env', 14 | }), 15 | TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService }), 16 | CqrsModule, 17 | ConsumerModule, 18 | LookupModule, 19 | ], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-withdrawn/consumer/funds-withdrawn.consumer.ts: -------------------------------------------------------------------------------- 1 | import { FundsWithdrawnEvent } from '@bank/sdk'; 2 | import { Controller, Inject } from '@nestjs/common'; 3 | import { EventBus } from '@nestjs/cqrs'; 4 | import { MessagePattern, Payload } from '@nestjs/microservices'; 5 | import { plainToClass } from 'class-transformer'; 6 | import { KafkaMessage } from 'kafkajs'; 7 | 8 | @Controller() 9 | export class FundsWithdrawnConsumer { 10 | @Inject(EventBus) 11 | private readonly eventBus: EventBus; 12 | 13 | @MessagePattern('FundsWithdrawnEvent') 14 | private fundsWithdrawn(@Payload() payload: KafkaMessage): void { 15 | const event: FundsWithdrawnEvent = plainToClass(FundsWithdrawnEvent, payload); 16 | 17 | this.eventBus.publish(event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/funds/command/src/receive-funds/events/funds-received.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 2 | import { FundsReceivedEvent } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | 6 | @EventsHandler(FundsReceivedEvent) 7 | export class FundsReceivedHandler implements IEventHandler { 8 | @Inject(FundsEventProducer) 9 | private readonly eventProducer: FundsEventProducer; 10 | 11 | public handle(event: FundsReceivedEvent): void { 12 | console.log('FundsReceivedEvent', { event }); 13 | const { constructor }: FundsReceivedEvent = Object.getPrototypeOf(event); 14 | 15 | this.eventProducer.produce(constructor.name, event); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /services/funds/query/src/common/services/kafka.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { ClientProvider, ClientsModuleOptionsFactory, Transport } from '@nestjs/microservices'; 4 | 5 | @Injectable() 6 | export class KafkaConfigService implements ClientsModuleOptionsFactory { 7 | @Inject(ConfigService) 8 | private readonly config: ConfigService; 9 | 10 | public createClientOptions(): ClientProvider { 11 | return { 12 | transport: Transport.KAFKA, 13 | options: { 14 | client: { 15 | clientId: 'my-app', 16 | brokers: [this.config.get('KAFKA_URL')], 17 | }, 18 | consumer: { 19 | groupId: 'test-group', 20 | }, 21 | }, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/funds/command/src/deposit-funds/events/funds-deposited.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 2 | import { FundsDepositedEvent } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | 6 | @EventsHandler(FundsDepositedEvent) 7 | export class FundsDepositedHandler implements IEventHandler { 8 | @Inject(FundsEventProducer) 9 | private readonly eventProducer: FundsEventProducer; 10 | 11 | public handle(event: FundsDepositedEvent): void { 12 | console.log('FundsDepositedHandler', { event }); 13 | const { constructor }: FundsDepositedEvent = Object.getPrototypeOf(event); 14 | 15 | this.eventProducer.produce(constructor.name, event); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shared/sdk/src/proto/account-command.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package account_command; 4 | 5 | option go_package = "./"; 6 | 7 | service AccountCommandService { 8 | rpc OpenAccount(OpenAccountRequest) returns (OpenAccountResponse) {} 9 | rpc CloseAccount(CloseAccountRequest) returns (CloseAccountResponse) {} 10 | } 11 | 12 | // OpenAccount 13 | 14 | message OpenAccountRequest { 15 | string holder = 1; 16 | string type = 2; 17 | string email = 3; 18 | int32 openingBalance = 4; 19 | } 20 | 21 | message OpenAccountResponse { 22 | int32 status = 1; 23 | repeated string error = 2; 24 | string data = 3; 25 | } 26 | 27 | // CloseAccount 28 | 29 | message CloseAccountRequest { string id = 1; } 30 | 31 | message CloseAccountResponse { 32 | int32 status = 1; 33 | repeated string error = 2; 34 | } -------------------------------------------------------------------------------- /services/funds/command/src/withdraw-funds/events/funds-withdrawn.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 2 | import { FundsWithdrawnEvent } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | 6 | @EventsHandler(FundsWithdrawnEvent) 7 | export class FundsWithdrawnHandler implements IEventHandler { 8 | @Inject(FundsEventProducer) 9 | private readonly eventProducer: FundsEventProducer; 10 | 11 | public handle(event: FundsWithdrawnEvent): void { 12 | console.log('FundsWithdrawedHandler/handle', { event }); 13 | const { constructor }: FundsWithdrawnEvent = Object.getPrototypeOf(event); 14 | 15 | this.eventProducer.produce(constructor.name, event); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /services/account/command/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { EventSourcingModule } from 'nestjs-event-sourcing'; 4 | import { CloseAccountModule } from './close-account/close-account.module'; 5 | import { OpenAccountModule } from './open-account/open-account.module'; 6 | 7 | @Module({ 8 | imports: [ 9 | ConfigModule.forRoot({ 10 | isGlobal: true, 11 | envFilePath: process.env.IS_DOCKER ? '.docker.env' : '.env', 12 | }), 13 | EventSourcingModule.forRootAsync({ 14 | inject: [ConfigService], 15 | useFactory: (config: ConfigService) => ({ 16 | mongoUrl: config.get('DB_URL'), 17 | }), 18 | }), 19 | OpenAccountModule, 20 | CloseAccountModule, 21 | ], 22 | }) 23 | export class AppModule {} 24 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/transfer/transfer-funds.command.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from 'nestjs-event-sourcing'; 2 | import { TransferFundsDto } from './transfer-funds.dto'; 3 | 4 | export class TransferFundsCommand extends BaseCommand { 5 | private targetedId: string; 6 | private amount: number; 7 | 8 | constructor(payload: TransferFundsDto) { 9 | super(); 10 | 11 | this.id = payload.fromId; 12 | this.targetedId = payload.toId; 13 | this.amount = payload.amount; 14 | } 15 | 16 | public getTargetedId(): string { 17 | return this.targetedId; 18 | } 19 | 20 | public setTargetedId(value: string) { 21 | this.targetedId = value; 22 | } 23 | 24 | public getAmount(): number { 25 | return this.amount; 26 | } 27 | 28 | public setAmount(value: number) { 29 | this.amount = value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-deposited/consumer/funds-deposited.consumer.ts: -------------------------------------------------------------------------------- 1 | import { FundsDepositedEvent } from '@bank/sdk'; 2 | import { Controller, Inject } from '@nestjs/common'; 3 | import { EventBus } from '@nestjs/cqrs'; 4 | import { MessagePattern, Payload } from '@nestjs/microservices'; 5 | import { plainToClass } from 'class-transformer'; 6 | import { KafkaMessage } from 'kafkajs'; 7 | 8 | @Controller() 9 | export class FundsDepositedConsumer { 10 | @Inject(EventBus) 11 | private readonly eventBus: EventBus; 12 | 13 | @MessagePattern('FundsDepositedEvent') 14 | private fundsDeposited(@Payload() payload: KafkaMessage): void { 15 | const event: FundsDepositedEvent = plainToClass(FundsDepositedEvent, payload); 16 | console.log('on FundsDepositedEvent', { event }); 17 | 18 | this.eventBus.publish(event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/funds/query/src/common/services/typeorm.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; 4 | import { Funds } from '../entity/funds.entity'; 5 | 6 | @Injectable() 7 | export class TypeOrmConfigService implements TypeOrmOptionsFactory { 8 | @Inject(ConfigService) 9 | private readonly config: ConfigService; 10 | 11 | public createTypeOrmOptions(): TypeOrmModuleOptions { 12 | return { 13 | type: 'postgres', 14 | url: this.config.get('DB_URL'), 15 | entities: [Funds], 16 | migrations: ['dist/migrations/*.js'], 17 | migrationsTableName: 'typeorm_migrations', 18 | migrationsRun: true, 19 | synchronize: true, 20 | logging: false, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/account-opened/event/account-opened.handler.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { AccountOpenedEvent, ACCOUNT_OPENED_EVENT_NAME } from '@bank/sdk'; 3 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { Repository } from 'typeorm'; 6 | 7 | @EventsHandler(AccountOpenedEvent) 8 | export class AccountOpenedHandler implements IEventHandler { 9 | @InjectRepository(Account) 10 | private readonly repository: Repository; 11 | 12 | public handle(event: AccountOpenedEvent): Promise { 13 | console.log('AccountOpenedHandler/handle', { event }); 14 | const account = new Account(ACCOUNT_OPENED_EVENT_NAME, event); 15 | 16 | return this.repository.save(account); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /services/account/query/src/common/services/typeorm.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; 4 | import { Account } from '../entity/account.entity'; 5 | 6 | @Injectable() 7 | export class TypeOrmConfigService implements TypeOrmOptionsFactory { 8 | @Inject(ConfigService) 9 | private readonly config: ConfigService; 10 | 11 | public createTypeOrmOptions(): TypeOrmModuleOptions { 12 | return { 13 | type: 'postgres', 14 | url: this.config.get('DB_URL'), 15 | entities: [Account], 16 | migrations: ['dist/migrations/*.js'], 17 | migrationsTableName: 'typeorm_migrations', 18 | migrationsRun: true, 19 | synchronize: true, 20 | logging: false, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-transfered/consumer/funds-transferred.consumer.ts: -------------------------------------------------------------------------------- 1 | import { FundsTransferredEvent } from '@bank/sdk'; 2 | import { Controller, Inject } from '@nestjs/common'; 3 | import { EventBus } from '@nestjs/cqrs'; 4 | import { MessagePattern, Payload } from '@nestjs/microservices'; 5 | import { plainToClass } from 'class-transformer'; 6 | import { KafkaMessage } from 'kafkajs'; 7 | 8 | @Controller() 9 | export class FundsTransferredConsumer { 10 | @Inject(EventBus) 11 | private readonly eventBus: EventBus; 12 | 13 | @MessagePattern('FundsTransferredEvent') 14 | private fundsTransferred(@Payload() payload: KafkaMessage): void { 15 | const event: FundsTransferredEvent = plainToClass(FundsTransferredEvent, payload); 16 | console.log('on FundsTransferredEvent', { event }); 17 | 18 | this.eventBus.publish(event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/account-closed/account-closed.module.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { KAFKA_SERVICE_NAME } from '@bank/sdk'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { AccountConsumer } from './consumer/account-closed.consumer'; 8 | import { AccountClosedHandler } from './event/account-closed.handler'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | ClientsModule.register([{ name: KAFKA_SERVICE_NAME, transport: Transport.KAFKA }]), 14 | TypeOrmModule.forFeature([Account]), 15 | ], 16 | controllers: [AccountConsumer], 17 | providers: [AccountClosedHandler], 18 | }) 19 | export class AccountClosedModule {} 20 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-received/funds-received.module.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { KAFKA_SERVICE_NAME } from '@bank/sdk'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { FundsReceivedConsumer } from './consumer/funds-received.consumer'; 8 | import { FundsReceivedHandler } from './event/funds-received.handler'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | ClientsModule.register([{ name: KAFKA_SERVICE_NAME, transport: Transport.KAFKA }]), 14 | TypeOrmModule.forFeature([Funds]), 15 | ], 16 | controllers: [FundsReceivedConsumer], 17 | providers: [FundsReceivedHandler], 18 | }) 19 | export class FundsReceivedModule {} 20 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/account-opened/consumer/account-opened.consumer.ts: -------------------------------------------------------------------------------- 1 | import { AccountOpenedEvent, ACCOUNT_OPENED_EVENT_NAME } from '@bank/sdk'; 2 | import { Controller, Inject } from '@nestjs/common'; 3 | import { EventBus } from '@nestjs/cqrs'; 4 | import { MessagePattern, Payload } from '@nestjs/microservices'; 5 | import { plainToClass } from 'class-transformer'; 6 | import { KafkaMessage } from 'kafkajs'; 7 | 8 | @Controller() 9 | export class AccountOpenedConsumer { 10 | @Inject(EventBus) 11 | private readonly eventBus: EventBus; 12 | 13 | @MessagePattern(ACCOUNT_OPENED_EVENT_NAME) 14 | private consume(@Payload() payload: KafkaMessage): void { 15 | console.log('AccountOpenedConsumer/consume', { payload }); 16 | const event: AccountOpenedEvent = plainToClass(AccountOpenedEvent, payload); 17 | 18 | this.eventBus.publish(event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/funds/command/src/deposit-funds/controllers/deposit-funds.controller.ts: -------------------------------------------------------------------------------- 1 | import { DepositFundsCommand, DepositFundsDto, DepositFundsResponse, FUNDS_COMMAND_SERVICE_NAME } from '@bank/sdk'; 2 | import { Body, Controller, HttpStatus, Inject } from '@nestjs/common'; 3 | import { CommandBus } from '@nestjs/cqrs'; 4 | import { GrpcMethod } from '@nestjs/microservices'; 5 | 6 | @Controller() 7 | export class DepositFundsController { 8 | @Inject(CommandBus) 9 | private readonly commandBus: CommandBus; 10 | 11 | @GrpcMethod(FUNDS_COMMAND_SERVICE_NAME, 'DepositFunds') 12 | private async depositFunds(@Body() payload: DepositFundsDto): Promise { 13 | const command: DepositFundsCommand = new DepositFundsCommand(payload); 14 | 15 | await this.commandBus.execute(command); 16 | 17 | return { status: HttpStatus.OK, error: null }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/account/command/src/close-account/controllers/close-account.controller.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_COMMAND_SERVICE_NAME, CloseAccountCommand, CloseAccountDto, CloseAccountResponse } from '@bank/sdk'; 2 | import { Body, Controller, HttpStatus, Inject } from '@nestjs/common'; 3 | import { CommandBus } from '@nestjs/cqrs'; 4 | import { GrpcMethod } from '@nestjs/microservices'; 5 | 6 | @Controller() 7 | export class CloseAccountController { 8 | @Inject(CommandBus) 9 | private readonly commandBus: CommandBus; 10 | 11 | @GrpcMethod(ACCOUNT_COMMAND_SERVICE_NAME, 'CloseAccount') 12 | private async openAccount(@Body() payload: CloseAccountDto): Promise { 13 | const command: CloseAccountCommand = new CloseAccountCommand(payload); 14 | 15 | await this.commandBus.execute(command); 16 | 17 | return { status: HttpStatus.OK, error: null }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/account-opened/account-opened.module.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { KAFKA_SERVICE_NAME } from '@bank/sdk'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { AccountOpenedConsumer } from './consumer/account-opened.consumer'; 8 | import { AccountOpenedHandler } from './event/account-opened.handler'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | ClientsModule.register([{ name: KAFKA_SERVICE_NAME, transport: Transport.KAFKA }]), 14 | TypeOrmModule.forFeature([Account]), 15 | ], 16 | controllers: [AccountOpenedConsumer], 17 | providers: [AccountOpenedHandler], 18 | }) 19 | export class AccountOpenedModule {} 20 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-deposited/funds-deposited.module.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { KAFKA_SERVICE_NAME } from '@bank/sdk'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { FundsDepositedConsumer } from './consumer/funds-deposited.consumer'; 8 | import { FundsDepositedHandler } from './event/funds-deposited.handler'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | ClientsModule.register([{ name: KAFKA_SERVICE_NAME, transport: Transport.KAFKA }]), 14 | TypeOrmModule.forFeature([Funds]), 15 | ], 16 | controllers: [FundsDepositedConsumer], 17 | providers: [FundsDepositedHandler], 18 | }) 19 | export class FundsDepositedModule {} 20 | -------------------------------------------------------------------------------- /shared/sdk/src/services/funds/receive/receive-funds.command.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from 'nestjs-event-sourcing'; 2 | import { FundsTransferredEvent } from '../transfer/funds-transferred.event'; 3 | 4 | export class ReceiveFundsCommand extends BaseCommand { 5 | private fromId: string; 6 | private amount: number; 7 | 8 | constructor(payload: FundsTransferredEvent) { 9 | console.log('ReceiveFundsCommand/con'); 10 | super(); 11 | 12 | this.id = payload.targetedId; 13 | this.fromId = payload.id; 14 | this.amount = payload.amount; 15 | } 16 | 17 | public getFromId(): string { 18 | return this.fromId; 19 | } 20 | 21 | public setFromId(value: string) { 22 | this.fromId = value; 23 | } 24 | 25 | public getAmount(): number { 26 | return this.amount; 27 | } 28 | 29 | public setAmount(value: number) { 30 | this.amount = value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gateway/src/funds/query/query.controller.ts: -------------------------------------------------------------------------------- 1 | import { FundsQueryServiceClient, FUNDS_QUERY_SERVICE_NAME, GetBalanceRequest, GetBalanceResponse } from '@bank/sdk'; 2 | import { Body, Controller, Inject, OnModuleInit, Post } from '@nestjs/common'; 3 | import { ClientGrpc } from '@nestjs/microservices'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Controller('funds/query') 7 | export class QueryController implements OnModuleInit { 8 | @Inject(FUNDS_QUERY_SERVICE_NAME) 9 | private readonly client: ClientGrpc; 10 | 11 | private service: FundsQueryServiceClient; 12 | 13 | public onModuleInit(): void { 14 | this.service = this.client.getService(FUNDS_QUERY_SERVICE_NAME); 15 | } 16 | 17 | @Post('balance') 18 | private getBalance(@Body() payload: GetBalanceRequest): Observable { 19 | return this.service.getBalance(payload); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-withdrawn/funds-withdrawn.module.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { KAFKA_SERVICE_NAME } from '@bank/sdk/dist/constants'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { FundsWithdrawnConsumer } from './consumer/funds-withdrawn.consumer'; 8 | import { FundsWithdrawnHandler } from './event/funds-withdrawn.handler'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | ClientsModule.register([{ name: KAFKA_SERVICE_NAME, transport: Transport.KAFKA }]), 14 | TypeOrmModule.forFeature([Funds]), 15 | ], 16 | controllers: [FundsWithdrawnConsumer], 17 | providers: [FundsWithdrawnHandler], 18 | }) 19 | export class FundsWithdrawnModule {} 20 | -------------------------------------------------------------------------------- /services/funds/command/src/transfer-funds/events/funds-transferred.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 2 | import { FundsTransferredEvent } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | 6 | @EventsHandler(FundsTransferredEvent) 7 | export class FundsTransferredHandler implements IEventHandler { 8 | // todo check if AccountEventProducer shouldnt be FundsEventProducer 9 | @Inject(FundsEventProducer) 10 | private readonly eventProducer: FundsEventProducer; 11 | 12 | public handle(event: FundsTransferredEvent): void { 13 | console.log('FundsTransferredHandler', { event }); 14 | const { constructor }: FundsTransferredEvent = Object.getPrototypeOf(event); 15 | 16 | this.eventProducer.produce(constructor.name, event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-transfered/funds-transferred.module.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { KAFKA_SERVICE_NAME } from '@bank/sdk'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { FundsTransferredConsumer } from './consumer/funds-transferred.consumer'; 8 | import { FundsTransferredHandler } from './event/funds-transferred.handler'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | ClientsModule.register([{ name: KAFKA_SERVICE_NAME, transport: Transport.KAFKA }]), 14 | TypeOrmModule.forFeature([Funds]), 15 | ], 16 | controllers: [FundsTransferredConsumer], 17 | providers: [FundsTransferredHandler], 18 | }) 19 | export class FundsTransferredModule {} 20 | -------------------------------------------------------------------------------- /services/account/command/src/close-account/aggregates/close-account.aggregate.ts: -------------------------------------------------------------------------------- 1 | import { AccountAggregate } from '@app/common/aggregates/account.aggregate'; 2 | import { AccountClosedEvent, CloseAccountCommand } from '@bank/sdk'; 3 | import { HttpException, HttpStatus } from '@nestjs/common'; 4 | 5 | export class CloseAccountAggregate extends AccountAggregate { 6 | public closeAccount(command: CloseAccountCommand): void | never { 7 | if (!this.getActive()) { 8 | throw new HttpException('This account is already closed!', HttpStatus.BAD_REQUEST); 9 | } 10 | 11 | const event: AccountClosedEvent = new AccountClosedEvent(command); 12 | // logic 13 | this.apply(event); 14 | } 15 | 16 | public onAccountClosedEvent(event: AccountClosedEvent): void { 17 | console.log('AccountAggregate/onAccountClosedEvent'); 18 | this.id = event.id; 19 | this.setActive(false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.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', 'plugin:prettier/recommended', 'prettier'], 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 | '@typescript-eslint/no-var-requires': 'off', 22 | '@typescript-eslint/no-inferrable-types': 'off', 23 | 'prettier/prettier': ['error'], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /services/account/command/src/open-account/controllers/open-account.controller.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_COMMAND_SERVICE_NAME, OpenAccountCommand, OpenAccountDto, OpenAccountResponse } from '@bank/sdk'; 2 | import { Body, Controller, HttpStatus, Inject } from '@nestjs/common'; 3 | import { CommandBus } from '@nestjs/cqrs'; 4 | import { GrpcMethod } from '@nestjs/microservices'; 5 | 6 | @Controller() 7 | export class OpenAccountController { 8 | @Inject(CommandBus) 9 | private readonly commandBus: CommandBus; 10 | 11 | @GrpcMethod(ACCOUNT_COMMAND_SERVICE_NAME, 'OpenAccount') 12 | private async openAccount(@Body() payload: OpenAccountDto): Promise { 13 | console.log('OpenAccount'); 14 | const command: OpenAccountCommand = new OpenAccountCommand(payload); 15 | 16 | await this.commandBus.execute(command); 17 | 18 | return { status: HttpStatus.OK, data: command.getId(), error: null }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/open-account/account-opened.event.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from 'nestjs-event-sourcing'; 2 | import { AccountType } from '../common/enums/account-type.enum'; 3 | import { OpenAccountCommand } from './open-account.command'; 4 | 5 | export const ACCOUNT_OPENED_EVENT_NAME: string = 'AccountOpenedEvent'; 6 | 7 | export class AccountOpenedEvent extends BaseEvent { 8 | public holder: string; 9 | public email: string; 10 | public type: AccountType; 11 | public openingBalance: number; 12 | public createdDate: Date; 13 | 14 | constructor(command?: OpenAccountCommand) { 15 | super(); 16 | 17 | if (!command) { 18 | return; 19 | } 20 | 21 | this.id = command.getId(); 22 | this.holder = command.getHolder(); 23 | this.email = command.getEmail(); 24 | this.type = command.getType(); 25 | this.openingBalance = command.getOpeningBalance(); 26 | this.createdDate = new Date(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gateway/src/funds/query/query.module.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_QUERY_PACKAGE_NAME, FUNDS_QUERY_SERVICE_NAME } from '@bank/sdk'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ClientsModule, Transport } from '@nestjs/microservices'; 5 | import { QueryController } from './query.controller'; 6 | 7 | @Module({ 8 | imports: [ 9 | ClientsModule.registerAsync([ 10 | { 11 | name: FUNDS_QUERY_SERVICE_NAME, 12 | inject: [ConfigService], 13 | useFactory: (config: ConfigService) => ({ 14 | transport: Transport.GRPC, 15 | options: { 16 | url: config.get('FUNDS_QUERY_GRPC_URL'), 17 | package: FUNDS_QUERY_PACKAGE_NAME, 18 | protoPath: 'node_modules/@bank/sdk/dist/proto/funds-query.proto', 19 | }, 20 | }), 21 | }, 22 | ]), 23 | ], 24 | controllers: [QueryController], 25 | }) 26 | export class QueryModule {} 27 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/account-closed/event/account-closed.handler.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { AccountClosedEvent } from '@bank/sdk'; 3 | import { HttpException, HttpStatus } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @EventsHandler(AccountClosedEvent) 9 | export class AccountClosedHandler implements IEventHandler { 10 | @InjectRepository(Account) 11 | private repository: Repository; 12 | 13 | public async handle({ id }: AccountClosedEvent) { 14 | const account: Account = await this.repository.findOneBy({ id }); 15 | 16 | if (!account) { 17 | throw new HttpException('No account found', HttpStatus.NO_CONTENT); 18 | } 19 | 20 | this.repository.update(account.id, { isActive: false }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/funds/command/src/deposit-funds/deposit-funds.module.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_PROVIDER } from '@app/common/options/grpc-client.option'; 2 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule } from '@nestjs/microservices'; 6 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 7 | import { DepositFundsHandler } from './commands/deposit-funds.handler'; 8 | import { DepositFundsController } from './controllers/deposit-funds.controller'; 9 | import { FundsDepositedHandler } from './events/funds-deposited.handler'; 10 | 11 | @Module({ 12 | imports: [CqrsModule, ClientsModule.registerAsync([ACCOUNT_QUERY_PROVIDER])], 13 | controllers: [DepositFundsController], 14 | providers: [DepositFundsHandler, FundsDepositedHandler, FundsEventProducer, EventSourcingHandler], 15 | }) 16 | export class DepositFundsModule {} 17 | -------------------------------------------------------------------------------- /gateway/src/account/query/query.module.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_PACKAGE_NAME, ACCOUNT_QUERY_SERVICE_NAME } from '@bank/sdk'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ClientsModule, Transport } from '@nestjs/microservices'; 5 | import { QueryController } from './query.controller'; 6 | 7 | @Module({ 8 | imports: [ 9 | ClientsModule.registerAsync([ 10 | { 11 | name: ACCOUNT_QUERY_SERVICE_NAME, 12 | inject: [ConfigService], 13 | useFactory: (config: ConfigService) => ({ 14 | transport: Transport.GRPC, 15 | options: { 16 | url: config.get('ACCOUNT_QUERY_GRPC_URL'), 17 | package: ACCOUNT_QUERY_PACKAGE_NAME, 18 | protoPath: 'node_modules/@bank/sdk/dist/proto/account-query.proto', 19 | }, 20 | }), 21 | }, 22 | ]), 23 | ], 24 | controllers: [QueryController], 25 | }) 26 | export class QueryModule {} 27 | -------------------------------------------------------------------------------- /services/funds/command/src/withdraw-funds/controllers/withdraw-funds.controller.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_COMMAND_SERVICE_NAME, WithdrawFundsCommand, WithdrawFundsDto, WithdrawFundsResponse } from '@bank/sdk'; 2 | import { Body, Controller, HttpStatus, Inject } from '@nestjs/common'; 3 | import { CommandBus } from '@nestjs/cqrs'; 4 | import { GrpcMethod } from '@nestjs/microservices'; 5 | 6 | @Controller() 7 | export class WithdrawFundsController { 8 | @Inject(CommandBus) 9 | private readonly commandBus: CommandBus; 10 | 11 | @GrpcMethod(FUNDS_COMMAND_SERVICE_NAME, 'WithdrawFunds') 12 | private async withdrawFunds(@Body() payload: WithdrawFundsDto): Promise { 13 | console.log(''); 14 | console.log('WithdrawFundsController/withdrawFunds'); 15 | const command: WithdrawFundsCommand = new WithdrawFundsCommand(payload); 16 | 17 | await this.commandBus.execute(command); 18 | 19 | return { status: HttpStatus.OK, error: null }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /services/funds/command/src/withdraw-funds/withdraw-funds.module.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_PROVIDER } from '@app/common/options/grpc-client.option'; 2 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule } from '@nestjs/microservices'; 6 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 7 | import { WithdrawFundsHandler } from './commands/withdraw-funds.handler'; 8 | import { WithdrawFundsController } from './controllers/withdraw-funds.controller'; 9 | import { FundsWithdrawnHandler } from './events/funds-withdrawn.handler'; 10 | 11 | @Module({ 12 | imports: [CqrsModule, ClientsModule.registerAsync([ACCOUNT_QUERY_PROVIDER])], 13 | controllers: [WithdrawFundsController], 14 | providers: [WithdrawFundsHandler, FundsWithdrawnHandler, FundsEventProducer, EventSourcingHandler], 15 | }) 16 | export class WithdrawFundsModule {} 17 | -------------------------------------------------------------------------------- /gateway/src/funds/command/command.module.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_COMMAND_PACKAGE_NAME, FUNDS_COMMAND_SERVICE_NAME } from '@bank/sdk'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ClientsModule, Transport } from '@nestjs/microservices'; 5 | import { CommandController } from './command.controller'; 6 | 7 | @Module({ 8 | imports: [ 9 | ClientsModule.registerAsync([ 10 | { 11 | name: FUNDS_COMMAND_SERVICE_NAME, 12 | inject: [ConfigService], 13 | useFactory: (config: ConfigService) => ({ 14 | transport: Transport.GRPC, 15 | options: { 16 | url: config.get('FUNDS_COMMAND_GRPC_URL'), 17 | package: FUNDS_COMMAND_PACKAGE_NAME, 18 | protoPath: 'node_modules/@bank/sdk/dist/proto/funds-command.proto', 19 | }, 20 | }), 21 | }, 22 | ]), 23 | ], 24 | controllers: [CommandController], 25 | }) 26 | export class CommandModule {} 27 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-received/event/funds-received.handler.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { FundsReceivedEvent } from '@bank/sdk'; 3 | import { HttpException, HttpStatus } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @EventsHandler(FundsReceivedEvent) 9 | export class FundsReceivedHandler implements IEventHandler { 10 | @InjectRepository(Funds) 11 | private readonly repository: Repository; 12 | 13 | public async handle(event: FundsReceivedEvent): Promise { 14 | const funds: Funds = await this.repository.findOneBy({ id: event.id }); 15 | 16 | if (!funds) { 17 | throw new HttpException('No account found', HttpStatus.NO_CONTENT); 18 | } 19 | 20 | this.repository.update(funds.id, { balance: funds.balance + event.amount }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gateway/src/account/command/command.module.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_COMMAND_PACKAGE_NAME, ACCOUNT_COMMAND_SERVICE_NAME } from '@bank/sdk'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ClientsModule, Transport } from '@nestjs/microservices'; 5 | import { CommandController } from './command.controller'; 6 | 7 | @Module({ 8 | imports: [ 9 | ClientsModule.registerAsync([ 10 | { 11 | name: ACCOUNT_COMMAND_SERVICE_NAME, 12 | inject: [ConfigService], 13 | useFactory: (config: ConfigService) => ({ 14 | transport: Transport.GRPC, 15 | options: { 16 | url: config.get('ACCOUNT_COMMAND_GRPC_URL'), 17 | package: ACCOUNT_COMMAND_PACKAGE_NAME, 18 | protoPath: 'node_modules/@bank/sdk/dist/proto/account-command.proto', 19 | }, 20 | }), 21 | }, 22 | ]), 23 | ], 24 | controllers: [CommandController], 25 | }) 26 | export class CommandModule {} 27 | -------------------------------------------------------------------------------- /services/funds/command/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { EventSourcingModule } from 'nestjs-event-sourcing'; 4 | 5 | import { DepositFundsModule } from './deposit-funds/deposit-funds.module'; 6 | import { ReceiveFundsModule } from './receive-funds/receive-funds.module'; 7 | import { TransferFundsModule } from './transfer-funds/transfer-funds.module'; 8 | import { WithdrawFundsModule } from './withdraw-funds/withdraw-funds.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.IS_DOCKER ? '.docker.env' : '.env' }), 13 | EventSourcingModule.forRootAsync({ 14 | inject: [ConfigService], 15 | useFactory: (config: ConfigService) => ({ 16 | mongoUrl: config.get('DB_URL'), 17 | }), 18 | }), 19 | DepositFundsModule, 20 | WithdrawFundsModule, 21 | TransferFundsModule, 22 | ReceiveFundsModule, 23 | ], 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /gateway/src/main.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Logger, VersioningType } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { NestFactory } from '@nestjs/core'; 4 | import { AppModule } from './app.module'; 5 | import { Version } from './common'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | const config: ConfigService = app.get(ConfigService); 10 | const logger: Logger = new Logger(); 11 | const port: number = config.get('PORT'); 12 | 13 | configure(app); 14 | 15 | await app.listen(port, () => { 16 | logger.log(`[NOD] ${process.version}`); 17 | logger.log(`[ENV] ${process.env.NODE_ENV}`); 18 | logger.log(`[DKR] ${process.env.IS_DOCKER ? true : false}`); 19 | logger.log(`[URL] http://localhost:${port}`); 20 | }); 21 | } 22 | 23 | function configure(app: INestApplication): void { 24 | app.setGlobalPrefix('api'); 25 | app.enableVersioning({ 26 | type: VersioningType.URI, 27 | defaultVersion: Version.One, 28 | }); 29 | } 30 | 31 | bootstrap(); 32 | -------------------------------------------------------------------------------- /shared/sdk/src/proto/account-query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package account_query; 4 | 5 | option go_package = "./"; 6 | 7 | service AccountQueryService { 8 | rpc FindAccount(FindAccountRequest) returns (FindAccountResponse) {} 9 | rpc FindAllAccounts(FindAllAccountsRequest) 10 | returns (FindAllAccountsResponse) {} 11 | } 12 | 13 | message AccountData { 14 | string id = 1; 15 | string holder = 2; 16 | bool isActive = 3; 17 | } 18 | 19 | message FindAllAccountsResponseData { 20 | repeated AccountData accounts = 3; 21 | int32 total = 4; 22 | int32 count = 5; 23 | int32 page = 6; 24 | } 25 | 26 | // FindAllAccounts 27 | 28 | message FindAllAccountsRequest { int32 page = 1; } 29 | 30 | message FindAllAccountsResponse { 31 | int32 status = 1; 32 | repeated string error = 2; 33 | FindAllAccountsResponseData data = 3; 34 | } 35 | 36 | // FindAccount 37 | 38 | message FindAccountRequest { string id = 1; } 39 | 40 | message FindAccountResponse { 41 | int32 status = 1; 42 | repeated string error = 2; 43 | AccountData data = 3; 44 | } -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-all-accounts/controller/find-all-accounts.controller.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_SERVICE_NAME, FindAllAccountsResponse, FindAllAccountsResponseData } from '@bank/sdk'; 2 | import { Controller, HttpStatus, Inject } from '@nestjs/common'; 3 | import { QueryBus } from '@nestjs/cqrs'; 4 | import { GrpcMethod, Payload } from '@nestjs/microservices'; 5 | import { FindAllAccountsQuery } from '../query/find-all-accounts.query'; 6 | import { FindAllAccountsDto } from './find-all-accounts.dto'; 7 | 8 | @Controller() 9 | export class FindAllAccountsController { 10 | @Inject(QueryBus) 11 | private readonly queryBus: QueryBus; 12 | 13 | @GrpcMethod(ACCOUNT_QUERY_SERVICE_NAME, 'FindAllAccounts') 14 | private async findAllAccounts(@Payload() payload: FindAllAccountsDto): Promise { 15 | const query: FindAllAccountsQuery = new FindAllAccountsQuery(payload); 16 | const data: FindAllAccountsResponseData = await this.queryBus.execute(query); 17 | 18 | return { data, status: HttpStatus.OK, error: null }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/funds/command/src/transfer-funds/controllers/transfer-funds.controller.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_COMMAND_SERVICE_NAME, TransferFundsCommand, TransferFundsDto, TransferFundsResponse } from '@bank/sdk'; 2 | import { Body, Controller, HttpException, HttpStatus, Inject } from '@nestjs/common'; 3 | import { CommandBus } from '@nestjs/cqrs'; 4 | import { GrpcMethod } from '@nestjs/microservices'; 5 | 6 | @Controller() 7 | export class TransferFundsController { 8 | @Inject(CommandBus) 9 | private readonly commandBus: CommandBus; 10 | 11 | @GrpcMethod(FUNDS_COMMAND_SERVICE_NAME, 'TransferFunds') 12 | private async transferFunds(@Body() payload: TransferFundsDto): Promise { 13 | if (payload.fromId === payload.toId) { 14 | throw new HttpException('Can not transfer money to the same account!', HttpStatus.CONFLICT); 15 | } 16 | 17 | const command: TransferFundsCommand = new TransferFundsCommand(payload); 18 | 19 | await this.commandBus.execute(command); 20 | 21 | return { status: HttpStatus.OK, error: null }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shared/sdk/src/proto/funds-command.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package funds_command; 4 | 5 | option go_package = "./"; 6 | 7 | service FundsCommandService { 8 | rpc DepositFunds(DepositFundsRequest) returns (DepositFundsResponse) {} 9 | rpc WithdrawFunds(WithdrawFundsRequest) returns (WithdrawFundsResponse) {} 10 | rpc TransferFunds(TransferFundsRequest) returns (TransferFundsResponse) {} 11 | } 12 | 13 | // DepositFunds 14 | 15 | message DepositFundsRequest { 16 | string id = 1; 17 | int32 amount = 2; 18 | } 19 | 20 | message DepositFundsResponse { 21 | int32 status = 1; 22 | repeated string error = 2; 23 | } 24 | 25 | // WithdrawFunds 26 | 27 | message WithdrawFundsRequest { 28 | string id = 1; 29 | int32 amount = 2; 30 | } 31 | 32 | message WithdrawFundsResponse { 33 | int32 status = 1; 34 | repeated string error = 2; 35 | } 36 | 37 | // TransferFunds 38 | 39 | message TransferFundsRequest { 40 | string fromId = 1; 41 | string toId = 2; 42 | int32 amount = 3; 43 | } 44 | 45 | message TransferFundsResponse { 46 | int32 status = 1; 47 | repeated string error = 2; 48 | } -------------------------------------------------------------------------------- /services/funds/command/src/common/producer/funds-event.producer.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Kafka, Producer } from 'kafkajs'; 4 | import { BaseEvent } from 'nestjs-event-sourcing'; 5 | 6 | @Injectable() 7 | export class FundsEventProducer implements OnModuleInit, OnModuleDestroy { 8 | private producer: Producer; 9 | 10 | @Inject(ConfigService) 11 | private readonly config: ConfigService; 12 | 13 | public async onModuleInit(): Promise { 14 | const kafka: Kafka = new Kafka({ 15 | clientId: 'BankFunds', 16 | brokers: [this.config.get('KAFKA_URL')], 17 | }); 18 | 19 | this.producer = kafka.producer(); 20 | 21 | await this.producer.connect(); 22 | } 23 | 24 | public onModuleDestroy(): void { 25 | this.producer.disconnect(); 26 | } 27 | 28 | public produce(topic: string, event: T): void { 29 | console.log('FundsEventProducer', topic); 30 | this.producer.send({ topic, messages: [{ value: JSON.stringify(event) }] }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/funds/command/src/transfer-funds/transfer-funds.module.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_PROVIDER } from '@app/common/options/grpc-client.option'; 2 | import { FundsEventProducer } from '@app/common/producer/funds-event.producer'; 3 | import { Module } from '@nestjs/common'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule } from '@nestjs/microservices'; 6 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 7 | import { TransferFundsHandler } from './commands/transfer-funds.handler'; 8 | import { TransferFundsController } from './controllers/transfer-funds.controller'; 9 | import { FundsTransferredHandler } from './events/funds-transferred.handler'; 10 | import { TransferFundsSaga } from './sagas/transfer-funds.saga'; 11 | 12 | @Module({ 13 | imports: [CqrsModule, ClientsModule.registerAsync([ACCOUNT_QUERY_PROVIDER])], 14 | controllers: [TransferFundsController], 15 | providers: [ 16 | TransferFundsHandler, 17 | FundsTransferredHandler, 18 | FundsEventProducer, 19 | EventSourcingHandler, 20 | TransferFundsSaga, 21 | ], 22 | }) 23 | export class TransferFundsModule {} 24 | -------------------------------------------------------------------------------- /services/account/command/src/open-account/commands/open-account.handler.ts: -------------------------------------------------------------------------------- 1 | import { OpenAccountCommand } from '@bank/sdk'; 2 | import { Inject } from '@nestjs/common'; 3 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 4 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 5 | import { OpenAccountAggregate } from '../aggregates/open-account.aggregate'; 6 | 7 | @CommandHandler(OpenAccountCommand) 8 | export class OpenAccountHandler implements ICommandHandler { 9 | @Inject(EventSourcingHandler) 10 | private readonly eventSourcingHandler: EventSourcingHandler; 11 | 12 | @Inject(EventPublisher) 13 | private readonly publisher: EventPublisher; 14 | 15 | public async execute(command: OpenAccountCommand): Promise { 16 | console.log('OpenAccountHandler/execute'); 17 | const aggregate: OpenAccountAggregate = new OpenAccountAggregate(); 18 | 19 | this.publisher.mergeObjectContext(aggregate); 20 | aggregate.openAccount(command); 21 | 22 | await this.eventSourcingHandler.save(aggregate); 23 | 24 | aggregate.commit(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-account/controller/find-account.controller.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { ACCOUNT_QUERY_SERVICE_NAME, FindAccountResponse } from '@bank/sdk'; 3 | import { Controller, HttpException, HttpStatus, Inject } from '@nestjs/common'; 4 | import { QueryBus } from '@nestjs/cqrs'; 5 | import { GrpcMethod, Payload } from '@nestjs/microservices'; 6 | import { FindAccountQuery } from '../query/find-account.query'; 7 | import { FindAccountDto } from './find-account.dto'; 8 | 9 | @Controller() 10 | export class FindAccountController { 11 | @Inject(QueryBus) 12 | private readonly queryBus: QueryBus; 13 | 14 | @GrpcMethod(ACCOUNT_QUERY_SERVICE_NAME, 'FindAccount') 15 | private async findAccount(@Payload() payload: FindAccountDto): Promise { 16 | const query: FindAccountQuery = new FindAccountQuery(payload); 17 | const data: Account = await this.queryBus.execute(query); 18 | 19 | if (!data) { 20 | throw new HttpException('No account found!', HttpStatus.NO_CONTENT); 21 | } 22 | 23 | return { data, status: HttpStatus.OK, error: null }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/account/query/src/common/entity/account.entity.ts: -------------------------------------------------------------------------------- 1 | import { AccountOpenedEvent, AccountType, ACCOUNT_OPENED_EVENT_NAME } from '@bank/sdk'; 2 | import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class Account { 6 | constructor(eventName?: string, payload?: AccountOpenedEvent) { 7 | if (payload && eventName === ACCOUNT_OPENED_EVENT_NAME) { 8 | this.id = payload.id; 9 | this.holder = payload.holder; 10 | this.type = payload.type; 11 | this.email = payload.email; 12 | this.createdDate = payload.createdDate; 13 | 14 | return this; 15 | } 16 | } 17 | 18 | @PrimaryColumn('uuid') 19 | public id: string; 20 | 21 | @Column() 22 | public holder: string; 23 | 24 | @Column() 25 | public email: string; 26 | 27 | @Column({ type: 'enum', enum: AccountType }) 28 | public type: AccountType; 29 | 30 | @Column({ name: 'is_active', default: true }) 31 | public isActive: boolean; 32 | 33 | @CreateDateColumn({ name: 'created_date' }) 34 | public createdDate: Date; 35 | 36 | @UpdateDateColumn({ name: 'updated_date' }) 37 | public updatedDate: Date; 38 | } 39 | -------------------------------------------------------------------------------- /services/funds/command/src/receive-funds/commands/receive-funds.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsAggregate } from '@app/common/aggregates/funds.aggregate'; 2 | import { ReceiveFundsCommand } from '@bank/sdk'; 3 | import { Inject } from '@nestjs/common'; 4 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 5 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 6 | 7 | @CommandHandler(ReceiveFundsCommand) 8 | export class RceiveFundsHandler implements ICommandHandler { 9 | @Inject(EventSourcingHandler) 10 | private readonly eventSourcingHandler: EventSourcingHandler; 11 | 12 | @Inject(EventPublisher) 13 | private readonly publisher: EventPublisher; 14 | 15 | public async execute(command: ReceiveFundsCommand): Promise { 16 | console.log('RceiveFundsHandler excuted GOOD'); 17 | const aggregate: FundsAggregate = await this.eventSourcingHandler.getById(FundsAggregate, command.id); 18 | 19 | this.publisher.mergeObjectContext(aggregate as any); 20 | aggregate.receiveFunds(command); 21 | 22 | await this.eventSourcingHandler.save(aggregate); 23 | 24 | aggregate.commit(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /services/account/query/src/consumer/account-closed/consumer/account-closed.consumer.ts: -------------------------------------------------------------------------------- 1 | import { AccountClosedEvent, KAFKA_SERVICE_NAME } from '@bank/sdk'; 2 | import { Controller, Inject, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common'; 3 | import { EventBus } from '@nestjs/cqrs'; 4 | import { ClientKafka, MessagePattern, Payload } from '@nestjs/microservices'; 5 | import { plainToClass } from 'class-transformer'; 6 | import { KafkaMessage } from 'kafkajs'; 7 | 8 | @Controller() 9 | export class AccountConsumer implements OnApplicationBootstrap, OnApplicationShutdown { 10 | @Inject(KAFKA_SERVICE_NAME) 11 | private readonly client: ClientKafka; 12 | 13 | @Inject(EventBus) 14 | private readonly eventBus: EventBus; 15 | 16 | public onApplicationBootstrap() { 17 | this.client.subscribeToResponseOf('AccountClosedEvent'); 18 | } 19 | 20 | public onApplicationShutdown() { 21 | this.client.close(); 22 | } 23 | 24 | @MessagePattern('AccountClosedEvent') 25 | private consume(@Payload() payload: KafkaMessage): void { 26 | const event: AccountClosedEvent = plainToClass(AccountClosedEvent, payload); 27 | 28 | this.eventBus.publish(event); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /services/account/command/src/close-account/commands/close-account.handler.ts: -------------------------------------------------------------------------------- 1 | import { CloseAccountCommand } from '@bank/sdk'; 2 | import { Inject } from '@nestjs/common'; 3 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 4 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 5 | import { CloseAccountAggregate } from '../aggregates/close-account.aggregate'; 6 | 7 | @CommandHandler(CloseAccountCommand) 8 | export class CloseAccountHandler implements ICommandHandler { 9 | @Inject(EventSourcingHandler) 10 | private readonly eventSourcingHandler: EventSourcingHandler; 11 | 12 | @Inject(EventPublisher) 13 | private readonly publisher: EventPublisher; 14 | 15 | public async execute(command: CloseAccountCommand): Promise { 16 | console.log('CloseAccountHandler/execute'); 17 | const aggregate: CloseAccountAggregate = await this.eventSourcingHandler.getById(CloseAccountAggregate, command.id); 18 | 19 | this.publisher.mergeObjectContext(aggregate); 20 | aggregate.closeAccount(command); 21 | 22 | await this.eventSourcingHandler.save(aggregate); 23 | 24 | aggregate.commit(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tools/nest-mono-new/index.ts: -------------------------------------------------------------------------------- 1 | // ts-node --project ./tools/tsconfig.json tools/nest-mono-new/index.ts funds 2 | import { rmSync } from 'fs'; 3 | import { cmd, editPackageJson, editTSConfigJson, getRootTSConfigPath, removeFiles } from './helper'; 4 | 5 | const NEST_APPS = [`services/${process.argv[2]}/command`, `services/${process.argv[2]}/query`]; 6 | const TAG = '[Tooling/NestNew]'; 7 | const DEPTH = NEST_APPS[0].split('/').length - 1; 8 | 9 | const TS_CONFIG = { 10 | extends: getRootTSConfigPath(DEPTH), 11 | compilerOptions: { 12 | baseUrl: './', 13 | outDir: './dist', 14 | paths: { 15 | '@app/*': ['./src/*'], 16 | }, 17 | }, 18 | }; 19 | 20 | const DEPENDENCIES = { 21 | '@bank/sdk': 'workspace:*', 22 | }; 23 | 24 | NEST_APPS.forEach((appPath) => { 25 | rmSync(appPath, { recursive: true, force: true }); 26 | 27 | if (!appPath) { 28 | throw new Error(`${TAG} No Nest APP Path`); 29 | } 30 | 31 | cmd(`nest new ${appPath} --skip-git --skip-install --package-manager pnpm`).on('close', () => { 32 | editPackageJson(appPath, DEPENDENCIES); 33 | editTSConfigJson(appPath, TS_CONFIG); 34 | removeFiles(appPath); 35 | cmd(`pnpm install`); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /gateway/src/account/command/command.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountCommandServiceClient, 3 | ACCOUNT_COMMAND_SERVICE_NAME, 4 | CloseAccountRequest, 5 | CloseAccountResponse, 6 | OpenAccountRequest, 7 | OpenAccountResponse, 8 | } from '@bank/sdk'; 9 | import { Body, Controller, Inject, OnModuleInit, Post } from '@nestjs/common'; 10 | import { ClientGrpc } from '@nestjs/microservices'; 11 | import { Observable } from 'rxjs'; 12 | 13 | @Controller('account/command') 14 | export class CommandController implements OnModuleInit { 15 | @Inject(ACCOUNT_COMMAND_SERVICE_NAME) 16 | private readonly client: ClientGrpc; 17 | 18 | private service: AccountCommandServiceClient; 19 | 20 | public onModuleInit(): void { 21 | this.service = this.client.getService(ACCOUNT_COMMAND_SERVICE_NAME); 22 | } 23 | 24 | @Post('open') 25 | private openAccount(@Body() payload: OpenAccountRequest): Observable { 26 | return this.service.openAccount(payload); 27 | } 28 | 29 | @Post('close') 30 | private closeAccount(@Body() payload: CloseAccountRequest): Observable { 31 | return this.service.closeAccount(payload); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gateway/src/account/query/query.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountQueryServiceClient, 3 | ACCOUNT_QUERY_SERVICE_NAME, 4 | FindAccountRequest, 5 | FindAccountResponse, 6 | FindAllAccountsRequest, 7 | FindAllAccountsResponse, 8 | } from '@bank/sdk'; 9 | import { Body, Controller, Inject, OnModuleInit, Post } from '@nestjs/common'; 10 | import { ClientGrpc } from '@nestjs/microservices'; 11 | import { Observable } from 'rxjs'; 12 | 13 | @Controller('account/query') 14 | export class QueryController implements OnModuleInit { 15 | @Inject(ACCOUNT_QUERY_SERVICE_NAME) 16 | private readonly client: ClientGrpc; 17 | 18 | private service: AccountQueryServiceClient; 19 | 20 | public onModuleInit(): void { 21 | this.service = this.client.getService(ACCOUNT_QUERY_SERVICE_NAME); 22 | } 23 | 24 | @Post('find-one') 25 | private findAccount(@Body() payload: FindAccountRequest): Observable { 26 | return this.service.findAccount(payload); 27 | } 28 | 29 | @Post('find') 30 | private findAllAccounts(@Body() payload: FindAllAccountsRequest): Observable { 31 | return this.service.findAllAccounts(payload); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/common/entity/account.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 2 | import { AccountOpenedEvent } from '../../../account/open-account'; 3 | import { AccountType } from '../enums'; 4 | 5 | @Entity() 6 | export class Account { 7 | constructor(eventName?: string, payload?: AccountOpenedEvent) { 8 | if (payload && eventName === 'AccountOpenedEvent') { 9 | this.id = payload.id; 10 | this.holder = payload.holder; 11 | this.type = payload.type; 12 | this.email = payload.email; 13 | this.createdDate = payload.createdDate; 14 | 15 | return this; 16 | } 17 | } 18 | 19 | @PrimaryColumn('uuid') 20 | public id: string; 21 | 22 | @Column() 23 | public holder: string; 24 | 25 | @Column() 26 | public email: string; 27 | 28 | @Column({ type: 'enum', enum: AccountType }) 29 | public type: AccountType; 30 | 31 | @Column({ name: 'is_active', default: true }) 32 | public isActive: boolean; 33 | 34 | @CreateDateColumn({ name: 'created_date' }) 35 | public createdDate: Date; 36 | 37 | @UpdateDateColumn({ name: 'updated_date' }) 38 | public updatedDate: Date; 39 | } 40 | -------------------------------------------------------------------------------- /services/account/command/src/common/producer/account-event.producer.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Kafka, Producer } from 'kafkajs'; 4 | import { BaseEvent } from 'nestjs-event-sourcing'; 5 | 6 | @Injectable() 7 | export class AccountEventProducer implements OnModuleInit, OnModuleDestroy { 8 | private producer: Producer; 9 | 10 | @Inject(ConfigService) 11 | private readonly config: ConfigService; 12 | 13 | public async onModuleInit(): Promise { 14 | const kafka: Kafka = new Kafka({ 15 | clientId: 'BankAccount', 16 | brokers: [this.config.get('KAFKA_URL')], 17 | }); 18 | 19 | this.producer = kafka.producer(); 20 | 21 | await this.producer.connect(); 22 | } 23 | 24 | public onModuleDestroy(): void { 25 | this.producer.disconnect(); 26 | } 27 | 28 | public async produce(topic: string, event: T): Promise { 29 | console.log('AccountEventProducer/produce'); 30 | 31 | // goes to query by kafka 32 | await this.producer.send({ 33 | topic, 34 | messages: [{ value: JSON.stringify(event) }], 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/funds/query/src/lookup/get-balance/controller/get-balance.controller.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { FUNDS_QUERY_SERVICE_NAME, GetBalanceResponse } from '@bank/sdk'; 3 | import { Controller, HttpException, HttpStatus, Inject } from '@nestjs/common'; 4 | import { QueryBus } from '@nestjs/cqrs'; 5 | import { GrpcMethod, Payload } from '@nestjs/microservices'; 6 | import { GetBalanceQuery } from '../query/get-balancet.query'; 7 | import { GetBalanceDto } from './get-balance.dto'; 8 | 9 | @Controller() 10 | export class GetBalanceController { 11 | @Inject(QueryBus) 12 | private readonly queryBus: QueryBus; 13 | 14 | @GrpcMethod(FUNDS_QUERY_SERVICE_NAME, 'GetBalance') 15 | private async getBalance(@Payload() payload: GetBalanceDto): Promise { 16 | console.log('GetBalance', { payload }); 17 | const query: GetBalanceQuery = new GetBalanceQuery(payload); 18 | const data: Funds = await this.queryBus.execute(query); 19 | console.log({ data }); 20 | 21 | if (!data) { 22 | throw new HttpException('No account found!', HttpStatus.NO_CONTENT); 23 | } 24 | 25 | return { data: data.balance, status: HttpStatus.OK, error: null }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bank/gateway", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "build": "nest build", 8 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 9 | "start": "nest start", 10 | "start:dev": "nest start --watch", 11 | "start:debug": "nest start --debug --watch", 12 | "start:prod": "node dist/main", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:cov": "jest --coverage", 17 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 18 | "test:e2e": "jest --config ./test/jest-e2e.json" 19 | }, 20 | "dependencies": { 21 | "@bank/sdk": "workspace:*" 22 | }, 23 | "jest": { 24 | "moduleFileExtensions": [ 25 | "js", 26 | "json", 27 | "ts" 28 | ], 29 | "rootDir": "src", 30 | "testRegex": ".*\\.spec\\.ts$", 31 | "transform": { 32 | "^.+\\.(t|j)s$": "ts-jest" 33 | }, 34 | "collectCoverageFrom": [ 35 | "**/*.(t|j)s" 36 | ], 37 | "coverageDirectory": "../coverage", 38 | "testEnvironment": "node" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-withdrawn/event/funds-withdrawn.handler.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { FundsWithdrawnEvent } from '@bank/sdk'; 3 | import { HttpException, HttpStatus } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @EventsHandler(FundsWithdrawnEvent) 9 | export class FundsWithdrawnHandler implements IEventHandler { 10 | @InjectRepository(Funds) 11 | private readonly repository: Repository; 12 | 13 | public async handle(event: FundsWithdrawnEvent): Promise { 14 | if (event.version === 0) { 15 | const funds: Funds = new Funds(); 16 | 17 | funds.id = event.id; 18 | funds.balance = event.amount; 19 | 20 | await this.repository.save(funds); 21 | 22 | return; 23 | } 24 | 25 | const funds: Funds = await this.repository.findOneBy({ id: event.id }); 26 | 27 | if (!funds) { 28 | throw new HttpException('No account found', HttpStatus.NO_CONTENT); 29 | } 30 | 31 | this.repository.update(funds.id, { balance: funds.balance - event.amount }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-transfered/event/funds-transferred.handler.ts: -------------------------------------------------------------------------------- 1 | import { Funds } from '@app/common/entity/funds.entity'; 2 | import { FundsTransferredEvent } from '@bank/sdk'; 3 | import { HttpException, HttpStatus } from '@nestjs/common'; 4 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @EventsHandler(FundsTransferredEvent) 9 | export class FundsTransferredHandler implements IEventHandler { 10 | @InjectRepository(Funds) 11 | private readonly repository: Repository; 12 | 13 | public async handle(event: FundsTransferredEvent): Promise { 14 | if (event.version === 0) { 15 | const funds: Funds = new Funds(); 16 | 17 | funds.id = event.id; 18 | funds.balance = event.amount; 19 | 20 | await this.repository.save(funds); 21 | 22 | return; 23 | } 24 | 25 | const funds: Funds = await this.repository.findOneBy({ id: event.id }); 26 | 27 | if (!funds) { 28 | throw new HttpException('No account found', HttpStatus.NO_CONTENT); 29 | } 30 | 31 | this.repository.update(funds.id, { balance: funds.balance - event.amount }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /services/funds/query/src/consumer/funds-deposited/event/funds-deposited.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsDepositedEvent } from '@bank/sdk'; 2 | import { HttpException, HttpStatus } from '@nestjs/common'; 3 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { Repository } from 'typeorm'; 6 | import { Funds } from '../../../common/entity/funds.entity'; 7 | 8 | @EventsHandler(FundsDepositedEvent) 9 | export class FundsDepositedHandler implements IEventHandler { 10 | @InjectRepository(Funds) 11 | private readonly repository: Repository; 12 | 13 | public async handle(event: FundsDepositedEvent): Promise { 14 | console.log('handle'); 15 | if (event.version === 0) { 16 | const funds: Funds = new Funds(); 17 | 18 | funds.id = event.id; 19 | funds.balance = event.amount; 20 | 21 | await this.repository.save(funds); 22 | 23 | return; 24 | } 25 | 26 | const funds: Funds = await this.repository.findOneBy({ id: event.id }); 27 | 28 | if (!funds) { 29 | throw new HttpException('No account found', HttpStatus.NO_CONTENT); 30 | } 31 | 32 | this.repository.update(funds.id, { balance: funds.balance + event.amount }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /services/account/query/src/lookup/find-all-accounts/query/find-all-accounts.handler.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@app/common/entity/account.entity'; 2 | import { FindAllAccountsResponseData } from '@bank/sdk'; 3 | import { ICommandHandler, QueryHandler } from '@nestjs/cqrs'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { Repository } from 'typeorm'; 6 | import { FindAllAccountsQuery } from './find-all-accounts.query'; 7 | 8 | @QueryHandler(FindAllAccountsQuery) 9 | export class FindAllAccountsQueryHandler implements ICommandHandler { 10 | @InjectRepository(Account) 11 | private readonly repository: Repository; 12 | 13 | public async execute(query: FindAllAccountsQuery): Promise { 14 | const take: number = 15; 15 | const total: number = await this.repository.count(); 16 | const pageLength: number = Math.ceil(total / take) || 1; 17 | const page: number = query.page > pageLength ? 1 : query.page; 18 | const skip: number = page > 1 ? take * (page - 1) : 0; 19 | const accounts: Account[] = await this.repository.find({ 20 | skip, 21 | take, 22 | select: ['id', 'holder', 'isActive'], 23 | }); 24 | 25 | return { accounts, page, total, count: accounts.length }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/funds/query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bank/funds-query", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "build": "nest build", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json" 20 | }, 21 | "dependencies": { 22 | "@bank/sdk": "workspace:*" 23 | }, 24 | "jest": { 25 | "moduleFileExtensions": [ 26 | "js", 27 | "json", 28 | "ts" 29 | ], 30 | "rootDir": "src", 31 | "testRegex": ".*\\.spec\\.ts$", 32 | "transform": { 33 | "^.+\\.(t|j)s$": "ts-jest" 34 | }, 35 | "collectCoverageFrom": [ 36 | "**/*.(t|j)s" 37 | ], 38 | "coverageDirectory": "../coverage", 39 | "testEnvironment": "node" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services/account/query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bank/account-query", 3 | "version": "0.0.1", 4 | "description": "", 5 | "private": true, 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "build": "nest build", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json" 20 | }, 21 | "dependencies": { 22 | "@bank/sdk": "workspace:*" 23 | }, 24 | "jest": { 25 | "moduleFileExtensions": [ 26 | "js", 27 | "json", 28 | "ts" 29 | ], 30 | "rootDir": "src", 31 | "testRegex": ".*\\.spec\\.ts$", 32 | "transform": { 33 | "^.+\\.(t|j)s$": "ts-jest" 34 | }, 35 | "collectCoverageFrom": [ 36 | "**/*.(t|j)s" 37 | ], 38 | "coverageDirectory": "../coverage", 39 | "testEnvironment": "node" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services/funds/command/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bank/funds-command", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "build": "nest build", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json" 20 | }, 21 | "dependencies": { 22 | "@bank/sdk": "workspace:*" 23 | }, 24 | "jest": { 25 | "moduleFileExtensions": [ 26 | "js", 27 | "json", 28 | "ts" 29 | ], 30 | "rootDir": "src", 31 | "testRegex": ".*\\.spec\\.ts$", 32 | "transform": { 33 | "^.+\\.(t|j)s$": "ts-jest" 34 | }, 35 | "collectCoverageFrom": [ 36 | "**/*.(t|j)s" 37 | ], 38 | "coverageDirectory": "../coverage", 39 | "testEnvironment": "node" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services/account/command/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bank/account-command", 3 | "version": "0.0.1", 4 | "description": "", 5 | "private": true, 6 | "license": "UNLICENSED", 7 | "scripts": { 8 | "build": "nest build", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json" 20 | }, 21 | "dependencies": { 22 | "@bank/sdk": "workspace:*" 23 | }, 24 | "jest": { 25 | "moduleFileExtensions": [ 26 | "js", 27 | "json", 28 | "ts" 29 | ], 30 | "rootDir": "src", 31 | "testRegex": ".*\\.spec\\.ts$", 32 | "transform": { 33 | "^.+\\.(t|j)s$": "ts-jest" 34 | }, 35 | "collectCoverageFrom": [ 36 | "**/*.(t|j)s" 37 | ], 38 | "coverageDirectory": "../coverage", 39 | "testEnvironment": "node" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gateway/src/funds/command/command.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DepositFundsRequest, 3 | DepositFundsResponse, 4 | FundsCommandServiceClient, 5 | FUNDS_COMMAND_SERVICE_NAME, 6 | TransferFundsRequest, 7 | TransferFundsResponse, 8 | WithdrawFundsRequest, 9 | WithdrawFundsResponse, 10 | } from '@bank/sdk'; 11 | import { Body, Controller, Inject, OnModuleInit, Post } from '@nestjs/common'; 12 | import { ClientGrpc } from '@nestjs/microservices'; 13 | import { Observable } from 'rxjs'; 14 | 15 | @Controller('funds/command') 16 | export class CommandController implements OnModuleInit { 17 | @Inject(FUNDS_COMMAND_SERVICE_NAME) 18 | private readonly client: ClientGrpc; 19 | 20 | private service: FundsCommandServiceClient; 21 | 22 | public onModuleInit(): void { 23 | this.service = this.client.getService(FUNDS_COMMAND_SERVICE_NAME); 24 | } 25 | 26 | @Post('deposit') 27 | private depositFunds(@Body() payload: DepositFundsRequest): Observable { 28 | return this.service.depositFunds(payload); 29 | } 30 | 31 | @Post('withdraw') 32 | private withdrawFunds(@Body() payload: WithdrawFundsRequest): Observable { 33 | return this.service.withdrawFunds(payload); 34 | } 35 | 36 | @Post('transfer') 37 | private transferFunds(@Body() payload: TransferFundsRequest): Observable { 38 | return this.service.transferFunds(payload); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/sdk/src/services/account/open-account/open-account.command.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { AccountType } from '../common/enums/account-type.enum'; 3 | import { OpenAccountDto } from './open-account.dto'; 4 | 5 | export class OpenAccountCommand { 6 | private readonly id: string; 7 | private holder: string; 8 | private email: string; 9 | private type: AccountType; 10 | private openingBalance: number; 11 | 12 | constructor(payload: OpenAccountDto) { 13 | this.id = uuidv4(); 14 | this.holder = payload.holder; 15 | this.email = payload.email; 16 | this.type = payload.type; 17 | this.openingBalance = payload.openingBalance; 18 | } 19 | 20 | public getId(): string { 21 | return this.id; 22 | } 23 | 24 | public getHolder(): string { 25 | return this.holder; 26 | } 27 | 28 | public setHolder(holder: string): void { 29 | this.holder = holder; 30 | } 31 | 32 | public getEmail(): string { 33 | return this.email; 34 | } 35 | 36 | public setEmail(email: string): void { 37 | this.email = email; 38 | } 39 | 40 | public getType(): AccountType { 41 | return this.type; 42 | } 43 | 44 | public setType(type: AccountType): void { 45 | this.type = type; 46 | } 47 | 48 | public getOpeningBalance(): number { 49 | return this.openingBalance; 50 | } 51 | 52 | public setOpeningBalance(openingBalance: number): void { 53 | this.openingBalance = openingBalance; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /services/account/command/src/open-account/open-account.module.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_COMMAND_PACKAGE_NAME, FUNDS_QUERY_SERVICE_NAME } from '@bank/sdk'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 7 | import { AccountEventProducer } from '../common/producer/account-event.producer'; 8 | import { OpenAccountHandler } from './commands/open-account.handler'; 9 | import { OpenAccountController } from './controllers/open-account.controller'; 10 | import { AccountOpenedHandler } from './events/account-opened.handler'; 11 | import { OpenAccountSaga } from './sagas/open-account.saga'; 12 | 13 | @Module({ 14 | imports: [ 15 | CqrsModule, 16 | ClientsModule.registerAsync([ 17 | { 18 | name: FUNDS_QUERY_SERVICE_NAME, 19 | inject: [ConfigService], 20 | useFactory: (config: ConfigService) => ({ 21 | transport: Transport.GRPC, 22 | options: { 23 | url: config.get('FUNDS_COMMAND_GRPC_URL'), 24 | package: FUNDS_COMMAND_PACKAGE_NAME, 25 | protoPath: 'node_modules/@bank/sdk/dist/proto/funds-command.proto', 26 | }, 27 | }), 28 | }, 29 | ]), 30 | ], 31 | controllers: [OpenAccountController], 32 | providers: [OpenAccountHandler, AccountOpenedHandler, AccountEventProducer, EventSourcingHandler, OpenAccountSaga], 33 | }) 34 | export class OpenAccountModule {} 35 | -------------------------------------------------------------------------------- /shared/sdk/src/pb/funds-query.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices"; 3 | import { Observable } from "rxjs"; 4 | 5 | export interface GetBalanceRequest { 6 | id: string; 7 | } 8 | 9 | export interface GetBalanceResponse { 10 | status: number; 11 | error: string[]; 12 | data: number; 13 | } 14 | 15 | export const FUNDS_QUERY_PACKAGE_NAME = "funds_query"; 16 | 17 | export interface FundsQueryServiceClient { 18 | getBalance(request: GetBalanceRequest): Observable; 19 | } 20 | 21 | export interface FundsQueryServiceController { 22 | getBalance( 23 | request: GetBalanceRequest, 24 | ): Promise | Observable | GetBalanceResponse; 25 | } 26 | 27 | export function FundsQueryServiceControllerMethods() { 28 | return function (constructor: Function) { 29 | const grpcMethods: string[] = ["getBalance"]; 30 | for (const method of grpcMethods) { 31 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 32 | GrpcMethod("FundsQueryService", method)(constructor.prototype[method], method, descriptor); 33 | } 34 | const grpcStreamMethods: string[] = []; 35 | for (const method of grpcStreamMethods) { 36 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 37 | GrpcStreamMethod("FundsQueryService", method)(constructor.prototype[method], method, descriptor); 38 | } 39 | }; 40 | } 41 | 42 | export const FUNDS_QUERY_SERVICE_NAME = "FundsQueryService"; 43 | -------------------------------------------------------------------------------- /services/account/command/src/open-account/sagas/open-account.saga.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountOpenedEvent, 3 | DepositFundsRequest, 4 | FundsCommandServiceClient, 5 | FUNDS_COMMAND_SERVICE_NAME, 6 | FUNDS_QUERY_SERVICE_NAME, 7 | } from '@bank/sdk'; 8 | import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; 9 | import { ICommand, ofType, Saga } from '@nestjs/cqrs'; 10 | import { ClientGrpc } from '@nestjs/microservices'; 11 | import { delay, firstValueFrom, map, Observable } from 'rxjs'; 12 | 13 | @Injectable() 14 | export class OpenAccountSaga implements OnModuleInit { 15 | @Inject(FUNDS_QUERY_SERVICE_NAME) 16 | private readonly client: ClientGrpc; 17 | 18 | private fundsCommandService: FundsCommandServiceClient; 19 | 20 | public onModuleInit() { 21 | this.fundsCommandService = this.client.getService(FUNDS_COMMAND_SERVICE_NAME); 22 | } 23 | 24 | @Saga() 25 | private onEvent(events$: Observable): Observable { 26 | const apply = map((event: AccountOpenedEvent) => { 27 | this.onAcountOpenedEvent(event); 28 | return null; 29 | }); 30 | 31 | return events$.pipe(ofType(AccountOpenedEvent), delay(1000), apply); 32 | } 33 | 34 | private async onAcountOpenedEvent({ id, openingBalance }: AccountOpenedEvent): Promise { 35 | // send to funds to deposit by grpc 36 | console.log('OpenAccountSaga/accountOpened', id); 37 | const req: DepositFundsRequest = { id, amount: openingBalance }; 38 | 39 | await firstValueFrom(this.fundsCommandService.depositFunds(req)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /services/funds/command/src/main.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_COMMAND_PACKAGE_NAME, HttpExceptionFilter } from '@bank/sdk'; 2 | import { INestApplication, Logger, ValidationPipe } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { NestFactory } from '@nestjs/core'; 5 | import { Transport } from '@nestjs/microservices'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap(): Promise { 9 | const app: INestApplication = await NestFactory.create(AppModule); 10 | const config: ConfigService = app.get(ConfigService); 11 | const logger: Logger = new Logger(); 12 | 13 | await configure(app, config); 14 | 15 | app.listen(null, () => { 16 | logger.log(`[NOD] ${process.version}`); 17 | logger.log(`[ENV] ${process.env.NODE_ENV}`); 18 | logger.log(`[DKR] ${process.env.IS_DOCKER ? true : false}`); 19 | logger.log(`[KFK] ${config.get('KAFKA_URL')}`); 20 | logger.log(`[URL] ${config.get('GRPC_URL')}`); 21 | }); 22 | } 23 | 24 | async function configure(app: INestApplication, config: ConfigService): Promise { 25 | app.enableShutdownHooks(); 26 | app.useGlobalFilters(new HttpExceptionFilter()); 27 | app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); 28 | 29 | app.connectMicroservice( 30 | { 31 | transport: Transport.GRPC, 32 | options: { 33 | url: config.get('GRPC_URL'), 34 | package: FUNDS_COMMAND_PACKAGE_NAME, 35 | protoPath: 'node_modules/@bank/sdk/dist/proto/funds-command.proto', 36 | }, 37 | }, 38 | { inheritAppConfig: true }, 39 | ); 40 | 41 | await app.startAllMicroservices(); 42 | } 43 | 44 | bootstrap(); 45 | -------------------------------------------------------------------------------- /tools/nest-mono-new/helper.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, exec } from 'child_process'; 2 | import { readFileSync, unlinkSync, writeFileSync } from 'fs'; 3 | 4 | export function removeFiles(appPath: string): void { 5 | unlinkSync(`${appPath}/.eslintrc.js`); 6 | unlinkSync(`${appPath}/.prettierrc`); 7 | } 8 | 9 | export function editTSConfigJson(appPath: string, tsConfig: any): void { 10 | writeFileSync(`${appPath}/tsconfig.json`, JSON.stringify(tsConfig, null, 2), 'utf8'); 11 | } 12 | 13 | export function editPackageJson(appPath: string, dependencies: any): void { 14 | const packageJsonPath = `${appPath}/package.json`; 15 | const data = readFileSync(packageJsonPath, 'utf8'); 16 | const parsedData = JSON.parse(data); 17 | 18 | parsedData.name = `@bank/${appPath.replace(/\//g, '-').replace('services-', '')}`; 19 | parsedData.version = '1.0.0'; 20 | parsedData.private = true; 21 | parsedData.dependencies = dependencies; 22 | 23 | delete parsedData.author; 24 | delete parsedData.devDependencies; 25 | 26 | writeFileSync(packageJsonPath, JSON.stringify(parsedData, null, 2), 'utf8'); 27 | } 28 | 29 | export function getRootTSConfigPath(depth: number): string { 30 | let prePath = ''; 31 | 32 | for (let index = 0; index < depth + 1; index++) { 33 | prePath += '../'; 34 | } 35 | 36 | return `${prePath}tsconfig.json`; 37 | } 38 | 39 | export function cmd(command: string): ChildProcess { 40 | return exec(command, (error, stdout, stderr) => { 41 | if (error) { 42 | throw new Error(`Exec Error: ${error.message}`); 43 | } else if (stderr) { 44 | throw new Error(`Stderr Error: ${stderr}`); 45 | } 46 | 47 | console.log(stdout); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /services/account/command/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_COMMAND_PACKAGE_NAME, HttpExceptionFilter } from '@bank/sdk'; 2 | import { INestApplication, Logger, ValidationPipe } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { NestFactory } from '@nestjs/core'; 5 | import { Transport } from '@nestjs/microservices'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap(): Promise { 9 | const app: INestApplication = await NestFactory.create(AppModule); 10 | const config: ConfigService = app.get(ConfigService); 11 | const logger: Logger = new Logger(); 12 | 13 | await configure(app, config); 14 | 15 | app.listen(null, () => { 16 | logger.log(`[NOD] ${process.version}`); 17 | logger.log(`[ENV] ${process.env.NODE_ENV}`); 18 | logger.log(`[DKR] ${process.env.IS_DOCKER ? true : false}`); 19 | logger.log(`[KFK] ${config.get('KAFKA_URL')}`); 20 | logger.log(`[MDB] ${config.get('DB_URL')}`); 21 | logger.log(`[URL] ${config.get('GRPC_URL')}`); 22 | }); 23 | } 24 | 25 | async function configure(app: INestApplication, config: ConfigService): Promise { 26 | app.enableShutdownHooks(); 27 | app.useGlobalFilters(new HttpExceptionFilter()); 28 | app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); 29 | 30 | app.connectMicroservice( 31 | { 32 | transport: Transport.GRPC, 33 | options: { 34 | url: config.get('GRPC_URL'), 35 | package: ACCOUNT_COMMAND_PACKAGE_NAME, 36 | protoPath: 'node_modules/@bank/sdk/dist/proto/account-command.proto', 37 | }, 38 | }, 39 | { inheritAppConfig: true }, 40 | ); 41 | 42 | await app.startAllMicroservices(); 43 | } 44 | 45 | bootstrap(); 46 | -------------------------------------------------------------------------------- /services/funds/command/src/deposit-funds/commands/deposit-funds.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsAggregate } from '@app/common/aggregates/funds.aggregate'; 2 | import { 3 | AccountQueryServiceClient, 4 | ACCOUNT_QUERY_SERVICE_NAME, 5 | DepositFundsCommand, 6 | FindAccountResponse, 7 | } from '@bank/sdk'; 8 | import { HttpException, HttpStatus, Inject } from '@nestjs/common'; 9 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 10 | import { ClientGrpc } from '@nestjs/microservices'; 11 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 12 | import { firstValueFrom } from 'rxjs'; 13 | 14 | @CommandHandler(DepositFundsCommand) 15 | export class DepositFundsHandler implements ICommandHandler { 16 | private accountQueryService: AccountQueryServiceClient; 17 | 18 | @Inject(EventSourcingHandler) 19 | private readonly eventSourcingHandler: EventSourcingHandler; 20 | 21 | @Inject(EventPublisher) 22 | private readonly publisher: EventPublisher; 23 | 24 | @Inject(ACCOUNT_QUERY_SERVICE_NAME) 25 | private readonly client: ClientGrpc; 26 | 27 | public onModuleInit() { 28 | this.accountQueryService = this.client.getService(ACCOUNT_QUERY_SERVICE_NAME); 29 | } 30 | 31 | public async execute(command: DepositFundsCommand): Promise { 32 | const res: FindAccountResponse = await firstValueFrom(this.accountQueryService.findAccount({ id: command.id })); 33 | 34 | if (!res || !res.data) { 35 | throw new HttpException('Account not found!', HttpStatus.NOT_FOUND); 36 | } 37 | 38 | const aggregate: FundsAggregate = await this.eventSourcingHandler.getById(FundsAggregate, command.id); 39 | 40 | this.publisher.mergeObjectContext(aggregate as any); 41 | aggregate.depositFunds(command); 42 | 43 | await this.eventSourcingHandler.save(aggregate); 44 | 45 | aggregate.commit(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /services/funds/query/src/main.ts: -------------------------------------------------------------------------------- 1 | import { FUNDS_QUERY_PACKAGE_NAME, HttpExceptionFilter } from '@bank/sdk'; 2 | import { INestApplication, Logger, ValidationPipe } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { NestFactory } from '@nestjs/core'; 5 | import { Transport } from '@nestjs/microservices'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap(): Promise { 9 | const app: INestApplication = await NestFactory.create(AppModule); 10 | const config: ConfigService = app.get(ConfigService); 11 | const logger: Logger = new Logger(); 12 | 13 | await configure(app, config); 14 | 15 | await app.listen(null, () => { 16 | logger.log(`[NOD] ${process.version}`); 17 | logger.log(`[ENV] ${process.env.NODE_ENV}`); 18 | logger.log(`[DKR] ${process.env.IS_DOCKER ? true : false}`); 19 | logger.log(`[KFK] ${config.get('KAFKA_URL')}`); 20 | logger.log(`[URL] ${config.get('GRPC_URL')}`); 21 | }); 22 | } 23 | 24 | async function configure(app: INestApplication, config: ConfigService): Promise { 25 | app.enableShutdownHooks(); 26 | app.useGlobalFilters(new HttpExceptionFilter()); 27 | app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); 28 | 29 | app.connectMicroservice( 30 | { 31 | transport: Transport.GRPC, 32 | options: { 33 | url: config.get('GRPC_URL'), 34 | package: FUNDS_QUERY_PACKAGE_NAME, 35 | protoPath: 'node_modules/@bank/sdk/dist/proto/funds-query.proto', 36 | }, 37 | }, 38 | { inheritAppConfig: true }, 39 | ); 40 | 41 | app.connectMicroservice( 42 | { 43 | transport: Transport.KAFKA, 44 | options: { 45 | client: { 46 | clientId: 'bank-funds-client', 47 | brokers: [config.get('KAFKA_URL')], 48 | }, 49 | consumer: { 50 | groupId: 'bank-funds-svc', 51 | }, 52 | }, 53 | }, 54 | { inheritAppConfig: true }, 55 | ); 56 | 57 | await app.startAllMicroservices(); 58 | } 59 | 60 | bootstrap(); 61 | -------------------------------------------------------------------------------- /services/account/query/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_QUERY_PACKAGE_NAME, HttpExceptionFilter } from '@bank/sdk'; 2 | import { INestApplication, Logger, ValidationPipe } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { NestFactory } from '@nestjs/core'; 5 | import { Transport } from '@nestjs/microservices'; 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap(): Promise { 9 | const app: INestApplication = await NestFactory.create(AppModule); 10 | const config: ConfigService = app.get(ConfigService); 11 | const logger: Logger = new Logger(); 12 | 13 | await configure(app, config); 14 | 15 | await app.listen(null, () => { 16 | logger.log(`[NOD] ${process.version}`); 17 | logger.log(`[ENV] ${process.env.NODE_ENV}`); 18 | logger.log(`[DKR] ${process.env.IS_DOCKER ? true : false}`); 19 | logger.log(`[PGS] ${config.get('PSQL_DB_NAME')}`); 20 | logger.log(`[KFK] ${config.get('KAFKA_URL')}`); 21 | logger.log(`[URL] ${config.get('GRPC_URL')}`); 22 | }); 23 | } 24 | 25 | async function configure(app: INestApplication, config: ConfigService): Promise { 26 | app.enableShutdownHooks(); 27 | app.useGlobalFilters(new HttpExceptionFilter()); 28 | app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); 29 | 30 | app.connectMicroservice( 31 | { 32 | transport: Transport.GRPC, 33 | options: { 34 | url: config.get('GRPC_URL'), 35 | package: ACCOUNT_QUERY_PACKAGE_NAME, 36 | protoPath: 'node_modules/@bank/sdk/dist/proto/account-query.proto', 37 | }, 38 | }, 39 | { inheritAppConfig: true }, 40 | ); 41 | 42 | app.connectMicroservice( 43 | { 44 | transport: Transport.KAFKA, 45 | options: { 46 | client: { 47 | clientId: 'bank-account-client', 48 | brokers: [config.get('KAFKA_URL')], 49 | }, 50 | consumer: { 51 | groupId: 'bank-account-svc', 52 | }, 53 | }, 54 | }, 55 | { inheritAppConfig: true }, 56 | ); 57 | 58 | await app.startAllMicroservices(); 59 | } 60 | 61 | bootstrap(); 62 | -------------------------------------------------------------------------------- /shared/sdk/src/pb/account-command.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices"; 3 | import { Observable } from "rxjs"; 4 | 5 | export interface OpenAccountRequest { 6 | holder: string; 7 | type: string; 8 | email: string; 9 | openingBalance: number; 10 | } 11 | 12 | export interface OpenAccountResponse { 13 | status: number; 14 | error: string[]; 15 | data: string; 16 | } 17 | 18 | export interface CloseAccountRequest { 19 | id: string; 20 | } 21 | 22 | export interface CloseAccountResponse { 23 | status: number; 24 | error: string[]; 25 | } 26 | 27 | export const ACCOUNT_COMMAND_PACKAGE_NAME = "account_command"; 28 | 29 | export interface AccountCommandServiceClient { 30 | openAccount(request: OpenAccountRequest): Observable; 31 | 32 | closeAccount(request: CloseAccountRequest): Observable; 33 | } 34 | 35 | export interface AccountCommandServiceController { 36 | openAccount( 37 | request: OpenAccountRequest, 38 | ): Promise | Observable | OpenAccountResponse; 39 | 40 | closeAccount( 41 | request: CloseAccountRequest, 42 | ): Promise | Observable | CloseAccountResponse; 43 | } 44 | 45 | export function AccountCommandServiceControllerMethods() { 46 | return function (constructor: Function) { 47 | const grpcMethods: string[] = ["openAccount", "closeAccount"]; 48 | for (const method of grpcMethods) { 49 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 50 | GrpcMethod("AccountCommandService", method)(constructor.prototype[method], method, descriptor); 51 | } 52 | const grpcStreamMethods: string[] = []; 53 | for (const method of grpcStreamMethods) { 54 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 55 | GrpcStreamMethod("AccountCommandService", method)(constructor.prototype[method], method, descriptor); 56 | } 57 | }; 58 | } 59 | 60 | export const ACCOUNT_COMMAND_SERVICE_NAME = "AccountCommandService"; 61 | -------------------------------------------------------------------------------- /services/funds/command/src/withdraw-funds/commands/withdraw-funds.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsAggregate } from '@app/common/aggregates/funds.aggregate'; 2 | import { 3 | AccountQueryServiceClient, 4 | ACCOUNT_QUERY_SERVICE_NAME, 5 | FindAccountResponse, 6 | WithdrawFundsCommand, 7 | } from '@bank/sdk'; 8 | import { HttpException, HttpStatus, Inject } from '@nestjs/common'; 9 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 10 | import { ClientGrpc } from '@nestjs/microservices'; 11 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 12 | import { firstValueFrom } from 'rxjs'; 13 | 14 | @CommandHandler(WithdrawFundsCommand) 15 | export class WithdrawFundsHandler implements ICommandHandler { 16 | private accountQueryService: AccountQueryServiceClient; 17 | 18 | @Inject(EventSourcingHandler) 19 | private readonly eventSourcingHandler: EventSourcingHandler; 20 | 21 | @Inject(EventPublisher) 22 | private readonly publisher: EventPublisher; 23 | 24 | @Inject(ACCOUNT_QUERY_SERVICE_NAME) 25 | private readonly client: ClientGrpc; 26 | 27 | public onModuleInit() { 28 | this.accountQueryService = this.client.getService(ACCOUNT_QUERY_SERVICE_NAME); 29 | } 30 | 31 | public async execute(command: WithdrawFundsCommand): Promise { 32 | console.log('WithdrawFundsHandler/execute'); 33 | const res: FindAccountResponse = await firstValueFrom(this.accountQueryService.findAccount({ id: command.id })); 34 | 35 | if (!res || !res.data) { 36 | throw new HttpException('Account not found!', HttpStatus.NOT_FOUND); 37 | } 38 | 39 | const aggregate: FundsAggregate = await this.eventSourcingHandler.getById(FundsAggregate, command.id); 40 | 41 | if (command.getAmount() > aggregate.getBalance()) { 42 | throw new HttpException('Withdraw declined, insufficient funds!', HttpStatus.CONFLICT); 43 | } 44 | 45 | this.publisher.mergeObjectContext(aggregate as any); 46 | aggregate.withdrawFunds(command); 47 | 48 | await this.eventSourcingHandler.save(aggregate); 49 | 50 | aggregate.commit(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /services/funds/command/src/common/aggregates/funds.aggregate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DepositFundsCommand, 3 | FundsDepositedEvent, 4 | FundsReceivedEvent, 5 | FundsTransferredEvent, 6 | FundsWithdrawnEvent, 7 | ReceiveFundsCommand, 8 | TransferFundsCommand, 9 | WithdrawFundsCommand, 10 | } from '@bank/sdk'; 11 | import { ExtendedAggregateRoot } from 'nestjs-event-sourcing'; 12 | 13 | export class FundsAggregate extends ExtendedAggregateRoot { 14 | private balance: number; 15 | 16 | constructor() { 17 | super(); 18 | 19 | this.balance = 0; 20 | } 21 | 22 | public getBalance(): number { 23 | return this.balance; 24 | } 25 | 26 | public setBalance(value: number) { 27 | this.balance = value; 28 | } 29 | 30 | // methods 31 | 32 | public depositFunds(command: DepositFundsCommand): void | never { 33 | const event: FundsDepositedEvent = new FundsDepositedEvent(command); 34 | // logic 35 | this.apply(event); 36 | } 37 | 38 | public onFundsDepositedEvent(event: FundsDepositedEvent): void { 39 | this.id = event.id; 40 | this.setBalance(this.getBalance() + event.amount); 41 | } 42 | 43 | public withdrawFunds(command: WithdrawFundsCommand): void | never { 44 | const event: FundsDepositedEvent = new FundsWithdrawnEvent(command); 45 | // logic 46 | this.apply(event); 47 | } 48 | 49 | public onFundsWithdrawnEvent(event: FundsWithdrawnEvent): void { 50 | this.id = event.id; 51 | this.setBalance(this.getBalance() - event.amount); 52 | } 53 | 54 | public transferFunds(command: TransferFundsCommand): void | never { 55 | const event: FundsTransferredEvent = new FundsTransferredEvent(command); 56 | // logic 57 | this.apply(event); 58 | } 59 | 60 | public onFundsTransferredEvent(event: FundsTransferredEvent): void { 61 | this.id = event.id; 62 | this.setBalance(this.getBalance() - event.amount); 63 | } 64 | 65 | public receiveFunds(command: ReceiveFundsCommand): void | never { 66 | const event: FundsReceivedEvent = new FundsReceivedEvent(command); 67 | // logic 68 | this.apply(event); 69 | } 70 | 71 | public onFundsReceivedEvent(event: FundsReceivedEvent): void { 72 | this.id = event.id; 73 | this.setBalance(this.getBalance() + event.amount); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /shared/sdk/tools/proto-gen.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import * as fg from 'fast-glob'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | const SDK_PATH = './'; 7 | const SRC_PATH = `${SDK_PATH}/src`; 8 | const PROTO_PATH = `${SRC_PATH}/proto`; 9 | const PB_PATH = `${SRC_PATH}/pb`; 10 | const PLUGIN = `--plugin=node_modules/.bin/protoc-gen-ts_proto -I=${PROTO_PATH}`; 11 | const SDK_PROTO = `${PROTO_PATH}/*.proto`; 12 | const NESTJS = '--ts_proto_opt=nestJs=true'; 13 | const SUFFIX = '--ts_proto_opt=fileSuffix=.pb'; 14 | 15 | // protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=shared/sdk/src/proto --ts_proto_out=shared/sdk/src/services/account shared/sdk/src/proto/account-*.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb 16 | 17 | cmd(`protoc ${PLUGIN} --ts_proto_out=${PB_PATH} ${SDK_PROTO} ${NESTJS} ${SUFFIX}`).on('close', () => { 18 | exportProtobufFiles(); 19 | cmd(`prettier --write src/index.ts && tsc --build`).on('close', () => { 20 | cmd('npx copyfiles src/proto/* dist/proto'); 21 | }); 22 | }); 23 | 24 | function cmd(value: string) { 25 | return exec(value, (error, stdout, stderr) => { 26 | if (error) { 27 | throw new Error(`[Exec] ${error.message}`); 28 | } else if (stderr) { 29 | throw new Error(`[Stderr] ${stderr}`); 30 | } 31 | 32 | if (stdout && stdout.length) { 33 | console.log(stdout); 34 | } 35 | }); 36 | } 37 | 38 | function exportProtobufFiles(): void { 39 | const srcDir = `${path.dirname(__dirname)}/src`; 40 | const outDir = `${srcDir}`; 41 | const outFile = `${outDir}/index.ts`; 42 | 43 | removeLineFromFile(outFile, '/pb/'); 44 | 45 | for (const item of fg.sync(`${srcDir}/pb/*.pb.ts`)) { 46 | removeLineFromFile(item, 'export const protobufPackage = "'); 47 | 48 | const filePath = path.relative(outDir, item).replace(/\.ts$/, ''); 49 | 50 | fs.writeFileSync(outFile, `\nexport * from './${filePath}'`, { flag: 'a+' }); 51 | } 52 | } 53 | 54 | function removeLineFromFile(filePath: string, searchKeyword: string): void { 55 | const data = fs.readFileSync(filePath, { encoding: 'utf-8' }); 56 | const lines = data.split('\n'); 57 | const newLines = lines.filter((line) => !line.includes(searchKeyword)); 58 | const updatedData = newLines.join('\n').replace(/\n\n/, '\n'); 59 | 60 | fs.writeFileSync(filePath, updatedData); 61 | } 62 | -------------------------------------------------------------------------------- /services/funds/command/src/transfer-funds/commands/transfer-funds.handler.ts: -------------------------------------------------------------------------------- 1 | import { FundsAggregate } from '@app/common/aggregates/funds.aggregate'; 2 | import { 3 | AccountQueryServiceClient, 4 | ACCOUNT_QUERY_SERVICE_NAME, 5 | FindAccountResponse, 6 | TransferFundsCommand, 7 | } from '@bank/sdk'; 8 | import { HttpException, HttpStatus, Inject } from '@nestjs/common'; 9 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 10 | import { ClientGrpc } from '@nestjs/microservices'; 11 | import { EventSourcingHandler } from 'nestjs-event-sourcing'; 12 | import { firstValueFrom } from 'rxjs'; 13 | 14 | @CommandHandler(TransferFundsCommand) 15 | export class TransferFundsHandler implements ICommandHandler { 16 | private accountQueryService: AccountQueryServiceClient; 17 | 18 | @Inject(EventSourcingHandler) 19 | private readonly eventSourcingHandler: EventSourcingHandler; 20 | 21 | @Inject(EventPublisher) 22 | private readonly publisher: EventPublisher; 23 | 24 | @Inject(ACCOUNT_QUERY_SERVICE_NAME) 25 | private readonly client: ClientGrpc; 26 | 27 | public onModuleInit() { 28 | this.accountQueryService = this.client.getService(ACCOUNT_QUERY_SERVICE_NAME); 29 | } 30 | 31 | public async execute(command: TransferFundsCommand): Promise { 32 | let res: FindAccountResponse = await firstValueFrom(this.accountQueryService.findAccount({ id: command.id })); 33 | 34 | if (!res || !res.data) { 35 | throw new HttpException('Account not found!', HttpStatus.NOT_FOUND); 36 | } 37 | 38 | res = await firstValueFrom(this.accountQueryService.findAccount({ id: command.getTargetedId() })); 39 | 40 | console.log(command.getTargetedId(), { res }); 41 | 42 | if (!res || !res.data) { 43 | throw new HttpException('Targeted account not found!', HttpStatus.NOT_FOUND); 44 | } 45 | 46 | const aggregate: FundsAggregate = await this.eventSourcingHandler.getById(FundsAggregate, command.id); 47 | 48 | if (command.getAmount() > aggregate.getBalance()) { 49 | throw new HttpException('Withdraw declined, insufficient funds!', HttpStatus.CONFLICT); 50 | } 51 | 52 | this.publisher.mergeObjectContext(aggregate as any); 53 | aggregate.transferFunds(command); 54 | 55 | await this.eventSourcingHandler.save(aggregate); 56 | 57 | aggregate.commit(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /shared/sdk/src/pb/account-query.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices"; 3 | import { Observable } from "rxjs"; 4 | 5 | export interface AccountData { 6 | id: string; 7 | holder: string; 8 | isActive: boolean; 9 | } 10 | 11 | export interface FindAllAccountsResponseData { 12 | accounts: AccountData[]; 13 | total: number; 14 | count: number; 15 | page: number; 16 | } 17 | 18 | export interface FindAllAccountsRequest { 19 | page: number; 20 | } 21 | 22 | export interface FindAllAccountsResponse { 23 | status: number; 24 | error: string[]; 25 | data: FindAllAccountsResponseData | undefined; 26 | } 27 | 28 | export interface FindAccountRequest { 29 | id: string; 30 | } 31 | 32 | export interface FindAccountResponse { 33 | status: number; 34 | error: string[]; 35 | data: AccountData | undefined; 36 | } 37 | 38 | export const ACCOUNT_QUERY_PACKAGE_NAME = "account_query"; 39 | 40 | export interface AccountQueryServiceClient { 41 | findAccount(request: FindAccountRequest): Observable; 42 | 43 | findAllAccounts(request: FindAllAccountsRequest): Observable; 44 | } 45 | 46 | export interface AccountQueryServiceController { 47 | findAccount( 48 | request: FindAccountRequest, 49 | ): Promise | Observable | FindAccountResponse; 50 | 51 | findAllAccounts( 52 | request: FindAllAccountsRequest, 53 | ): Promise | Observable | FindAllAccountsResponse; 54 | } 55 | 56 | export function AccountQueryServiceControllerMethods() { 57 | return function (constructor: Function) { 58 | const grpcMethods: string[] = ["findAccount", "findAllAccounts"]; 59 | for (const method of grpcMethods) { 60 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 61 | GrpcMethod("AccountQueryService", method)(constructor.prototype[method], method, descriptor); 62 | } 63 | const grpcStreamMethods: string[] = []; 64 | for (const method of grpcStreamMethods) { 65 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 66 | GrpcStreamMethod("AccountQueryService", method)(constructor.prototype[method], method, descriptor); 67 | } 68 | }; 69 | } 70 | 71 | export const ACCOUNT_QUERY_SERVICE_NAME = "AccountQueryService"; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bank-api-system", 3 | "version": "1.0.0", 4 | "description": "Bank API", 5 | "author": "Kevin Vogel", 6 | "npmClient": "pnpm", 7 | "scripts": { 8 | "build": "pnpm run '/build:*/'", 9 | "build:sdk": "pnpm --filter @bank/sdk build:all", 10 | "gateway:dev": "pnpm --filter @bank/gateway run start:dev", 11 | "account:command:dev": "pnpm --filter @bank/account-command run start:dev", 12 | "account:query:dev": "pnpm --filter @bank/account-query run start:dev", 13 | "funds:command:dev": "pnpm --filter @bank/funds-command run start:dev", 14 | "funds:query:dev": "pnpm --filter @bank/funds-query run start:dev", 15 | "prettier": "prettier --write .", 16 | "graph": "nx graph", 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "keywords": [], 20 | "license": "ISC", 21 | "dependencies": { 22 | "@bank/sdk": "workspace:*", 23 | "@grpc/grpc-js": "^1.8.14", 24 | "@nestjs/common": "^9.4.0", 25 | "@nestjs/config": "^2.3.1", 26 | "@nestjs/core": "^9.4.0", 27 | "@nestjs/cqrs": "^9.0.3", 28 | "@nestjs/microservices": "^9.4.0", 29 | "@nestjs/platform-express": "^9.4.0", 30 | "@nestjs/typeorm": "^9.0.1", 31 | "class-transformer": "^0.5.1", 32 | "class-validator": "^0.14.0", 33 | "kafkajs": "^2.2.4", 34 | "nestjs-event-sourcing": "^1.0.2", 35 | "pg": "^8.10.0", 36 | "reflect-metadata": "^0.1.13", 37 | "rxjs": "^7.8.1", 38 | "typeorm": "^0.3.16", 39 | "uuid": "^9.0.0" 40 | }, 41 | "devDependencies": { 42 | "@grpc/proto-loader": "^0.7.7", 43 | "@nestjs/cli": "^9.4.2", 44 | "@nestjs/schematics": "^9.1.0", 45 | "@nestjs/testing": "^9.4.0", 46 | "@types/express": "^4.17.17", 47 | "@types/jest": "29.5.0", 48 | "@types/node": "18.15.11", 49 | "@types/supertest": "^2.0.12", 50 | "@types/uuid": "^9.0.1", 51 | "@typescript-eslint/eslint-plugin": "^5.59.5", 52 | "@typescript-eslint/parser": "^5.59.5", 53 | "eslint": "^8.40.0", 54 | "eslint-config-prettier": "^8.8.0", 55 | "eslint-plugin-prettier": "^4.2.1", 56 | "fast-glob": "^3.2.12", 57 | "jest": "29.5.0", 58 | "nx": "^16.1.4", 59 | "prettier": "^2.8.8", 60 | "prettier-plugin-organize-imports": "^3.2.2", 61 | "rimraf": "^5.0.0", 62 | "source-map-support": "^0.5.21", 63 | "supertest": "^6.3.3", 64 | "ts-jest": "29.0.5", 65 | "ts-loader": "^9.4.2", 66 | "ts-node": "^10.9.1", 67 | "tsconfig-paths": "4.2.0", 68 | "typescript": "^4.9.5", 69 | "watch": "^1.0.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /shared/sdk/src/pb/funds-command.pb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices"; 3 | import { Observable } from "rxjs"; 4 | 5 | export interface DepositFundsRequest { 6 | id: string; 7 | amount: number; 8 | } 9 | 10 | export interface DepositFundsResponse { 11 | status: number; 12 | error: string[]; 13 | } 14 | 15 | export interface WithdrawFundsRequest { 16 | id: string; 17 | amount: number; 18 | } 19 | 20 | export interface WithdrawFundsResponse { 21 | status: number; 22 | error: string[]; 23 | } 24 | 25 | export interface TransferFundsRequest { 26 | fromId: string; 27 | toId: string; 28 | amount: number; 29 | } 30 | 31 | export interface TransferFundsResponse { 32 | status: number; 33 | error: string[]; 34 | } 35 | 36 | export const FUNDS_COMMAND_PACKAGE_NAME = "funds_command"; 37 | 38 | export interface FundsCommandServiceClient { 39 | depositFunds(request: DepositFundsRequest): Observable; 40 | 41 | withdrawFunds(request: WithdrawFundsRequest): Observable; 42 | 43 | transferFunds(request: TransferFundsRequest): Observable; 44 | } 45 | 46 | export interface FundsCommandServiceController { 47 | depositFunds( 48 | request: DepositFundsRequest, 49 | ): Promise | Observable | DepositFundsResponse; 50 | 51 | withdrawFunds( 52 | request: WithdrawFundsRequest, 53 | ): Promise | Observable | WithdrawFundsResponse; 54 | 55 | transferFunds( 56 | request: TransferFundsRequest, 57 | ): Promise | Observable | TransferFundsResponse; 58 | } 59 | 60 | export function FundsCommandServiceControllerMethods() { 61 | return function (constructor: Function) { 62 | const grpcMethods: string[] = ["depositFunds", "withdrawFunds", "transferFunds"]; 63 | for (const method of grpcMethods) { 64 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 65 | GrpcMethod("FundsCommandService", method)(constructor.prototype[method], method, descriptor); 66 | } 67 | const grpcStreamMethods: string[] = []; 68 | for (const method of grpcStreamMethods) { 69 | const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method); 70 | GrpcStreamMethod("FundsCommandService", method)(constructor.prototype[method], method, descriptor); 71 | } 72 | }; 73 | } 74 | 75 | export const FUNDS_COMMAND_SERVICE_NAME = "FundsCommandService"; 76 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | mongodb: 5 | container_name: mongodb 6 | image: mongo:5.0 7 | environment: 8 | - MONGO_INITDB_DATABASE=${MONGODB_DB_NAME} 9 | ports: 10 | - 27017:27017 11 | volumes: 12 | - mongodb_data:/data/db 13 | networks: 14 | - bank_api_network 15 | 16 | postgres: 17 | container_name: postgres 18 | image: postgres:14.2 19 | environment: 20 | POSTGRES_USER: ${PSQL_DB_USER} 21 | POSTGRES_PASSWORD: ${PSQL_DB_PASS} 22 | POSTGRES_DB: ${PSQL_DB_NAME} 23 | ports: 24 | - 5432:5432 25 | volumes: 26 | - postgres_data:/var/lib/postgresql/data 27 | - ./tools/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql 28 | networks: 29 | - bank_api_network 30 | healthcheck: 31 | test: ['CMD-SHELL', 'pg_isready -U ${PSQL_DB_USER} -d ${PSQL_DB_NAME}'] 32 | interval: 5s 33 | timeout: 10s 34 | retries: 5 35 | 36 | zookeeper: 37 | image: antrea/confluentinc-zookeeper:6.2.0 38 | hostname: zookeeper 39 | container_name: zookeeper 40 | ports: 41 | - '2181:2181' 42 | environment: 43 | ZOOKEEPER_CLIENT_PORT: 2181 44 | ZOOKEEPER_TICK_TIME: 2000 45 | 46 | broker: 47 | image: antrea/confluentinc-kafka:6.2.0 48 | hostname: broker 49 | container_name: broker 50 | depends_on: 51 | - zookeeper 52 | ports: 53 | - '9092:9092' 54 | - '9101:9101' 55 | environment: 56 | KAFKA_BROKER_ID: 1 57 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 58 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 59 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 60 | # NOTE: Not supported by current container 61 | # KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter 62 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 63 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 64 | KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 65 | KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 66 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 67 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 68 | KAFKA_JMX_PORT: 9101 69 | KAFKA_JMX_HOSTNAME: localhost 70 | # TODO: Uncomment once enable schema registry 71 | # KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 72 | CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 73 | CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 74 | CONFLUENT_METRICS_ENABLE: 'true' 75 | CONFLUENT_SUPPORT_CUSTOMER_ID: 'anonymous' 76 | 77 | volumes: 78 | zookeeper_data: 79 | driver: local 80 | kafka_data: 81 | driver: local 82 | postgres_data: 83 | driver: local 84 | mongodb_data: 85 | driver: local 86 | 87 | networks: 88 | bank_api_network: 89 | name: bank_api_network 90 | driver: bridge 91 | -------------------------------------------------------------------------------- /services/funds/query/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /services/account/command/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /services/account/query/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /services/funds/command/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | --------------------------------------------------------------------------------