diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..259de13 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/.gitignore b/.gitignore index 939a4d3..f0b6393 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,59 @@ -.terraform/ -.shelltool/ -makefiles/ -passwd - -.npm/ -.esbuild/ -.serverlessrc -.npmrc -.config/ -.yarn/ -.cache/ -node_modules -**/node_modules -.dccache +# compiled output +/dist +/node_modules +/.build +/.serverless + +package-lock.json + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files .env -coverage/ -.vscode +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock -dist/ \ No newline at end of file +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1bfeba0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "editor.formatOnSave": true, + "tabWidth": 2, + "useTabs": false, + "indent_style": "space", + "indent_size": 2 +} \ No newline at end of file diff --git a/.serverless/cloudformation-template-create-stack.json b/.serverless/cloudformation-template-create-stack.json new file mode 100644 index 0000000..85ca881 --- /dev/null +++ b/.serverless/cloudformation-template-create-stack.json @@ -0,0 +1,82 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application", + "Resources": { + "ServerlessDeploymentBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + } + } + }, + "ServerlessDeploymentBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Effect": "Deny", + "Principal": "*", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "ServerlessDeploymentBucket" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "ServerlessDeploymentBucket" + } + ] + ] + } + ], + "Condition": { + "Bool": { + "aws:SecureTransport": false + } + } + } + ] + } + } + } + }, + "Outputs": { + "ServerlessDeploymentBucketName": { + "Value": { + "Ref": "ServerlessDeploymentBucket" + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index f1e1b05..d4b4b72 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,26 @@ # Reto técnico iO - Backend +## Pre-requisitos para la instalacion y despliegue +- nodejs v20 +- Tener el Access Key ID y Secret Access Key de su cuenta de AWS + +## Instalacion +- npm install +- configure aws + +## Ejecución en local +- npm run start:dev + +## Ejecución Unit Test +- npm run test + +## Despliegue en AWS +- comentar/quitar la funcion activities del archivo config/functions.yaml para el primer despliegue, esto por un problema con el stream de dynamo. Al no existir, no se puede referenciar +- npm run sls-deploy +- descomentar o agregar las funcion activities que se indico en el primer paso, para volver a desplegar +- npm run sls-deploy + + ## Descripción: Se requiere implementar un proyecto serverless de registro de pagos y consulta de transacciones. A continuación se muestran los diagramas correspondientes: diff --git a/config/common-custom-config.yaml b/config/common-custom-config.yaml new file mode 100644 index 0000000..e1d982b --- /dev/null +++ b/config/common-custom-config.yaml @@ -0,0 +1,3 @@ +logRetentionInDays: + DESA: 7 + PROD: 30 diff --git a/config/dynamodb-seed.yaml b/config/dynamodb-seed.yaml new file mode 100644 index 0000000..57e609c --- /dev/null +++ b/config/dynamodb-seed.yaml @@ -0,0 +1,5 @@ +seedUsersTable: + table: ${self:provider.environment.DYNAMODB_TABLE_USERS} + sources: + - config/seeders/${self:provider.stage}/users/seed-user-1.json + - config/seeders/${self:provider.stage}/users/seed-user-2.json \ No newline at end of file diff --git a/config/dynamodb-tables.yaml b/config/dynamodb-tables.yaml new file mode 100644 index 0000000..26c134b --- /dev/null +++ b/config/dynamodb-tables.yaml @@ -0,0 +1,37 @@ +usersTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: ${self:provider.environment.DYNAMODB_TABLE_USERS} + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: userId + AttributeType: S + KeySchema: + - AttributeName: userId + KeyType: HASH + +transactionsTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: ${self:provider.environment.DYNAMODB_TABLE_TRANSACTIONS} + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: transactionId + AttributeType: S + KeySchema: + - AttributeName: transactionId + KeyType: HASH + StreamSpecification: + StreamViewType: NEW_IMAGE + +ActivityTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: ${self:provider.environment.DYNAMODB_TABLE_ACTIVITY} + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: activityId + AttributeType: S + KeySchema: + - AttributeName: activityId + KeyType: HASH \ No newline at end of file diff --git a/config/environments.yaml b/config/environments.yaml new file mode 100644 index 0000000..ffdc6ac --- /dev/null +++ b/config/environments.yaml @@ -0,0 +1,3 @@ +DYNAMODB_TABLE_USERS: TABLE_${self:provider.stage}_USERS +DYNAMODB_TABLE_TRANSACTIONS: TABLE_${self:provider.stage}_TRANSACTIONS +DYNAMODB_TABLE_ACTIVITY: TABLE_${self:provider.stage}_ACTIVITY \ No newline at end of file diff --git a/config/functions.yaml b/config/functions.yaml new file mode 100644 index 0000000..e18d889 --- /dev/null +++ b/config/functions.yaml @@ -0,0 +1,23 @@ +payments: + handler: src/Payments/Infrastructure/AppLambda.handler + name: LMB_${self:provider.stage}_PAYMENTS +transactions: + handler: src/Transactions/Infrastructure/AppLambda.handler + name: LMB_${self:provider.stage}_TRANSACTIONS + events: + - http: + method: GET + path: 'V1/transactions' +# Comentar la siguiente function 'activities' en el primer despliegue +# luego descomentar y volver a desplegar +# por un problema del stream que no existe inicialmente (no se puede referenciar) +# pendiente de revisar y mejorar +activities: + handler: src/Activities/Infrastructure/AppLambda.handler + name: LMB_${self:provider.stage}_ACTIVITIES + events: + - stream: + type: dynamodb + batchSize: 1 + startingPosition: LATEST + arn: ${fetchStreamARN(${self:provider.environment.DYNAMODB_TABLE_TRANSACTIONS})} diff --git a/config/http-response.yaml b/config/http-response.yaml new file mode 100644 index 0000000..b436a52 --- /dev/null +++ b/config/http-response.yaml @@ -0,0 +1,3 @@ +headers: + Content-Type: "'application/json'" +template: ${file(config/response.vm)} \ No newline at end of file diff --git a/config/iam-role-statements.yaml b/config/iam-role-statements.yaml new file mode 100644 index 0000000..bd9b08b --- /dev/null +++ b/config/iam-role-statements.yaml @@ -0,0 +1,9 @@ +- Effect: Allow + Action: + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:GetRecords + - dynamodb:GetShardIterator + - dynamodb:DescribeStream + - dynamodb:ListStreams + Resource: '*' \ No newline at end of file diff --git a/config/request/request-stepfunction-payments.vm b/config/request/request-stepfunction-payments.vm new file mode 100644 index 0000000..6275674 --- /dev/null +++ b/config/request/request-stepfunction-payments.vm @@ -0,0 +1,5 @@ +#set($input = $input.json('$')) +{ + "input": "$util.escapeJavaScript($input).replaceAll("\\'", "'")", + "stateMachineArn": "arn:aws:states:${self:provider.region}:$context.accountId:stateMachine:SF_PAYMENT_WORKFLOW" +} \ No newline at end of file diff --git a/config/resources.yaml b/config/resources.yaml new file mode 100644 index 0000000..2ce91d5 --- /dev/null +++ b/config/resources.yaml @@ -0,0 +1,8 @@ +Resources: + DynamoDBUsersTable: ${file(config/dynamodb-tables.yaml):usersTable} + DynamoDBTransactionsTable: ${file(config/dynamodb-tables.yaml):transactionsTable} + DynamoDBActivityTable: ${file(config/dynamodb-tables.yaml):ActivityTable} + StepFuncLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: /aws/stepfunctions/${self:service}-${self:provider.stage} \ No newline at end of file diff --git a/config/response.vm b/config/response.vm new file mode 100644 index 0000000..2edfc77 --- /dev/null +++ b/config/response.vm @@ -0,0 +1 @@ +$input.json("$") \ No newline at end of file diff --git a/config/response/response-stepfunction-payments.vm b/config/response/response-stepfunction-payments.vm new file mode 100644 index 0000000..d8545a6 --- /dev/null +++ b/config/response/response-stepfunction-payments.vm @@ -0,0 +1,11 @@ +#set( $output = $util.parseJson($input.json('$.output')) ) +#if( "$output" == "" ) +{ + "message": "Something was wrong" +} +#else +{ + "message": "Payment registered successfully", + "transactionId": $output +} +#end \ No newline at end of file diff --git a/config/seeders/DESA/users/seed-user-1.json b/config/seeders/DESA/users/seed-user-1.json new file mode 100644 index 0000000..f634aaf --- /dev/null +++ b/config/seeders/DESA/users/seed-user-1.json @@ -0,0 +1,5 @@ +{ + "userId": "f529177d-0521-414e-acd9-6ac840549e97", + "name": "Pedro", + "lastName": "Suarez" +} \ No newline at end of file diff --git a/config/seeders/DESA/users/seed-user-2.json b/config/seeders/DESA/users/seed-user-2.json new file mode 100644 index 0000000..d7343d9 --- /dev/null +++ b/config/seeders/DESA/users/seed-user-2.json @@ -0,0 +1,5 @@ +{ + "userId": "15f1c60a-2833-49b7-8660-065b58be2f89", + "name": "Andrea", + "lastName": "Vargas" +} \ No newline at end of file diff --git a/config/stepfunctions.yaml b/config/stepfunctions.yaml new file mode 100644 index 0000000..619ebe4 --- /dev/null +++ b/config/stepfunctions.yaml @@ -0,0 +1,4 @@ +stateMachines: + PaymentWorkFlow: ${file(config/stepfunctions/sf-paymentworkflow.yaml)} + +validate: true \ No newline at end of file diff --git a/config/stepfunctions/sf-paymentworkflow.yaml b/config/stepfunctions/sf-paymentworkflow.yaml new file mode 100644 index 0000000..7699965 --- /dev/null +++ b/config/stepfunctions/sf-paymentworkflow.yaml @@ -0,0 +1,92 @@ +name: SF_PAYMENT_WORKFLOW +type: EXPRESS +events: + - http: + method: POST + path: 'V1/payments' + action: StartSyncExecution + request: + template: + application/json: ${file(config/request/request-stepfunction-payments.vm)} + response: + template: + application/json: ${file(config/response/response-stepfunction-payments.vm)} +loggingConfig: + level: ALL + includeExecutionData: true + destinations: + - Fn::GetAtt: [StepFuncLogGroup, Arn] + +definition: + Comment: WorkFlow Payments + StartAt: ValidateUser + States: + ValidateUser: + Type: Task + Resource: arn:aws:states:::dynamodb:getItem + InputPath: "$" + Parameters: + TableName: ${self:provider.environment.DYNAMODB_TABLE_USERS} + Key: + userId: + S.$: "$.input.userId" + ResultPath: "$.ValidateUserOutput" + Next: UserValidationSuccessful + UserValidationSuccessful: + Type: Choice + Choices: + - And: + - Variable: "$.ValidateUserOutput.Item" + IsPresent: true + - Variable: "$.ValidateUserOutput.Item.userId.S" + IsNull: false + Next: ExecutePayment + Default: Fail + ExecutePayment: + Type: Task + Resource: !GetAtt payments.Arn + InputPath: "$" + Parameters: + body: + action: create + userId.$: "$.input.userId" + amount.$: "$.input.amount" + Retry: + - ErrorEquals: + - States.ALL + IntervalSeconds: 2 + MaxAttempts: 2 + BackoffRate: 1 + ResultPath: "$.ExecutePaymentOutput" + Next: PaymentSuccessful + PaymentSuccessful: + Type: Choice + Choices: + - And: + - Variable: "$.ExecutePaymentOutput.response.status" + IsPresent: true + - Variable: "$.ExecutePaymentOutput.response.status" + StringEquals: OK + Next: SaveTransactionPayment + Default: Fail + SaveTransactionPayment: + Type: Task + Resource: arn:aws:states:::dynamodb:putItem + InputPath: "$" + Parameters: + TableName: ${self:provider.environment.DYNAMODB_TABLE_TRANSACTIONS} + Item: + transactionId: + S.$: $.ExecutePaymentOutput.response.transactionId + userId: + S.$: "$.input.userId" + amount: + N.$: States.Format('{}', $.input.amount) + ResultPath: "$.SaveTransactionPaymentOutput" + Next: Success + Success: + Type: Succeed + InputPath: "$" + OutputPath: "$.ExecutePaymentOutput.response.transactionId" + Fail: + Type: Fail diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..9f526a5 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,26 @@ +module.exports = { + verbose: true, + moduleFileExtensions: [ + "js", + "json", + "ts" + ], + rootDir: "test", + transform: { + "^.+\\.(t|j)s$": "ts-jest" + }, + collectCoverageFrom: [ + "**/*.{js,ts,tsx}", + "!**/node_modules/**" + ], + coveragePathIgnorePatterns: [ + "/node_modules/", + "/test/" + ], + testMatch: [ + '**/*.steps.ts' + ], + coverageDirectory: "../coverage", + testEnvironment: "node", + collectCoverage: true +} \ No newline at end of file diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e0bd21f --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "io-node-challenge", + "version": "0.0.1", + "description": "reto tecnico BCP", + "author": "Ericson Luis Quispe Abad", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "sls offline", + "start:dev": "sls offline --stage DESA", + "sls-deploy": "serverless deploy --stage DESA --region us-east-2", + "sls-seed": "serverless dynamodb:seed --stage DESA --region us-east-2", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@codegenie/serverless-express": "^4.14.1", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "aws-lambda": "^1.0.7", + "aws-sdk": "^2.1659.0", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@automock/adapters.nestjs": "^2.1.0", + "@automock/jest": "^2.1.0", + "@nestjs/cli": "^10.0.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.3.1", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.7.0", + "jest-cucumber": "^4.4.0", + "prettier": "^3.0.0", + "serverless": "^3.39.0", + "serverless-dynamodb-seed": "^0.3.0", + "serverless-dynamodb-stream-arn-plugin": "^0.0.7", + "serverless-offline": "^13.6.0", + "serverless-plugin-reducer": "^4.0.1", + "serverless-plugin-typescript": "^2.1.5", + "serverless-step-functions": "^3.21.0", + "ts-jest": "^29.2.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } +} diff --git a/serverless.yaml b/serverless.yaml new file mode 100644 index 0000000..d0329e1 --- /dev/null +++ b/serverless.yaml @@ -0,0 +1,30 @@ +service: io-node-challenge +provider: + name: aws + runtime: nodejs20.x + stage: ${opt:stage,'DESA'} + region: us-east-2 + timeout: 15 + logRetentionInDays: ${self:custom.logRetentionInDays.${self:provider.stage}} + environment: ${file(config/environments.yaml)} + iamRoleStatements: ${file(config/iam-role-statements.yaml)} +custom: + reducer: + ignoreMissing: true + seed: + seedUsersTable: ${file(config/dynamodb-seed.yaml):seedUsersTable} + logRetentionInDays: ${file(./config/common-custom-config.yaml):logRetentionInDays} +plugins: + - serverless-plugin-typescript + - serverless-offline + - serverless-plugin-reducer + - serverless-dynamodb-seed + - serverless-step-functions + - serverless-dynamodb-stream-arn-plugin +package: + individually: true + exclude: + - npm-cache/** +functions: ${file(config/functions.yaml)} +resources: ${file(config/resources.yaml)} +stepFunctions: ${file(config/stepfunctions.yaml)} \ No newline at end of file diff --git a/src/Activities/Application/DTOs/ActivityRequestDTO.ts b/src/Activities/Application/DTOs/ActivityRequestDTO.ts new file mode 100644 index 0000000..d71cddb --- /dev/null +++ b/src/Activities/Application/DTOs/ActivityRequestDTO.ts @@ -0,0 +1,5 @@ +export interface ActivityRequestDTO { + activityId?: string; + transactionId: string; + date?: string; +} \ No newline at end of file diff --git a/src/Activities/Application/DTOs/ActivityResponseDTO.ts b/src/Activities/Application/DTOs/ActivityResponseDTO.ts new file mode 100644 index 0000000..485ff1b --- /dev/null +++ b/src/Activities/Application/DTOs/ActivityResponseDTO.ts @@ -0,0 +1,4 @@ +export interface ActivityResponseDTO { + status: string; + message: string; +} \ No newline at end of file diff --git a/src/Activities/Application/UseCases/ActivityUseCase.ts b/src/Activities/Application/UseCases/ActivityUseCase.ts new file mode 100644 index 0000000..4d59fda --- /dev/null +++ b/src/Activities/Application/UseCases/ActivityUseCase.ts @@ -0,0 +1,31 @@ +import { Injectable } from "@nestjs/common"; +import { Activity } from "../../Domain/Entities/Activity"; +import { ActivityService } from "../../Domain/Services/ActivityService"; +import { ActivityRequestDTO } from "../DTOs/ActivityRequestDTO"; +import { ActivityResponseDTO } from "../DTOs/ActivityResponseDTO"; +import { RESPONSE_STATUS } from "../../../Commons/Constants"; + +@Injectable() +export class ActivityUseCase { + private readonly ActivityService: ActivityService; + + constructor(activityService: ActivityService) { + this.ActivityService = activityService; + } + + async createActivity(activityRequest: ActivityRequestDTO): Promise { + console.log('-- ActivityUseCase.createActivity --'); + const activity = activityRequest as Activity + const activityCreate = await this.ActivityService.createActivity(activity); + if (activityCreate?.activityId) { + return { + status: RESPONSE_STATUS.OK, + message: 'Activity created successfully', + } + } + return { + status: RESPONSE_STATUS.ERROR, + message: 'Error creating activity', + } + } +} \ No newline at end of file diff --git a/src/Activities/Domain/Entities/Activity.ts b/src/Activities/Domain/Entities/Activity.ts new file mode 100644 index 0000000..0f7c256 --- /dev/null +++ b/src/Activities/Domain/Entities/Activity.ts @@ -0,0 +1,5 @@ +export interface Activity { + activityId?: string; + transactionId: string; + date?: string; +} \ No newline at end of file diff --git a/src/Activities/Domain/Ports/ActivityRepository.ts b/src/Activities/Domain/Ports/ActivityRepository.ts new file mode 100644 index 0000000..3231355 --- /dev/null +++ b/src/Activities/Domain/Ports/ActivityRepository.ts @@ -0,0 +1,7 @@ +import { Activity } from "../Entities/Activity"; + +export interface ActivityRepository { + + createActivity(activity: Activity): Promise; + +} \ No newline at end of file diff --git a/src/Activities/Domain/Services/ActivityService.ts b/src/Activities/Domain/Services/ActivityService.ts new file mode 100644 index 0000000..005025f --- /dev/null +++ b/src/Activities/Domain/Services/ActivityService.ts @@ -0,0 +1,24 @@ +import { Inject } from "@nestjs/common"; +import { v4 as uuidv4 } from 'uuid'; +import { Activity } from "../Entities/Activity"; +import { ActivityRepository } from "../Ports/ActivityRepository"; + +export class ActivityService { + + private readonly activityRepository: ActivityRepository; + + constructor( + @Inject('ActivityRepository') + activityRepository: ActivityRepository + ) { + this.activityRepository = activityRepository; + } + + async createActivity(activity: Activity): Promise { + console.log('-- ActivityService.createActivity --'); + activity.activityId = uuidv4(); + activity.date = new Date().toISOString() + return this.activityRepository.createActivity(activity); + } + +} \ No newline at end of file diff --git a/src/Activities/Infrastructure/ActivityController.ts b/src/Activities/Infrastructure/ActivityController.ts new file mode 100644 index 0000000..b76ed1f --- /dev/null +++ b/src/Activities/Infrastructure/ActivityController.ts @@ -0,0 +1,28 @@ +import { Controller } from '@nestjs/common'; +import { ActivityUseCase } from '../Application/UseCases/ActivityUseCase'; +import { ActivityRequestDTO } from '../Application/DTOs/ActivityRequestDTO'; +import { ActivityResponseDTO } from '../Application/DTOs/ActivityResponseDTO'; + +@Controller() +export class ActivityController { + private readonly activityUseCase: ActivityUseCase; + + constructor(activityUseCase: ActivityUseCase) { + this.activityUseCase = activityUseCase; + } + + async execute(action: string, request: object): Promise { + console.log('-- ActivityController.execute --'); + console.log({ + action, + request + }); + return this[action](request); + } + + async create(activityRequest: ActivityRequestDTO): Promise { + console.log('-- ActivityController.create --'); + return this.activityUseCase.createActivity(activityRequest); + } + +} \ No newline at end of file diff --git a/src/Activities/Infrastructure/ActivityModule.ts b/src/Activities/Infrastructure/ActivityModule.ts new file mode 100644 index 0000000..a8ae1a9 --- /dev/null +++ b/src/Activities/Infrastructure/ActivityModule.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { ActivityController } from './ActivityController'; +import { ActivityService } from '../Domain/Services/ActivityService'; +import { ActivityUseCase } from '../Application/UseCases/ActivityUseCase'; +import { ActivityRepositoryImpl } from './Adapters/ActivityRepositoryImpl'; + +@Module({ + controllers: [ + ActivityController + ], + providers: [ + ActivityService, + ActivityUseCase, + { + provide: 'ActivityRepository', + useClass: ActivityRepositoryImpl + } + ], +}) +export class ActivityModule { } \ No newline at end of file diff --git a/src/Activities/Infrastructure/Adapters/ActivityRepositoryImpl.ts b/src/Activities/Infrastructure/Adapters/ActivityRepositoryImpl.ts new file mode 100644 index 0000000..827d03c --- /dev/null +++ b/src/Activities/Infrastructure/Adapters/ActivityRepositoryImpl.ts @@ -0,0 +1,21 @@ +import { DynamoDBUtils } from '../../../Commons/DynamoDBUtils'; +import { Activity } from '../../Domain/Entities/Activity'; +import { ActivityRepository } from '../../Domain/Ports/ActivityRepository'; + +export class ActivityRepositoryImpl implements ActivityRepository { + + async createActivity(activity: Activity): Promise { + console.log('-- ActivityRepositoryImpl.createActivity --'); + console.log({ activity }); + const tabla = process.env.DYNAMODB_TABLE_ACTIVITY; + const params = { + TableName: tabla, + Item: activity + }; + const resultDB = await DynamoDBUtils.putItem(params); + if (resultDB) { + return activity; + } + return null; + } +} \ No newline at end of file diff --git a/src/Activities/Infrastructure/AppLambda.ts b/src/Activities/Infrastructure/AppLambda.ts new file mode 100644 index 0000000..143657f --- /dev/null +++ b/src/Activities/Infrastructure/AppLambda.ts @@ -0,0 +1,42 @@ +import { NestFactory } from '@nestjs/core'; +import { INestApplicationContext } from '@nestjs/common'; +import { Context } from 'aws-lambda'; +import { AppModule } from './AppModule'; +import { ActivityModule } from './ActivityModule'; +import { ActivityController } from './ActivityController'; +import { UtilsLambda } from '../../Commons/UtilsLambda'; + +let appContext: INestApplicationContext; + +async function bootstrap() { + console.log('-- bootstrap --'); + if (!appContext) { + appContext = await NestFactory.createApplicationContext(AppModule); + } + return appContext.select(ActivityModule).get(ActivityController); +} + +export const handler = async (event: any, context: Context, callback: any) => { + try { + console.log('-- handler --'); + const appContext = await bootstrap(); + const { eventName, dynamodb } = UtilsLambda.getRequestStremDynamoDB(event); + const request: any = {}; + let action = null; + if (eventName === 'INSERT') { + action = 'create' + const transactionId = dynamodb.Keys.transactionId.S; + request.transactionId = transactionId; + } else { + throw new Error('Invalid event name: ' + eventName); + } + const response = await appContext.execute(action, request); + console.log({ response }); + const responseLambda = UtilsLambda.getResponseLambda(response); + callback(null, responseLambda); + } catch (error) { + console.log({ error }); + const responseLambda = UtilsLambda.getResponseLambdaError(error); + callback(null, responseLambda); + } +}; \ No newline at end of file diff --git a/src/Activities/Infrastructure/AppModule.ts b/src/Activities/Infrastructure/AppModule.ts new file mode 100644 index 0000000..d098aa8 --- /dev/null +++ b/src/Activities/Infrastructure/AppModule.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { ActivityModule } from './ActivityModule'; + +@Module({ + imports: [ActivityModule], +}) +export class AppModule {} \ No newline at end of file diff --git a/src/Commons/Constants.ts b/src/Commons/Constants.ts new file mode 100644 index 0000000..7c52e89 --- /dev/null +++ b/src/Commons/Constants.ts @@ -0,0 +1,4 @@ +export enum RESPONSE_STATUS { + OK = 'OK', + ERROR = 'ERROR' +} \ No newline at end of file diff --git a/src/Commons/DynamoDBUtils.ts b/src/Commons/DynamoDBUtils.ts new file mode 100644 index 0000000..4261178 --- /dev/null +++ b/src/Commons/DynamoDBUtils.ts @@ -0,0 +1,46 @@ +import { DynamoDB } from 'aws-sdk'; + +const ACTIONS = { + put: 'put', + get: 'get', + delete: 'delete', + update: 'update', + query: 'query', + scan: 'scan', +}; + +export class DynamoDBUtils { + + static async callSingleOperation(action: string, params: any) { + console.log('-- DynamoDBUtils.callSingleOperation --'); + console.log({ action, params }); + try { + const dynamoDb = new DynamoDB.DocumentClient(); + return dynamoDb[action](params).promise(); + } catch (error) { + console.log(error); + throw new Error('Error calling DynamoDB'); + } + } + + static async putItem(params: { TableName: string, Item: any }): Promise { + console.log('-- DynamoDBUtils.putItem --'); + return await this.callSingleOperation(ACTIONS.put, params).then((data) => { + console.log({ data }); + if (data) { + return true; + } else { + return false; + }; + }); + } + + static async getItem(params: { TableName: string, Key: any }): Promise { + console.log('-- DynamoDBUtils.getItem --'); + return await this.callSingleOperation(ACTIONS.get, params).then((data) => { + console.log({ data }); + return data.Item; + }); + } + +} \ No newline at end of file diff --git a/src/Commons/UtilsLambda.ts b/src/Commons/UtilsLambda.ts new file mode 100644 index 0000000..140134a --- /dev/null +++ b/src/Commons/UtilsLambda.ts @@ -0,0 +1,37 @@ +export class UtilsLambda { + + static getRequestController(event: any) { + let body = event.body; + if (typeof event.body === 'string') { + body = JSON.parse(event.body); + } + return { + action: body.action, + request: body + }; + } + + static getRequestStremDynamoDB(event: any) { + const record = event.Records[0]; + return { + eventName: record.eventName, + dynamodb: record.dynamodb + }; + } + + static getResponseLambda(response: any) { + return { + statusCode: 200, + response, + }; + } + + static getResponseLambdaError(error: any) { + const message = error.message || 'Ocurrió un error inesperado'; + return { + statusCode: 500, + response: { message }, + }; + } + +} \ No newline at end of file diff --git a/src/Payments/Application/DTOs/PaymentRequestDTO.ts b/src/Payments/Application/DTOs/PaymentRequestDTO.ts new file mode 100644 index 0000000..5922236 --- /dev/null +++ b/src/Payments/Application/DTOs/PaymentRequestDTO.ts @@ -0,0 +1,4 @@ +export interface PaymentRequestDTO { + userId: string; + amount: number; +} \ No newline at end of file diff --git a/src/Payments/Application/DTOs/PaymentResponseDTO.ts b/src/Payments/Application/DTOs/PaymentResponseDTO.ts new file mode 100644 index 0000000..f20d180 --- /dev/null +++ b/src/Payments/Application/DTOs/PaymentResponseDTO.ts @@ -0,0 +1,4 @@ +export interface PaymentResponseDTO { + status: string; + transactionId?: string; +} \ No newline at end of file diff --git a/src/Payments/Application/UseCases/PaymentUseCase.ts b/src/Payments/Application/UseCases/PaymentUseCase.ts new file mode 100644 index 0000000..e74b680 --- /dev/null +++ b/src/Payments/Application/UseCases/PaymentUseCase.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@nestjs/common"; +import { v4 as uuidv4 } from 'uuid'; +import { PaymentService } from "../../Domain/Services/PaymentService"; +import { PaymentRequestDTO } from "../DTOs/PaymentRequestDTO"; +import { PaymentResponseDTO } from "../DTOs/PaymentResponseDTO"; +import { RESPONSE_STATUS } from "../../../Commons/Constants"; + +@Injectable() +export class PaymentUseCase { + + private readonly paymentService: PaymentService; + + constructor(paymentService: PaymentService) { + this.paymentService = paymentService; + } + + async createPayment(paymentRequest: PaymentRequestDTO): Promise { + console.log('-- PaymentUseCase.createPayment --'); + console.log(paymentRequest); + const result = await this.paymentService.createPayment(paymentRequest.userId); + let responsePayment: PaymentResponseDTO = result; + if (responsePayment.status === RESPONSE_STATUS.OK) { + responsePayment.transactionId = uuidv4(); + } + return responsePayment + } +} \ No newline at end of file diff --git a/src/Payments/Domain/Services/PaymentService.ts b/src/Payments/Domain/Services/PaymentService.ts new file mode 100644 index 0000000..cb78331 --- /dev/null +++ b/src/Payments/Domain/Services/PaymentService.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@nestjs/common"; +import { RESPONSE_STATUS } from "../../../Commons/Constants"; + +const USERS_RESPONSE_STATUS = { + "f529177d-0521-414e-acd9-6ac840549e97": RESPONSE_STATUS.OK, + "15f1c60a-2833-49b7-8660-065b58be2f89": RESPONSE_STATUS.ERROR +} +@Injectable() +export class PaymentService { + + constructor() { + console.log('PaymentService instantiated'); + } + + async createPayment(userId: string): Promise<{ status: string }> { + console.log('-- PaymentService.createPayment --'); + const randomStatus = USERS_RESPONSE_STATUS[userId] ?? RESPONSE_STATUS.ERROR; + return { + status: randomStatus + }; + } + +} \ No newline at end of file diff --git a/src/Payments/Infrastructure/AppLambda.ts b/src/Payments/Infrastructure/AppLambda.ts new file mode 100644 index 0000000..10eec0a --- /dev/null +++ b/src/Payments/Infrastructure/AppLambda.ts @@ -0,0 +1,36 @@ +import { NestFactory } from '@nestjs/core'; +import { INestApplicationContext } from '@nestjs/common'; +import { Context } from 'aws-lambda'; +import { AppModule } from './AppModule'; +import { PaymentModule } from './PaymentModule'; +import { PaymentController } from './PaymentController'; +import { UtilsLambda } from '../../Commons/UtilsLambda'; + +let appContext: INestApplicationContext; + +async function bootstrap() { + console.log('-- bootstrap --'); + if (!appContext) { + appContext = await NestFactory.createApplicationContext(AppModule); + } + return appContext.select(PaymentModule).get(PaymentController); +} + +export const handler = async (event: any, context: Context, callback: any) => { + console.log('-- handler --'); + try { + const appContext = await bootstrap(); + console.log({ event }); + console.log('--------------------------'); + const requestController = UtilsLambda.getRequestController(event); + const response = await appContext.execute(requestController.action, requestController.request); + console.log({ response }); + const responseLambda = UtilsLambda.getResponseLambda(response); + console.log({ responseLambda }); + callback(null, responseLambda); + } catch (error) { + console.log({ error }); + const responseLambda = UtilsLambda.getResponseLambdaError(error); + callback(null, responseLambda); + } +}; \ No newline at end of file diff --git a/src/Payments/Infrastructure/AppModule.ts b/src/Payments/Infrastructure/AppModule.ts new file mode 100644 index 0000000..f466876 --- /dev/null +++ b/src/Payments/Infrastructure/AppModule.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { PaymentModule } from './PaymentModule'; + +@Module({ + imports: [PaymentModule], +}) +export class AppModule {} \ No newline at end of file diff --git a/src/Payments/Infrastructure/PaymentController.ts b/src/Payments/Infrastructure/PaymentController.ts new file mode 100644 index 0000000..5f9e92e --- /dev/null +++ b/src/Payments/Infrastructure/PaymentController.ts @@ -0,0 +1,28 @@ +import { Controller } from "@nestjs/common"; +import { PaymentUseCase } from "../Application/UseCases/PaymentUseCase"; +import { PaymentResponseDTO } from "../Application/DTOs/PaymentResponseDTO"; +import { PaymentRequestDTO } from "../Application/DTOs/PaymentRequestDTO"; + +@Controller() +export class PaymentController { + private readonly paymentUseCase: PaymentUseCase; + + constructor(paymentUseCase: PaymentUseCase) { + this.paymentUseCase = paymentUseCase; + } + + async execute(action: string, request: object): Promise { + console.log('-- PaymentController.execute --'); + console.log({ + action, + request + }); + return this[action](request); + } + + async create(payment: PaymentRequestDTO): Promise { + console.log('-- PaymentController.create --'); + return this.paymentUseCase.createPayment(payment); + } + +} \ No newline at end of file diff --git a/src/Payments/Infrastructure/PaymentModule.ts b/src/Payments/Infrastructure/PaymentModule.ts new file mode 100644 index 0000000..b1088ba --- /dev/null +++ b/src/Payments/Infrastructure/PaymentModule.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; +import { PaymentController } from "./PaymentController"; +import { PaymentService } from "../Domain/Services/PaymentService"; +import { PaymentUseCase } from "../Application/UseCases/PaymentUseCase"; + +@Module({ + controllers: [ + PaymentController + ], + providers: [ + PaymentService, + PaymentUseCase + ], +}) +export class PaymentModule {} \ No newline at end of file diff --git a/src/Transactions/Application/DTOs/TransactionRequestDTO.ts b/src/Transactions/Application/DTOs/TransactionRequestDTO.ts new file mode 100644 index 0000000..1859509 --- /dev/null +++ b/src/Transactions/Application/DTOs/TransactionRequestDTO.ts @@ -0,0 +1,4 @@ +export interface TransactionRequestDTO { + userId: string; + amount: number; +} \ No newline at end of file diff --git a/src/Transactions/Application/DTOs/TransactionResponseDTO.ts b/src/Transactions/Application/DTOs/TransactionResponseDTO.ts new file mode 100644 index 0000000..7809674 --- /dev/null +++ b/src/Transactions/Application/DTOs/TransactionResponseDTO.ts @@ -0,0 +1,5 @@ +export interface TransactionResponseDTO { + transactionId: string; + userId: string; + amount: number; +} \ No newline at end of file diff --git a/src/Transactions/Application/UseCases/TransactionUseCase.ts b/src/Transactions/Application/UseCases/TransactionUseCase.ts new file mode 100644 index 0000000..2f7ae05 --- /dev/null +++ b/src/Transactions/Application/UseCases/TransactionUseCase.ts @@ -0,0 +1,33 @@ +import { Injectable } from "@nestjs/common"; +import { TransactionService } from "../../Domain/Services/TransactionService"; +import { TransactionRequestDTO } from "../DTOs/TransactionRequestDTO"; +import { TransactionResponseDTO } from "../DTOs/TransactionResponseDTO"; +import { Transaction } from "../../Domain/Entities/Transaction"; + +@Injectable() +export class TransactionUseCase { + + private readonly transactionService: TransactionService + + constructor(transactionService: TransactionService) { + this.transactionService = transactionService; + } + + async createTransaction(transactionRequest: TransactionRequestDTO): Promise { + const transaction: Transaction = { + userId: transactionRequest.userId, + amount: transactionRequest.amount + } + const transactionCreate = await this.transactionService.createTransaction(transaction); + return transactionCreate as TransactionResponseDTO; + } + + async getTransaction(transactionId: string): Promise { + console.log('-- TransactionUseCase.getTransaction --'); + const transaction = await this.transactionService.getTransaction(transactionId); + if (transaction === null) { + return { message: 'Transaction not found' }; + } + return transaction as TransactionResponseDTO; + } +} \ No newline at end of file diff --git a/src/Transactions/Domain/Entities/Transaction.ts b/src/Transactions/Domain/Entities/Transaction.ts new file mode 100644 index 0000000..30a0aac --- /dev/null +++ b/src/Transactions/Domain/Entities/Transaction.ts @@ -0,0 +1,5 @@ +export interface Transaction { + transactionId?: string; + userId: string; + amount: number; +} \ No newline at end of file diff --git a/src/Transactions/Domain/Ports/TransactionRepository.ts b/src/Transactions/Domain/Ports/TransactionRepository.ts new file mode 100644 index 0000000..91007bc --- /dev/null +++ b/src/Transactions/Domain/Ports/TransactionRepository.ts @@ -0,0 +1,9 @@ +import { Transaction } from "../Entities/Transaction"; + +export interface TransactionRepository { + + createTransaction(transaction: Transaction) : Promise; + + getTransaction(transactionId: string) : Promise; + +} \ No newline at end of file diff --git a/src/Transactions/Domain/Services/TransactionService.ts b/src/Transactions/Domain/Services/TransactionService.ts new file mode 100644 index 0000000..7a86c38 --- /dev/null +++ b/src/Transactions/Domain/Services/TransactionService.ts @@ -0,0 +1,30 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { TransactionRepository } from "../Ports/TransactionRepository"; +import { Transaction } from "../Entities/Transaction"; +import { v4 as uuidv4 } from 'uuid'; + + +@Injectable() +export class TransactionService { + + private readonly transactionRepository: TransactionRepository; + + constructor( + @Inject('TransactionRepository') + transactionRepository: TransactionRepository + ) { + this.transactionRepository = transactionRepository; + } + + async createTransaction(transaction: Transaction): Promise { + transaction.transactionId = uuidv4(); + return this.transactionRepository.createTransaction(transaction); + } + + async getTransaction(transactionId: string): Promise { + console.log('-- TransactionService.getTransaction --'); + console.log({ transactionId}); + return this.transactionRepository.getTransaction(transactionId); + } + +} \ No newline at end of file diff --git a/src/Transactions/Infrastructure/Adapters/TransactionRepositoryImpl.ts b/src/Transactions/Infrastructure/Adapters/TransactionRepositoryImpl.ts new file mode 100644 index 0000000..51fdbe9 --- /dev/null +++ b/src/Transactions/Infrastructure/Adapters/TransactionRepositoryImpl.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@nestjs/common"; +import { Transaction } from "../../Domain/Entities/Transaction"; +import { TransactionRepository } from "../../Domain/Ports/TransactionRepository"; +import { DynamoDBUtils } from "../../../Commons/DynamoDBUtils"; + +@Injectable() +export class TransactionRepositoryImpl implements TransactionRepository { + + createTransaction(transaction: Transaction): Promise { + // TODO: implementar + return Promise.resolve(transaction); + } + + async getTransaction(transactionId: string): Promise { + console.log('-- TransactionRepositoryImpl.getTransaction --'); + const tabla = process.env.DYNAMODB_TABLE_TRANSACTIONS; + const params = { + TableName: tabla, + Key: { + transactionId: transactionId + } + }; + const result = await DynamoDBUtils.getItem(params); + return result ? result as Transaction : null; + } + +} \ No newline at end of file diff --git a/src/Transactions/Infrastructure/AppLambda.ts b/src/Transactions/Infrastructure/AppLambda.ts new file mode 100644 index 0000000..ec2b5ec --- /dev/null +++ b/src/Transactions/Infrastructure/AppLambda.ts @@ -0,0 +1,32 @@ +import { NestFactory } from '@nestjs/core'; +import { ExpressAdapter } from '@nestjs/platform-express'; +import serverlessExpress from '@codegenie/serverless-express'; +import { Context, Handler } from 'aws-lambda'; +import express from 'express'; + +import { AppModule } from './AppModule'; + +let cachedServer: Handler; + +async function bootstrap() { + console.log('-- bootstrap --'); + if (!cachedServer) { + const expressApp = express(); + const nestApp = await NestFactory.create( + AppModule, + new ExpressAdapter(expressApp), + ); + nestApp.enableCors(); + await nestApp.init(); + + cachedServer = serverlessExpress({ app: expressApp }); + } + + return cachedServer; +} + +export const handler = async (event: any, context: Context, callback: any) => { + console.log('-- handler --'); + const server = await bootstrap(); + return server(event, context, callback); +}; \ No newline at end of file diff --git a/src/Transactions/Infrastructure/AppModule.ts b/src/Transactions/Infrastructure/AppModule.ts new file mode 100644 index 0000000..bb64b23 --- /dev/null +++ b/src/Transactions/Infrastructure/AppModule.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { TransactionModule } from './TransactionModule'; + +@Module({ + imports: [TransactionModule], +}) +export class AppModule {} \ No newline at end of file diff --git a/src/Transactions/Infrastructure/TransactionController.ts b/src/Transactions/Infrastructure/TransactionController.ts new file mode 100644 index 0000000..e0bf378 --- /dev/null +++ b/src/Transactions/Infrastructure/TransactionController.ts @@ -0,0 +1,26 @@ +import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { TransactionUseCase } from '../Application/UseCases/TransactionUseCase'; +import { TransactionRequestDTO } from '../Application/DTOs/TransactionRequestDTO'; + +@Controller('V1/transactions') +export class TransactionController { + + private readonly transactionUseCase: TransactionUseCase; + + constructor(transactionUseCase: TransactionUseCase) { + this.transactionUseCase = transactionUseCase; + } + + @Post() + async create(@Body() transaction: TransactionRequestDTO) { + return this.transactionUseCase.createTransaction(transaction); + } + + @Get() + async getTransaction(@Query('transactionId') transactionId: string) { + console.log('-- TransactionController.getTransaction --'); + console.log('transactionId:', transactionId); + return this.transactionUseCase.getTransaction(transactionId); + } + +} \ No newline at end of file diff --git a/src/Transactions/Infrastructure/TransactionModule.ts b/src/Transactions/Infrastructure/TransactionModule.ts new file mode 100644 index 0000000..eddc2e8 --- /dev/null +++ b/src/Transactions/Infrastructure/TransactionModule.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TransactionController } from './TransactionController'; +import { TransactionService } from '../Domain/Services/TransactionService'; +import { TransactionUseCase } from '../Application/UseCases/TransactionUseCase'; +import { TransactionRepositoryImpl } from './Adapters/TransactionRepositoryImpl'; + +@Module({ + controllers: [ + TransactionController + ], + providers: [ + TransactionService, + TransactionUseCase, + { + provide: 'TransactionRepository', + useClass: TransactionRepositoryImpl + } + ], +}) +export class TransactionModule {} \ No newline at end of file diff --git a/test/Activities/ActivityUseCase.feature b/test/Activities/ActivityUseCase.feature new file mode 100644 index 0000000..351fcbf --- /dev/null +++ b/test/Activities/ActivityUseCase.feature @@ -0,0 +1,11 @@ +Feature: Activity Use Case + + Scenario: Create a new activity successfully + Given I have a valid activity data + When I try to create a new activity + Then the activity should be created successfully + + Scenario: Fail to create a new activity with invalid data + Given I have invalid activity data + When I try to create a new activity + Then the creation should fail with an error message \ No newline at end of file diff --git a/test/Activities/ActivityUseCase.steps.ts b/test/Activities/ActivityUseCase.steps.ts new file mode 100644 index 0000000..333d097 --- /dev/null +++ b/test/Activities/ActivityUseCase.steps.ts @@ -0,0 +1,70 @@ +import { defineFeature, loadFeature } from 'jest-cucumber'; +import { ActivityUseCase } from '../../src/Activities/Application/UseCases/ActivityUseCase'; +import { ActivityService } from '../../src/Activities/Domain/Services/ActivityService'; +import { ActivityRequestDTO } from '../../src/Activities/Application/DTOs/ActivityRequestDTO'; +import { Activity } from '../../src/Activities/Domain/Entities/Activity'; +import { ActivityRepositoryImpl } from '../../src/Activities/Infrastructure/Adapters/ActivityRepositoryImpl'; +import { RESPONSE_STATUS } from '../../src/Commons/Constants'; + +const feature = loadFeature('./test/Activities/ActivityUseCase.feature'); + +defineFeature(feature, test => { + let activityRepository : ActivityRepositoryImpl; + let activityService : ActivityService; + let activityUseCase: ActivityUseCase; + let activityRequest: ActivityRequestDTO + let result: any; + + beforeEach(() => { + activityRepository = new ActivityRepositoryImpl(); + activityService = new ActivityService(activityRepository); + activityUseCase = new ActivityUseCase(activityService); + }); + + test('Create a new activity successfully', ({ given, when, then }) => { + given('I have a valid activity data', () => { + activityRequest = { + transactionId: '123456789', + } + }); + + when('I try to create a new activity', async () => { + const activityMock: Activity = { + activityId: '123456789', + transactionId: '123456789', + date: new Date().toISOString() + } + jest.spyOn(activityRepository, 'createActivity').mockResolvedValue(activityMock); + result = await activityUseCase.createActivity(activityRequest); + }); + + then('the activity should be created successfully', () => { + console.log({ result }); + expect(result).toBeDefined(); + expect(result.status).toEqual(RESPONSE_STATUS.OK); + }); + }); + + test('Fail to create a new activity with invalid data', ({ given, when, then }) => { + given('I have invalid activity data', () => { + activityRequest = { + transactionId: 'xxxxxxxx', + } + }); + + when('I try to create a new activity', async () => { + try { + jest.spyOn(activityRepository, 'createActivity').mockResolvedValue(null); + result = await activityUseCase.createActivity(activityRequest); + } catch (e) { + result = e; + } + }); + + then('the creation should fail with an error message', () => { + console.log({ result }); + expect(result).toBeDefined(); + expect(result.status).toEqual(RESPONSE_STATUS.ERROR); + }); + }); +}); \ No newline at end of file diff --git a/test/Payments/PaymentUseCase.steps.ts b/test/Payments/PaymentUseCase.steps.ts new file mode 100644 index 0000000..6a08217 --- /dev/null +++ b/test/Payments/PaymentUseCase.steps.ts @@ -0,0 +1,56 @@ +import { defineFeature, loadFeature } from 'jest-cucumber'; +import { RESPONSE_STATUS } from '../../src/Commons/Constants'; +import { PaymentRequestDTO } from '../../src/Payments/Application/DTOs/PaymentRequestDTO'; +import { PaymentUseCase } from '../../src/Payments/Application/UseCases/PaymentUseCase'; +import { PaymentService } from '../../src/Payments/Domain/Services/PaymentService'; + + +const feature = loadFeature('./test/Payments/PaymentUseCase.feature'); + +defineFeature(feature, test => { + let paymentService: PaymentService; + let paymentUseCase: PaymentUseCase + let paymentRequest: PaymentRequestDTO + let result: any; + + beforeEach(() => { + paymentService = new PaymentService(); + paymentUseCase = new PaymentUseCase(paymentService); + }); + + test('Create a new payment successfully', ({ given, when, then }) => { + given('I have a valid payment data', () => { + paymentRequest = { + userId: 'f529177d-0521-414e-acd9-6ac840549e97', + amount: 100 + } + }); + + when('I try to create a new payment', async () => { + result = await paymentUseCase.createPayment(paymentRequest); + }); + + then('the payment should be created successfully', () => { + expect(result).toBeDefined(); + expect(result.status).toEqual(RESPONSE_STATUS.OK); + }); + }); + + test('Fail to create a new payment with invalid data', ({ given, when, then }) => { + given('I have invalid payment data', () => { + paymentRequest = { + userId: '15f1c60a-2833-49b7-8660-065b58be2f89', + amount: 50 + } + }); + + when('I try to create a new payment', async () => { + result = await paymentUseCase.createPayment(paymentRequest); + }); + + then('the creation should fail with an error message', () => { + expect(result).toBeDefined(); + expect(result.status).toEqual(RESPONSE_STATUS.ERROR); + }); + }); +}); \ No newline at end of file diff --git a/test/Payments/PaymentuseCase.feature b/test/Payments/PaymentuseCase.feature new file mode 100644 index 0000000..e74bb12 --- /dev/null +++ b/test/Payments/PaymentuseCase.feature @@ -0,0 +1,11 @@ +Feature: Payment Use Case + + Scenario: Create a new payment successfully + Given I have a valid payment data + When I try to create a new payment + Then the payment should be created successfully + + Scenario: Fail to create a new payment with invalid data + Given I have invalid payment data + When I try to create a new payment + Then the creation should fail with an error message \ No newline at end of file diff --git a/test/Transactions/TransactionUseCase.feature b/test/Transactions/TransactionUseCase.feature new file mode 100644 index 0000000..45d8ab0 --- /dev/null +++ b/test/Transactions/TransactionUseCase.feature @@ -0,0 +1,11 @@ +Feature: Transaction Use Case + + Scenario: Get transaction successfully + Given I have a valid transaction data + When I try get transaction + Then the transaction should be successfully + + Scenario: Fail to get transaction with invalid data + Given I have invalid transaction data + When I try get transaction + Then the transaction should fail with an error message \ No newline at end of file diff --git a/test/Transactions/TransactionUseCase.steps.ts b/test/Transactions/TransactionUseCase.steps.ts new file mode 100644 index 0000000..bdef93d --- /dev/null +++ b/test/Transactions/TransactionUseCase.steps.ts @@ -0,0 +1,61 @@ +import { defineFeature, loadFeature } from 'jest-cucumber'; +import { TransactionUseCase } from '../../src/Transactions/Application/UseCases/TransactionUseCase'; +import { TransactionService } from '../../src/Transactions/Domain/Services/TransactionService'; +import { TransactionRepositoryImpl } from '../../src/Transactions/Infrastructure/Adapters/TransactionRepositoryImpl'; +import { Transaction } from '../../src/Transactions/Domain/Entities/Transaction'; + + +const feature = loadFeature('./test/Transactions/TransactionUseCase.feature'); + +defineFeature(feature, test => { + let transactionRepository: TransactionRepositoryImpl; + let transactionService: TransactionService; + let transactionUseCase: TransactionUseCase; + let transactionIdRequest: string; + let result: any; + + beforeEach(() => { + transactionRepository = new TransactionRepositoryImpl(); + transactionService = new TransactionService(transactionRepository); + transactionUseCase = new TransactionUseCase(transactionService); + }); + + test('Get transaction successfully', ({ given, when, then }) => { + given('I have a valid transaction data', () => { + transactionIdRequest = '123456789'; + }); + + when('I try get transaction', async () => { + const transactionMock: Transaction = { + transactionId: transactionIdRequest, + userId: 'xxxxxxxxxxxxxxxxxxx', + amount: 1000 + }; + jest.spyOn(transactionRepository, 'getTransaction').mockResolvedValue(transactionMock); + result = await transactionUseCase.getTransaction(transactionIdRequest); + }); + + then('the transaction should be successfully', () => { + console.log({result}); + expect(result).toBeDefined(); + expect(result.transactionId).toEqual(transactionIdRequest); + }); + }); + + test('Fail to get transaction with invalid data', ({ given, when, then }) => { + given('I have invalid transaction data', () => { + transactionIdRequest = '987654321'; + }); + + when('I try get transaction', async () => { + jest.spyOn(transactionRepository, 'getTransaction').mockResolvedValue(null); + result = await transactionUseCase.getTransaction(transactionIdRequest); + }); + + then('the transaction should fail with an error message', () => { + console.log({ result }); + expect(result.message).toEqual('Transaction not found'); + }); + }); + +}); \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..27cb02b --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*steps.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5aa7d94 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + //"incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true + } +}