Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ node_modules
coverage/
.vscode

dist/
dist/
.serverless/
11,530 changes: 11,530 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
{
"name": "io-node-challenge",
"version": "1.0.0",
"author": "io-developers",
"author": "Smith Vasquez",
"description": "API para procesar pagos y actualizar saldos",
"license": "ISC",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts,.tsx --fix",
"format": "prettier --write 'src/**/*.ts' --config ./.prettierrc",
"prepare": "husky"
},
"dependencies": {},
"dependencies": {
"aws-sdk": "2.1691.0",
"axios": "1.7.7",
"uuid": "10.0.0"
},
"devDependencies": {
"@types/aws-lambda": "8.10.130",
"@types/jest": "29.5.5",
"@types/node": "20.14.1",
"@types/serverless": "3.12.22",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"eslint": "8.8.0",
Expand All @@ -22,7 +30,12 @@
"husky": "9.1.4",
"jest": "29.7.0",
"jest-mock-extended": "3.0.5",
"nodemon": "3.1.4",
"prettier": "3.0.3",
"serverless": "3.38.0",
"serverless-iam-roles-per-function": "3.2.0",
"serverless-plugin-typescript": "2.1.5",
"serverless-step-functions": "3.21.0",
"ts-jest": "29.1.2",
"ts-node": "10.9.1",
"typescript": "4.7.4"
Expand Down
204 changes: 204 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
service: io-node-challenge

provider:
name: aws
runtime: nodejs18.x # Utilizar la última versión de Node.js soportada
region: us-east-1
environment:
ACCOUNTS_TABLE: ${self:custom.accountsTable}
TRANSACTION_TABLE: ${self:custom.transactionsTable}
MOCK_API_URL: https://q7sfxpg9mg.execute-api.us-east-1.amazonaws.com/dev/mock/payments


plugins:
- serverless-step-functions
- serverless-plugin-typescript
- serverless-iam-roles-per-function

# Definir los recursos de la aplicación (tablas DynamoDB)
resources:
Resources:
AccountsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.accountsTable}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST

TransactionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.transactionsTable}
AttributeDefinitions:
- AttributeName: source
AttributeType: S
- AttributeName: id
AttributeType: N
KeySchema:
- AttributeName: source
KeyType: HASH
- AttributeName: id
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
StreamSpecification:
StreamViewType: NEW_IMAGE

# Definir funciones Lambda
functions:
GetAccount:
handler: src/functions/get-account.handler
events:
- http:
path: v1/accounts/{accountId}
method: get
cors: true
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:GetItem
Resource:
- { "Fn::GetAtt": [AccountsTable, Arn] }

ValidateAccountLambdaFunction:
handler: src/handlers/validate-account.handler
memorySize: 128
timeout: 10
environment:
ACCOUNTS_TABLE: ${self:custom.accountsTable}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:GetItem
Resource:
- { "Fn::GetAtt": [AccountsTable, Arn] }

ExecutePaymentLambdaFunction:
handler: src/functions/execute-payment.handler
memorySize: 128
timeout: 10
events:
- http:
path: v1/payments # Ruta principal para realizar pagos
method: post
cors: true
environment:
MOCK_API_URL: ${self:provider.environment.MOCK_API_URL}
TRANSACTION_TABLE: ${self:custom.transactionsTable}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource:
- { "Fn::GetAtt": [TransactionsTable, Arn] }

SaveTransactionLambdaFunction:
handler: src/handlers/save-transaction.handler
memorySize: 128
timeout: 10
environment:
TRANSACTION_TABLE: ${self:custom.transactionsTable}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource:
- { "Fn::GetAtt": [TransactionsTable, Arn] }

UpdateAccountLambdaFunction:
handler: src/functions/update-account.handler
memorySize: 128
timeout: 10
environment:
ACCOUNTS_TABLE: ${self:custom.accountsTable}
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt:
- TransactionsTable
- StreamArn
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:UpdateItem
Resource:
- { "Fn::GetAtt": [AccountsTable, Arn] }

mockPaymentAPI:
handler: src/functions/mock-payment-api.handler
memorySize: 128
timeout: 10
events:
- http:
path: mock/payments
method: post

