-
Notifications
You must be signed in to change notification settings - Fork 468
SHIP: solution #572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
SHIP: solution #572
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a complete anti-fraud transaction processing system for Yape's technical challenge. The solution uses event-driven architecture with NestJS, Kafka, and PostgreSQL to validate financial transactions in real-time.
Key Changes:
- Implements dual API support (REST + GraphQL) for transaction management
- Microservices architecture with Transaction API and Anti-Fraud Service
- Event-driven communication using Kafka for asynchronous validation
- CQRS pattern with clean architecture principles
- Comprehensive test coverage with unit and E2E tests
Reviewed changes
Copilot reviewed 51 out of 53 changed files in this pull request and generated 25 comments.
Show a summary per file
| File | Description |
|---|---|
package.json, tsconfig.json, nest-cli.json |
Workspace configuration for NestJS monorepo setup |
docker-compose.yml |
Infrastructure setup with PostgreSQL, Kafka, and Zookeeper |
libs/shared-types/ |
Shared TypeScript types for events across services |
apps/transaction-api/ |
Main transaction API with REST/GraphQL endpoints, CQRS handlers, and Kafka integration |
apps/anti-fraud-service/ |
Anti-fraud validation service that consumes and validates transactions |
apps/transaction-api/prisma/ |
Database schema, migrations, and seed data |
scripts/ |
Automation scripts for setup, starting services, and testing |
README.md |
Comprehensive documentation with architecture diagrams and API examples |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Injectable() | ||
| export class FraudDetectionService { | ||
| private readonly logger = new Logger(FraudDetectionService.name); | ||
| private readonly AMOUNT_THRESHOLD = 1000; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The anti-fraud threshold of 1000 is hardcoded as a private class member. This business rule should be configurable via environment variables or a configuration file to allow different thresholds in different environments without code changes.
| it('debería crear una transacción válida con estado pending', async () => { | ||
| // Prepare: Arrange transaction data | ||
| const transactionDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| tranferTypeId: 1, | ||
| value: 500, | ||
| }; | ||
|
|
||
| // Execute: Act - Create transaction | ||
| const response = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(transactionDto) | ||
| .expect(201); | ||
|
|
||
| // Validate: Assert response structure and data | ||
| expect(response.body).toHaveProperty('transactionExternalId'); | ||
| expect(response.body.transactionExternalId).toMatch( | ||
| /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, | ||
| ); | ||
|
|
||
| expect(response.body.transactionType).toEqual({ | ||
| name: 'Transfer', | ||
| }); | ||
|
|
||
| expect(response.body.transactionStatus).toEqual({ | ||
| name: 'pending', | ||
| }); | ||
|
|
||
| expect(response.body.value).toBe(500); | ||
| expect(response.body.createdAt).toBeDefined(); | ||
| expect(new Date(response.body.createdAt)).toBeInstanceOf(Date); | ||
| }); | ||
|
|
||
| it('debería crear una transacción de alto valor con estado pending', async () => { | ||
| // Prepare: Arrange high-value transaction | ||
| const transactionDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440002', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440003', | ||
| tranferTypeId: 1, | ||
| value: 1500, | ||
| }; | ||
|
|
||
| // Execute: Act | ||
| const response = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(transactionDto) | ||
| .expect(201); | ||
|
|
||
| // Validate: Assert | ||
| expect(response.body.transactionStatus.name).toBe('pending'); | ||
| expect(response.body.value).toBe(1500); | ||
| }); | ||
|
|
||
| it('debería rechazar una transacción con valor negativo', async () => { | ||
| // Prepare: Arrange invalid transaction with negative value | ||
| const invalidDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| tranferTypeId: 1, | ||
| value: -100, | ||
| }; | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| const response = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(invalidDto) | ||
| .expect(400); | ||
|
|
||
| expect(response.body.message).toBeDefined(); | ||
| }); | ||
|
|
||
| it('debería rechazar una transacción sin accountExternalIdDebit', async () => { | ||
| // Prepare: Arrange incomplete transaction data | ||
| const invalidDto = { | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| tranferTypeId: 1, | ||
| value: 500, | ||
| }; | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(invalidDto) | ||
| .expect(400); | ||
| }); | ||
|
|
||
| it('debería rechazar una transacción con UUID inválido', async () => { | ||
| // Prepare: Arrange transaction with invalid UUID | ||
| const invalidDto = { | ||
| accountExternalIdDebit: 'invalid-uuid', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| tranferTypeId: 1, | ||
| value: 500, | ||
| }; | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(invalidDto) | ||
| .expect(400); | ||
| }); | ||
|
|
||
| it('debería rechazar una transacción con tranferTypeId inválido', async () => { | ||
| // Prepare: Arrange transaction with invalid type ID | ||
| const invalidDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| tranferTypeId: 999, // ID no existente | ||
| value: 500, | ||
| }; | ||
|
|
||
| // Execute: Act | ||
| const response = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(invalidDto); | ||
|
|
||
| // Validate: Assert - Should fail (400 or 404 depending on implementation) | ||
| expect([400, 404, 500]).toContain(response.status); | ||
| }); | ||
| }); | ||
|
|
||
| describe('GET /transactions/:id - Get Transaction', () => { | ||
| it('debería obtener una transacción existente por su ID', async () => { | ||
| // Prepare: Create a transaction first | ||
| const createDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440010', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440011', | ||
| tranferTypeId: 1, | ||
| value: 750, | ||
| }; | ||
|
|
||
| const createResponse = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(createDto) | ||
| .expect(201); | ||
|
|
||
| const transactionId = createResponse.body.transactionExternalId; | ||
|
|
||
| // Execute: Act - Get the transaction | ||
| const getResponse = await request(app.getHttpServer()) | ||
| .get(`/transactions/${transactionId}`) | ||
| .expect(200); | ||
|
|
||
| // Validate: Assert | ||
| expect(getResponse.body.transactionExternalId).toBe(transactionId); | ||
| expect(getResponse.body.value).toBe(750); | ||
| expect(getResponse.body.transactionType).toEqual({ | ||
| name: 'Transfer', | ||
| }); | ||
| expect(getResponse.body.transactionStatus).toBeDefined(); | ||
| }); | ||
|
|
||
| it('debería retornar 404 para una transacción inexistente', async () => { | ||
| // Prepare: Arrange non-existent transaction ID | ||
| const nonExistentId = '550e8400-e29b-41d4-a716-999999999999'; | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await request(app.getHttpServer()) | ||
| .get(`/transactions/${nonExistentId}`) | ||
| .expect(404); | ||
| }); | ||
|
|
||
| it('debería retornar 400 para un ID inválido', async () => { | ||
| // Prepare: Arrange invalid transaction ID | ||
| const invalidId = 'not-a-valid-uuid'; | ||
|
|
||
| // Execute: Act | ||
| const response = await request(app.getHttpServer()) | ||
| .get(`/transactions/${invalidId}`); | ||
|
|
||
| // Validate: Assert - Should be 400 or 404 | ||
| expect([400, 404]).toContain(response.status); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Complete Transaction Flow with Anti-Fraud', () => { | ||
| it('debería procesar el flujo completo: crear → validar → actualizar estado (approved)', async () => { | ||
| // Prepare: Arrange transaction that should be approved (value <= 1000) | ||
| const transactionDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440020', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440021', | ||
| tranferTypeId: 1, | ||
| value: 800, | ||
| }; | ||
|
|
||
| // Execute Step 1: Create transaction | ||
| const createResponse = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(transactionDto) | ||
| .expect(201); | ||
|
|
||
| const transactionId = createResponse.body.transactionExternalId; | ||
|
|
||
| // Validate Step 1: Transaction created with pending status | ||
| expect(createResponse.body.transactionStatus.name).toBe('pending'); | ||
|
|
||
| // Execute Step 2: Wait for anti-fraud processing | ||
| // En un entorno de pruebas real, esperarías a que Kafka procese el evento | ||
| await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
|
|
||
| // Execute Step 3: Get updated transaction | ||
| const getResponse = await request(app.getHttpServer()) | ||
| .get(`/transactions/${transactionId}`) | ||
| .expect(200); | ||
|
|
||
| // Validate Step 3: Transaction should be approved | ||
| expect(getResponse.body.transactionExternalId).toBe(transactionId); | ||
| expect(getResponse.body.value).toBe(800); | ||
| // Note: En un test E2E real con Kafka, el estado debería ser 'approved' | ||
| // Si Kafka no está corriendo, seguirá siendo 'pending' | ||
| expect(['pending', 'approved']).toContain(getResponse.body.transactionStatus.name); | ||
| }, 10000); // Timeout aumentado para esperar procesamiento asíncrono | ||
|
|
||
| it('debería procesar el flujo completo: crear → validar → actualizar estado (rejected)', async () => { | ||
| // Prepare: Arrange transaction that should be rejected (value > 1000) | ||
| const transactionDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440022', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440023', | ||
| tranferTypeId: 1, | ||
| value: 1500, | ||
| }; | ||
|
|
||
| // Execute Step 1: Create transaction | ||
| const createResponse = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(transactionDto) | ||
| .expect(201); | ||
|
|
||
| const transactionId = createResponse.body.transactionExternalId; | ||
|
|
||
| // Validate Step 1: Transaction created with pending status | ||
| expect(createResponse.body.transactionStatus.name).toBe('pending'); | ||
|
|
||
| // Execute Step 2: Wait for anti-fraud processing | ||
| await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
|
|
||
| // Execute Step 3: Get updated transaction | ||
| const getResponse = await request(app.getHttpServer()) | ||
| .get(`/transactions/${transactionId}`) | ||
| .expect(200); | ||
|
|
||
| // Validate Step 3: Transaction should be rejected | ||
| expect(getResponse.body.transactionExternalId).toBe(transactionId); | ||
| expect(getResponse.body.value).toBe(1500); | ||
| // Note: En un test E2E real con Kafka, el estado debería ser 'rejected' | ||
| expect(['pending', 'rejected']).toContain(getResponse.body.transactionStatus.name); | ||
| }, 10000); | ||
|
|
||
| it('debería manejar correctamente el caso límite de $1000', async () => { | ||
| // Prepare: Arrange transaction with boundary value (exactly 1000) | ||
| const transactionDto = { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440024', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440025', | ||
| tranferTypeId: 1, | ||
| value: 1000, | ||
| }; | ||
|
|
||
| // Execute Step 1: Create transaction | ||
| const createResponse = await request(app.getHttpServer()) | ||
| .post('/transactions') | ||
| .send(transactionDto) | ||
| .expect(201); | ||
|
|
||
| const transactionId = createResponse.body.transactionExternalId; | ||
|
|
||
| // Validate Step 1: Transaction created | ||
| expect(createResponse.body.transactionStatus.name).toBe('pending'); | ||
| expect(createResponse.body.value).toBe(1000); | ||
|
|
||
| // Execute Step 2: Wait for processing | ||
| await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
|
|
||
| // Execute Step 3: Get updated transaction | ||
| const getResponse = await request(app.getHttpServer()) | ||
| .get(`/transactions/${transactionId}`) | ||
| .expect(200); | ||
|
|
||
| // Validate Step 3: Transaction with value = 1000 should be approved | ||
| expect(['pending', 'approved']).toContain(getResponse.body.transactionStatus.name); | ||
| }, 10000); | ||
| }); | ||
|
|
||
| describe('Multiple Concurrent Transactions', () => { | ||
| it('debería manejar múltiples transacciones concurrentes', async () => { | ||
| // Prepare: Arrange multiple transactions | ||
| const transactions = [ | ||
| { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440030', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440031', | ||
| tranferTypeId: 1, | ||
| value: 100, | ||
| }, | ||
| { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440032', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440033', | ||
| tranferTypeId: 1, | ||
| value: 200, | ||
| }, | ||
| { | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440034', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440035', | ||
| tranferTypeId: 1, | ||
| value: 300, | ||
| }, | ||
| ]; | ||
|
|
||
| // Execute: Act - Create all transactions concurrently | ||
| const createPromises = transactions.map((dto) => | ||
| request(app.getHttpServer()).post('/transactions').send(dto), | ||
| ); | ||
|
|
||
| const responses = await Promise.all(createPromises); | ||
|
|
||
| // Validate: Assert all transactions were created | ||
| responses.forEach((response) => { | ||
| expect(response.status).toBe(201); | ||
| expect(response.body.transactionExternalId).toBeDefined(); | ||
| expect(response.body.transactionStatus.name).toBe('pending'); | ||
| }); | ||
|
|
||
| // Validate: All transactions have unique IDs | ||
| const ids = responses.map((r) => r.body.transactionExternalId); | ||
| const uniqueIds = new Set(ids); | ||
| expect(uniqueIds.size).toBe(transactions.length); | ||
| }); | ||
| }); | ||
| }); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test descriptions are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. All test descriptions should be rewritten in English for consistency with the codebase standards.
| it('debería actualizar el estado de la transacción a approved cuando el evento indica aprobación', async () => { | ||
| // Prepare: Arrange approved transaction event | ||
| const event: TransactionValidatedEvent = { | ||
| eventId: 'event-123', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-123', | ||
| data: { | ||
| transactionExternalId: 'transaction-456', | ||
| status: 'approved', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| repository.updateStatus.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.handle(event); | ||
|
|
||
| // Validate: Assert | ||
| expect(repository.updateStatus).toHaveBeenCalledTimes(1); | ||
| expect(repository.updateStatus).toHaveBeenCalledWith( | ||
| 'transaction-456', | ||
| 2, // ID de estado "approved" | ||
| ); | ||
| }); | ||
|
|
||
| it('debería actualizar el estado de la transacción a rejected cuando el evento indica rechazo', async () => { | ||
| // Prepare: Arrange rejected transaction event | ||
| const event: TransactionValidatedEvent = { | ||
| eventId: 'event-789', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-789', | ||
| data: { | ||
| transactionExternalId: 'transaction-999', | ||
| status: 'rejected', | ||
| reason: 'Transaction amount 1500 exceeds threshold 1000', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| repository.updateStatus.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.handle(event); | ||
|
|
||
| // Validate: Assert | ||
| expect(repository.updateStatus).toHaveBeenCalledTimes(1); | ||
| expect(repository.updateStatus).toHaveBeenCalledWith( | ||
| 'transaction-999', | ||
| 3, // ID de estado "rejected" | ||
| ); | ||
| }); | ||
|
|
||
| it('debería propagar errores del repositorio', async () => { | ||
| // Prepare: Arrange error scenario | ||
| const event: TransactionValidatedEvent = { | ||
| eventId: 'event-error', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-error', | ||
| data: { | ||
| transactionExternalId: 'transaction-error', | ||
| status: 'approved', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| const dbError = new Error('Transaction not found'); | ||
| repository.updateStatus.mockRejectedValue(dbError); | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await expect(handler.handle(event)).rejects.toThrow('Transaction not found'); | ||
|
|
||
| expect(repository.updateStatus).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('debería manejar correctamente el mapeo de status approved a statusId 2', async () => { | ||
| // Prepare: Arrange approved event | ||
| const event: TransactionValidatedEvent = { | ||
| eventId: 'event-mapping-approved', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-mapping', | ||
| data: { | ||
| transactionExternalId: 'transaction-mapping-approved', | ||
| status: 'approved', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| repository.updateStatus.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.handle(event); | ||
|
|
||
| // Validate: Assert | ||
| const [transactionId, statusId] = repository.updateStatus.mock.calls[0]; | ||
| expect(transactionId).toBe('transaction-mapping-approved'); | ||
| expect(statusId).toBe(2); // approved | ||
| }); | ||
|
|
||
| it('debería manejar correctamente el mapeo de status rejected a statusId 3', async () => { | ||
| // Prepare: Arrange rejected event | ||
| const event: TransactionValidatedEvent = { | ||
| eventId: 'event-mapping-rejected', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-mapping-rejected', | ||
| data: { | ||
| transactionExternalId: 'transaction-mapping-rejected', | ||
| status: 'rejected', | ||
| reason: 'Amount exceeds limit', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| repository.updateStatus.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.handle(event); | ||
|
|
||
| // Validate: Assert | ||
| const [transactionId, statusId] = repository.updateStatus.mock.calls[0]; | ||
| expect(transactionId).toBe('transaction-mapping-rejected'); | ||
| expect(statusId).toBe(3); // rejected | ||
| }); | ||
|
|
||
| it('debería manejar eventos con campo reason opcional', async () => { | ||
| // Prepare: Arrange approved event without reason | ||
| const eventWithoutReason: TransactionValidatedEvent = { | ||
| eventId: 'event-no-reason', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-no-reason', | ||
| data: { | ||
| transactionExternalId: 'transaction-no-reason', | ||
| status: 'approved', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| repository.updateStatus.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.handle(eventWithoutReason); | ||
|
|
||
| // Validate: Assert | ||
| expect(repository.updateStatus).toHaveBeenCalledWith('transaction-no-reason', 2); | ||
|
|
||
| // Prepare: Arrange rejected event with reason | ||
| const eventWithReason: TransactionValidatedEvent = { | ||
| eventId: 'event-with-reason', | ||
| eventType: 'transaction.validated', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-with-reason', | ||
| data: { | ||
| transactionExternalId: 'transaction-with-reason', | ||
| status: 'rejected', | ||
| reason: 'Fraud detected', | ||
| validatedAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| repository.updateStatus.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.handle(eventWithReason); | ||
|
|
||
| // Validate: Assert | ||
| expect(repository.updateStatus).toHaveBeenCalledWith('transaction-with-reason', 3); | ||
| }); | ||
| }); | ||
| }); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test descriptions are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. All test descriptions should be rewritten in English for consistency with the codebase standards.
| it('debería crear una transacción con estado pending y publicar evento a Kafka', async () => { | ||
| // Prepare: Arrange test data | ||
| const command = new CreateTransactionCommand( | ||
| '550e8400-e29b-41d4-a716-446655440000', | ||
| '550e8400-e29b-41d4-a716-446655440001', | ||
| 1, | ||
| 500, | ||
| ); | ||
|
|
||
| const mockTransaction = { | ||
| transactionExternalId: 'mock-uuid-transaction', | ||
| accountExternalIdDebit: command.accountExternalIdDebit, | ||
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| createdAt: new Date('2024-01-15T10:00:00Z'), | ||
| transactionType: { | ||
| name: 'Transfer', | ||
| }, | ||
| transactionStatus: { | ||
| name: 'pending', | ||
| }, | ||
| }; | ||
|
|
||
| repository.create.mockResolvedValue(mockTransaction as any); | ||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| const result = await handler.execute(command); | ||
|
|
||
| // Validate: Assert | ||
| // Verificar que se llamó al repositorio con los datos correctos | ||
| expect(repository.create).toHaveBeenCalledTimes(1); | ||
| expect(repository.create).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| accountExternalIdDebit: command.accountExternalIdDebit, | ||
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| transactionStatusId: 1, // pending | ||
| }), | ||
| ); | ||
|
|
||
| // Verificar que se publicó el evento a Kafka | ||
| expect(kafkaProducer.send).toHaveBeenCalledTimes(1); | ||
| expect(kafkaProducer.send).toHaveBeenCalledWith( | ||
| 'transaction.created', | ||
| expect.objectContaining({ | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| data: expect.objectContaining({ | ||
| transactionExternalId: mockTransaction.transactionExternalId, | ||
| accountExternalIdDebit: command.accountExternalIdDebit, | ||
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| }), | ||
| }), | ||
| ); | ||
|
|
||
| // Verificar la respuesta | ||
| expect(result).toEqual({ | ||
| transactionExternalId: mockTransaction.transactionExternalId, | ||
| transactionType: { | ||
| name: 'Transfer', | ||
| }, | ||
| transactionStatus: { | ||
| name: 'pending', | ||
| }, | ||
| value: 500, | ||
| createdAt: mockTransaction.createdAt, | ||
| }); | ||
| }); | ||
|
|
||
| it('debería manejar transacciones con valores grandes correctamente', async () => { | ||
| // Prepare: Arrange test data with high value | ||
| const command = new CreateTransactionCommand( | ||
| '550e8400-e29b-41d4-a716-446655440000', | ||
| '550e8400-e29b-41d4-a716-446655440001', | ||
| 1, | ||
| 1500, // Valor mayor a 1000 | ||
| ); | ||
|
|
||
| const mockTransaction = { | ||
| transactionExternalId: 'mock-uuid-high-value', | ||
| accountExternalIdDebit: command.accountExternalIdDebit, | ||
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| createdAt: new Date('2024-01-15T10:00:00Z'), | ||
| transactionType: { | ||
| name: 'Transfer', | ||
| }, | ||
| transactionStatus: { | ||
| name: 'pending', | ||
| }, | ||
| }; | ||
|
|
||
| repository.create.mockResolvedValue(mockTransaction as any); | ||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| const result = await handler.execute(command); | ||
|
|
||
| // Validate: Assert | ||
| expect(repository.create).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| value: 1500, | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.value).toBe(1500); | ||
| }); | ||
|
|
||
| it('debería propagar errores del repositorio', async () => { | ||
| // Prepare: Arrange error scenario | ||
| const command = new CreateTransactionCommand( | ||
| '550e8400-e29b-41d4-a716-446655440000', | ||
| '550e8400-e29b-41d4-a716-446655440001', | ||
| 1, | ||
| 500, | ||
| ); | ||
|
|
||
| const dbError = new Error('Database connection failed'); | ||
| repository.create.mockRejectedValue(dbError); | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await expect(handler.execute(command)).rejects.toThrow('Database connection failed'); | ||
|
|
||
| // Verificar que Kafka no se llamó si falla el repositorio | ||
| expect(kafkaProducer.send).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('debería propagar errores de Kafka producer', async () => { | ||
| // Prepare: Arrange Kafka error scenario | ||
| const command = new CreateTransactionCommand( | ||
| '550e8400-e29b-41d4-a716-446655440000', | ||
| '550e8400-e29b-41d4-a716-446655440001', | ||
| 1, | ||
| 500, | ||
| ); | ||
|
|
||
| const mockTransaction = { | ||
| transactionExternalId: 'mock-uuid-kafka-error', | ||
| accountExternalIdDebit: command.accountExternalIdDebit, | ||
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| createdAt: new Date('2024-01-15T10:00:00Z'), | ||
| transactionType: { | ||
| name: 'Transfer', | ||
| }, | ||
| transactionStatus: { | ||
| name: 'pending', | ||
| }, | ||
| }; | ||
|
|
||
| repository.create.mockResolvedValue(mockTransaction as any); | ||
| const kafkaError = new Error('Kafka broker not available'); | ||
| kafkaProducer.send.mockRejectedValue(kafkaError); | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await expect(handler.execute(command)).rejects.toThrow('Kafka broker not available'); | ||
|
|
||
| // Verificar que el repositorio sí se llamó | ||
| expect(repository.create).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('debería incluir eventId y correlationId en el evento publicado', async () => { | ||
| // Prepare: Arrange test data | ||
| const command = new CreateTransactionCommand( | ||
| '550e8400-e29b-41d4-a716-446655440000', | ||
| '550e8400-e29b-41d4-a716-446655440001', | ||
| 1, | ||
| 500, | ||
| ); | ||
|
|
||
| const mockTransaction = { | ||
| transactionExternalId: 'mock-uuid-event-ids', | ||
| accountExternalIdDebit: command.accountExternalIdDebit, | ||
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| createdAt: new Date('2024-01-15T10:00:00Z'), | ||
| transactionType: { | ||
| name: 'Transfer', | ||
| }, | ||
| transactionStatus: { | ||
| name: 'pending', | ||
| }, | ||
| }; | ||
|
|
||
| repository.create.mockResolvedValue(mockTransaction as any); | ||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await handler.execute(command); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionCreatedEvent; | ||
|
|
||
| expect(publishedEvent.eventId).toBeDefined(); | ||
| expect(publishedEvent.eventId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); | ||
|
|
||
| expect(publishedEvent.correlationId).toBeDefined(); | ||
| expect(publishedEvent.correlationId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); | ||
|
|
||
| expect(publishedEvent.timestamp).toBeDefined(); | ||
| expect(new Date(publishedEvent.timestamp)).toBeInstanceOf(Date); | ||
| }); | ||
| }); | ||
| }); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test descriptions are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. All test descriptions should be rewritten in English for consistency with the codebase standards.
| it('debería aprobar transacciones con valor menor o igual a 1000', async () => { | ||
| // Prepare: Arrange transaction with value <= 1000 | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-approved', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-approved', | ||
| data: { | ||
| transactionExternalId: 'transaction-approved', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 500, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| expect(kafkaProducer.send).toHaveBeenCalledTimes(1); | ||
| expect(kafkaProducer.send).toHaveBeenCalledWith( | ||
| 'transaction.validated', | ||
| expect.objectContaining({ | ||
| eventType: 'transaction.validated', | ||
| correlationId: 'correlation-approved', | ||
| data: expect.objectContaining({ | ||
| transactionExternalId: 'transaction-approved', | ||
| status: 'approved', | ||
| reason: undefined, | ||
| }), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('debería aprobar transacciones con valor exactamente igual a 1000', async () => { | ||
| // Prepare: Arrange transaction with value = 1000 (boundary case) | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-boundary', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-boundary', | ||
| data: { | ||
| transactionExternalId: 'transaction-boundary', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 1000, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionValidatedEvent; | ||
| expect(publishedEvent.data.status).toBe('approved'); | ||
| expect(publishedEvent.data.reason).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('debería rechazar transacciones con valor mayor a 1000', async () => { | ||
| // Prepare: Arrange transaction with value > 1000 | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-rejected', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-rejected', | ||
| data: { | ||
| transactionExternalId: 'transaction-rejected', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440002', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440003', | ||
| transferTypeId: 1, | ||
| value: 1500, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| expect(kafkaProducer.send).toHaveBeenCalledTimes(1); | ||
| expect(kafkaProducer.send).toHaveBeenCalledWith( | ||
| 'transaction.validated', | ||
| expect.objectContaining({ | ||
| eventType: 'transaction.validated', | ||
| correlationId: 'correlation-rejected', | ||
| data: expect.objectContaining({ | ||
| transactionExternalId: 'transaction-rejected', | ||
| status: 'rejected', | ||
| reason: 'Transaction amount 1500 exceeds threshold 1000', | ||
| }), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it('debería rechazar transacciones con valor inmediatamente superior a 1000', async () => { | ||
| // Prepare: Arrange transaction with value = 1001 (boundary case) | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-boundary-rejected', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-boundary-rejected', | ||
| data: { | ||
| transactionExternalId: 'transaction-boundary-rejected', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440004', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440005', | ||
| transferTypeId: 1, | ||
| value: 1001, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionValidatedEvent; | ||
| expect(publishedEvent.data.status).toBe('rejected'); | ||
| expect(publishedEvent.data.reason).toBe('Transaction amount 1001 exceeds threshold 1000'); | ||
| }); | ||
|
|
||
| it('debería incluir eventId y timestamp en el evento publicado', async () => { | ||
| // Prepare: Arrange transaction | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-metadata', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-metadata', | ||
| data: { | ||
| transactionExternalId: 'transaction-metadata', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 750, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionValidatedEvent; | ||
|
|
||
| expect(publishedEvent.eventId).toBeDefined(); | ||
| expect(publishedEvent.eventId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); | ||
|
|
||
| expect(publishedEvent.timestamp).toBeDefined(); | ||
| expect(new Date(publishedEvent.timestamp)).toBeInstanceOf(Date); | ||
|
|
||
| expect(publishedEvent.eventVersion).toBe('1.0'); | ||
| }); | ||
|
|
||
| it('debería preservar el correlationId del evento original', async () => { | ||
| // Prepare: Arrange transaction with specific correlationId | ||
| const originalCorrelationId = 'correlation-preserve-test'; | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-correlation', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: originalCorrelationId, | ||
| data: { | ||
| transactionExternalId: 'transaction-correlation', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 300, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionValidatedEvent; | ||
| expect(publishedEvent.correlationId).toBe(originalCorrelationId); | ||
| }); | ||
|
|
||
| it('debería propagar errores del KafkaProducer', async () => { | ||
| // Prepare: Arrange Kafka error scenario | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-kafka-error', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-kafka-error', | ||
| data: { | ||
| transactionExternalId: 'transaction-kafka-error', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 500, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| const kafkaError = new Error('Failed to send message to Kafka'); | ||
| kafkaProducer.send.mockRejectedValue(kafkaError); | ||
|
|
||
| // Execute & Validate: Act & Assert | ||
| await expect(service.validateTransaction(event)).rejects.toThrow('Failed to send message to Kafka'); | ||
| }); | ||
|
|
||
| it('debería incluir validatedAt timestamp en el evento de validación', async () => { | ||
| // Prepare: Arrange transaction | ||
| const beforeValidation = new Date(); | ||
|
|
||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-validated-at', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-validated-at', | ||
| data: { | ||
| transactionExternalId: 'transaction-validated-at', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 250, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| const afterValidation = new Date(); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionValidatedEvent; | ||
|
|
||
| expect(publishedEvent.data.validatedAt).toBeDefined(); | ||
| const validatedAt = new Date(publishedEvent.data.validatedAt); | ||
| expect(validatedAt).toBeInstanceOf(Date); | ||
| expect(validatedAt.getTime()).toBeGreaterThanOrEqual(beforeValidation.getTime()); | ||
| expect(validatedAt.getTime()).toBeLessThanOrEqual(afterValidation.getTime()); | ||
| }); | ||
|
|
||
| it('debería manejar transacciones con valores decimales', async () => { | ||
| // Prepare: Arrange transaction with decimal value | ||
| const event: TransactionCreatedEvent = { | ||
| eventId: 'event-decimal', | ||
| eventType: 'transaction.created', | ||
| eventVersion: '1.0', | ||
| timestamp: '2024-01-15T10:00:00Z', | ||
| correlationId: 'correlation-decimal', | ||
| data: { | ||
| transactionExternalId: 'transaction-decimal', | ||
| accountExternalIdDebit: '550e8400-e29b-41d4-a716-446655440000', | ||
| accountExternalIdCredit: '550e8400-e29b-41d4-a716-446655440001', | ||
| transferTypeId: 1, | ||
| value: 999.99, | ||
| createdAt: '2024-01-15T10:00:00Z', | ||
| }, | ||
| }; | ||
|
|
||
| kafkaProducer.send.mockResolvedValue(undefined); | ||
|
|
||
| // Execute: Act | ||
| await service.validateTransaction(event); | ||
|
|
||
| // Validate: Assert | ||
| const publishedEvent = kafkaProducer.send.mock.calls[0][1] as TransactionValidatedEvent; | ||
| expect(publishedEvent.data.status).toBe('approved'); | ||
| }); | ||
| }); | ||
| }); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test descriptions are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. All test descriptions should be rewritten in English for consistency with the codebase standards.
| // Mapear status string a ID | ||
| const statusId = event.data.status === 'approved' ? 2 : 3; | ||
|
|
||
| // Actualizar status en DB |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. Comments should be translated to English for consistency.
| ? undefined | ||
| : `Transaction amount ${data.value} exceeds threshold ${this.AMOUNT_THRESHOLD}`; | ||
|
|
||
| // Crear evento de validación |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. Comments should be translated to English for consistency.
|
|
||
| this.logger.log(`Processing transaction ${event.data.transactionExternalId}`); | ||
|
|
||
| // Validar transacción |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. Comments should be translated to English for consistency.
| // Transaction Types (datos de referencia) | ||
| model TransactionType { | ||
| id Int @id @default(autoincrement()) | ||
| name String @unique @db.VarChar(50) | ||
| description String? | ||
| createdAt DateTime @default(now()) | ||
| transactions Transaction[] | ||
|
|
||
| @@map("transaction_types") | ||
| } | ||
|
|
||
| // Transaction Statuses (pending, approved, rejected) | ||
| model TransactionStatus { | ||
| id Int @id @default(autoincrement()) | ||
| name String @unique @db.VarChar(50) | ||
| description String? | ||
| createdAt DateTime @default(now()) | ||
| transactions Transaction[] | ||
|
|
||
| @@map("transaction_statuses") | ||
| } | ||
|
|
||
| // Entidad principal: Transaction |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments are written in Spanish, which violates the coding guideline requiring all code, comments, and variable names to be in English. Comments should be translated to English for consistency.
| accountExternalIdCredit: command.accountExternalIdCredit, | ||
| transferTypeId: command.transferTypeId, | ||
| value: command.value, | ||
| transactionStatusId: 1, // pending |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded status ID (1 for pending) is fragile and could break if database seed data changes. Consider fetching status IDs from the database or using a constant mapping to make the code more maintainable and less prone to errors.
Solution for tech challenge