import request from 'supertest'; import app from '@/app'; import { TestHelpers } from '../utils/test-helpers'; import { PaymentType, Currency, PaymentStatus } from '@/models/payment'; import { PaymentRequest } from '@/gateway/validation/payment-validation'; describe('E2E Payment Workflow', () => { let makerToken: string; let checkerToken: string; let makerOperator: any; let checkerOperator: any; beforeAll(async () => { // Clean database first (non-blocking) TestHelpers.cleanDatabase().catch(() => {}); // Fire and forget // Create test operators with timeout protection const operatorPromises = [ TestHelpers.createTestOperator('E2E_MAKER', 'MAKER' as any, 'Test123!@#'), TestHelpers.createTestOperator('E2E_CHECKER', 'CHECKER' as any, 'Test123!@#') ]; [makerOperator, checkerOperator] = await Promise.all(operatorPromises); // Generate tokens makerToken = TestHelpers.generateTestToken( makerOperator.operatorId, makerOperator.id, makerOperator.role ); checkerToken = TestHelpers.generateTestToken( checkerOperator.operatorId, checkerOperator.id, checkerOperator.role ); }, 90000); afterAll(async () => { // Fast cleanup with timeout protection await Promise.race([ TestHelpers.cleanDatabase(), new Promise(resolve => setTimeout(resolve, 5000)) ]); }, 30000); beforeEach(async () => { // Skip cleanup in beforeEach to speed up tests // Tests should clean up their own data }); describe('Complete Payment Flow', () => { it('should complete full payment workflow: initiate → approve → process', async () => { // Step 1: Maker initiates payment const paymentRequest: PaymentRequest = { type: PaymentType.CUSTOMER_CREDIT_TRANSFER, amount: 1000.50, currency: Currency.USD, senderAccount: 'ACC001', senderBIC: 'TESTBIC1', receiverAccount: 'ACC002', receiverBIC: 'TESTBIC2', beneficiaryName: 'E2E Test Beneficiary', purpose: 'E2E test payment', }; const initiateResponse = await request(app) .post('/api/v1/payments') .set('Authorization', `Bearer ${makerToken}`) .send(paymentRequest) .expect(201); expect(initiateResponse.body.paymentId).toBeDefined(); expect(initiateResponse.body.status).toBe(PaymentStatus.PENDING_APPROVAL); const paymentId = initiateResponse.body.paymentId; // Step 2: Checker approves payment const approveResponse = await request(app) .post(`/api/v1/payments/${paymentId}/approve`) .set('Authorization', `Bearer ${checkerToken}`) .expect(200); expect(approveResponse.body.message).toContain('approved'); // Step 3: Verify payment status updated // Note: Processing happens asynchronously, so we check status const statusResponse = await request(app) .get(`/api/v1/payments/${paymentId}`) .set('Authorization', `Bearer ${makerToken}`) .expect(200); expect(statusResponse.body.paymentId).toBe(paymentId); expect(statusResponse.body.status).toBeDefined(); }); it('should reject payment when checker rejects', async () => { const paymentRequest: PaymentRequest = { type: PaymentType.CUSTOMER_CREDIT_TRANSFER, amount: 2000, currency: Currency.USD, senderAccount: 'ACC003', senderBIC: 'TESTBIC3', receiverAccount: 'ACC004', receiverBIC: 'TESTBIC4', beneficiaryName: 'Test Beneficiary Reject', }; const initiateResponse = await request(app) .post('/api/v1/payments') .set('Authorization', `Bearer ${makerToken}`) .send(paymentRequest) .expect(201); const paymentId = initiateResponse.body.paymentId; // Checker rejects payment const rejectResponse = await request(app) .post(`/api/v1/payments/${paymentId}/reject`) .set('Authorization', `Bearer ${checkerToken}`) .send({ reason: 'E2E test rejection' }) .expect(200); expect(rejectResponse.body.message).toContain('rejected'); // Verify status const statusResponse = await request(app) .get(`/api/v1/payments/${paymentId}`) .set('Authorization', `Bearer ${makerToken}`) .expect(200); expect(['REJECTED', 'CANCELLED']).toContain(statusResponse.body.status); }); it('should enforce dual control - maker cannot approve', async () => { const paymentRequest: PaymentRequest = { type: PaymentType.CUSTOMER_CREDIT_TRANSFER, amount: 3000, currency: Currency.EUR, senderAccount: 'ACC005', senderBIC: 'TESTBIC5', receiverAccount: 'ACC006', receiverBIC: 'TESTBIC6', beneficiaryName: 'Test Dual Control', }; const initiateResponse = await request(app) .post('/api/v1/payments') .set('Authorization', `Bearer ${makerToken}`) .send(paymentRequest) .expect(201); const paymentId = initiateResponse.body.paymentId; // Maker tries to approve - should fail await request(app) .post(`/api/v1/payments/${paymentId}/approve`) .set('Authorization', `Bearer ${makerToken}`) .expect(403); // Forbidden }); it('should allow maker to cancel before approval', async () => { const paymentRequest: PaymentRequest = { type: PaymentType.CUSTOMER_CREDIT_TRANSFER, amount: 4000, currency: Currency.GBP, senderAccount: 'ACC007', senderBIC: 'TESTBIC7', receiverAccount: 'ACC008', receiverBIC: 'TESTBIC8', beneficiaryName: 'Test Cancellation', }; const initiateResponse = await request(app) .post('/api/v1/payments') .set('Authorization', `Bearer ${makerToken}`) .send(paymentRequest) .expect(201); const paymentId = initiateResponse.body.paymentId; // Maker cancels payment const cancelResponse = await request(app) .post(`/api/v1/payments/${paymentId}/cancel`) .set('Authorization', `Bearer ${makerToken}`) .send({ reason: 'E2E test cancellation' }) .expect(200); expect(cancelResponse.body.message).toContain('cancelled'); }); }); describe('Payment Listing', () => { it('should list payments with pagination', async () => { // Create multiple payments for (let i = 0; i < 3; i++) { const paymentRequest: PaymentRequest = { type: PaymentType.CUSTOMER_CREDIT_TRANSFER, amount: 1000 + i * 100, currency: Currency.USD, senderAccount: `ACC${i}`, senderBIC: `TESTBIC${i}`, receiverAccount: `ACCR${i}`, receiverBIC: `TESTBICR${i}`, beneficiaryName: `Beneficiary ${i}`, }; await request(app) .post('/api/v1/payments') .set('Authorization', `Bearer ${makerToken}`) .send(paymentRequest) .expect(201); } const listResponse = await request(app) .get('/api/v1/payments?limit=2&offset=0') .set('Authorization', `Bearer ${makerToken}`) .expect(200); expect(listResponse.body.payments).toBeDefined(); expect(listResponse.body.payments.length).toBeLessThanOrEqual(2); }); }); });