#Definir las máquinas de estado de Step Functions
stepFunctions:
stateMachines:
paymentProcessStateMachine:
name: PaymentProcessStateMachine
definition:
Comment: "State machine to process payments and update accounts"
StartAt: GetAccount
States:
GetAccount:
Type: Task
Resource:
Fn::GetAtt: [ ValidateAccountLambdaFunction, Arn ]
Next: CheckAccount

CheckAccount:
Type: Choice
Choices:
- Variable: "$.accountFound"
BooleanEquals: true
Next: ExecutePayment
Default: FailState

ExecutePayment:
Type: Task
Resource:
Fn::GetAtt: [ ExecutePaymentLambdaFunction, Arn ]

Catch:
- ErrorEquals:
- States.ALL
Next: FailState
Next: SaveTransaction

SaveTransaction:
Type: Task
Resource:
Fn::GetAtt: [ SaveTransactionLambdaFunction, Arn ]
Next: SuccessState

SuccessState:
Type: Succeed

FailState:
Type: Fail
Cause: "Account not found or payment failed"
Error: "ValidationError"


# Agregar permisos para que las Step Functions puedan invocar Lambdas
role:
statements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource:
- Fn::GetAtt: [ ValidateAccountLambdaFunction, Arn ]
- Fn::GetAtt: [ ExecutePaymentLambdaFunction, Arn ]
- Fn::GetAtt: [ SaveTransactionLambdaFunction, Arn ]

# Configuración custom para las tablas
custom:
accountsTable: Accounts
transactionsTable: Transactions
65 changes: 65 additions & 0 deletions src/functions/execute-payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { DynamoDB } from 'aws-sdk';
import axios from 'axios';// para hacer solicitudes HTTP
import { v4 as uuidv4 } from 'uuid';

const dynamoDb = new DynamoDB.DocumentClient();
const transactionsTable = process.env.TRANSACTION_TABLE || '';

export const handler = async (event) => {
try {
const { accountId, amount } = JSON.parse(event.body || '{}');
console.debug("Initialize handler execute payments", accountId);

// Validación básica de los parámetros de entrada
if (!accountId || !amount) {
return {
statusCode: 400,
body: JSON.stringify({ message: 'Invalid request. Missing parameters.' }),
};
}

// Llamar al API Mock que simula la pasarela de pagos
const paymentResponse = await axios.post(
`${process.env.MOCK_API_URL}`,
JSON.stringify({ accountId, amount }),
{
headers: { 'Content-Type': 'application/json' },
}
);

const paymentResult = await paymentResponse.data;
if (paymentResponse.status === 200) {
// Generar el ID único para la transacción
const transactionId = uuidv4();
const transaction = {
source: transactionId,
id: Date.now(),
data: { accountId, amount },
};
// Guardar la transacción en DynamoDB
await dynamoDb.put({
TableName: transactionsTable,
Item: transaction,
}).promise();

return {
statusCode: 201,
body: JSON.stringify({
message: 'Payment registered successfully',
transactionId: transactionId,
}),
};
}

return {
statusCode: 400,
body: JSON.stringify({ message: 'Payment failed' }),
};
} catch (error) {
console.error("Error execute payments: ", error);
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal server error', error: error.message }),
};
}
};
41 changes: 41 additions & 0 deletions src/functions/get-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DynamoDB } from 'aws-sdk';

const dynamoDb = new DynamoDB.DocumentClient();
const accountsTable = process.env.ACCOUNTS_TABLE || '';

export const handler = async (event) => {
const accountId = event.pathParameters?.accountId;

if (!accountId) {
return {
statusCode: 400,
body: JSON.stringify({ message: 'Account ID is required' }),
};
}

const params = {
TableName: accountsTable,
Key: { id: accountId },
};

try {
const result = await dynamoDb.get(params).promise();

if (!result.Item) {
return {
statusCode: 404,
body: JSON.stringify({ message: 'Account not found' }),
};
}

return {
statusCode: 200,
body: JSON.stringify(result.Item),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal server error', error: error.message }),
};
}
};
20 changes: 20 additions & 0 deletions src/functions/mock-payment-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

export const handler = async (event) => {
const body = JSON.parse(event.body || '{}');

// Validación de parámetros
if (body.accountId && body.amount) {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Transaction successful',
transactionId: '8db0a6fc-ad42-4974-ac1f-36bb90730afe',
}),
};
}

return {
statusCode: 400,
body: JSON.stringify({ message: 'Payment failed' }),
};
};
Loading