import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import request from 'supertest'; import { AppModule } from '../../app.module'; import { PrismaService } from '../../common/prisma.service'; describe('Payments E2E', () => { let app: INestApplication; let prisma: PrismaService; let authToken: string; let bookingId: string; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); await app.init(); prisma = app.get(PrismaService); const testUser = await prisma.user.create({ data: { email: 'payment-test@example.com', phone: '+251911111112', fullName: 'Payment Test User', passwordHash: '$2b$10$abcdefghijklmnopqrstuvwxyz', role: 'PASSENGER' }, }); const passenger = await prisma.passenger.create({ data: { userId: testUser.id } }); await prisma.walletAccount.create({ data: { passengerId: passenger.id, balanceMinor: 100000, currency: 'ETB' } }); authToken = 'mock-jwt-token'; const station1 = await prisma.station.create({ data: { code: 'TST1', name: 'Test Station 1', city: 'Test City', lat: 9.0, lng: 38.0 } }); const station2 = await prisma.station.create({ data: { code: 'TST2', name: 'Test Station 2', city: 'Test City 2', lat: 9.5, lng: 38.5 } }); const train = await prisma.train.create({ data: { number: 'TEST-001', name: 'Test Train' } }); const schedule = await prisma.trainSchedule.create({ data: { trainId: train.id, originStationId: station1.id, destinationStationId: station2.id, departureAt: new Date(Date.now() + 86400000), arrivalAt: new Date(Date.now() + 90000000), durationMinutes: 60 }, }); const seatClass = await prisma.seatClass.upsert({ where: { name: 'Economy Regular' }, update: {}, create: { name: 'Economy Regular', description: 'Standard economy seating', basePrice: 45000, isActive: true }, }); const coach = await prisma.coach.create({ data: { coachNumber: 'TEST-C1', label: 'A', seatClassId: seatClass.id, mode: 'seat', totalUnits: 10 }, }); await prisma.coachAssignment.create({ data: { scheduleId: schedule.id, coachId: coach.id, positionNumber: 1 } }); const seat = await prisma.seat.create({ data: { coachId: coach.id, row: 1, col: 'A', label: '1A', status: 'AVAILABLE' } }); const booking = await prisma.booking.create({ data: { bookingRef: 'TEST-BOOK-001', passengerId: passenger.id, scheduleId: schedule.id, status: 'PENDING_PAYMENT', totalMinor: 50000, currency: 'ETB' }, }); await prisma.bookingSeat.create({ data: { bookingId: booking.id, seatId: seat.id, passengerName: 'Test Passenger' } }); bookingId = booking.id; }); afterAll(async () => { await prisma.$transaction([ prisma.bookingSeat.deleteMany(), prisma.paymentIntent.deleteMany(), prisma.booking.deleteMany(), prisma.coachAssignment.deleteMany(), prisma.seat.deleteMany(), prisma.coach.deleteMany(), prisma.trainSchedule.deleteMany(), prisma.train.deleteMany(), prisma.station.deleteMany({ where: { code: { in: ['TST1', 'TST2'] } } }), prisma.walletLedgerEntry.deleteMany(), prisma.walletAccount.deleteMany(), prisma.passenger.deleteMany(), prisma.user.deleteMany({ where: { email: 'payment-test@example.com' } }), ]); await app.close(); }); describe('POST /payments/initiate', () => { it('should initiate wallet payment successfully', async () => { const response = await request(app.getHttpServer()) .post('/payments/initiate') .set('Authorization', `Bearer ${authToken}`) .send({ bookingId, method: 'WALLET' }) .expect(201); expect(response.body.intentId).toBeDefined(); expect(response.body.status).toBe('SUCCEEDED'); }); it('should return 400 for invalid payment method', async () => { await request(app.getHttpServer()) .post('/payments/initiate') .set('Authorization', `Bearer ${authToken}`) .send({ bookingId, method: 'INVALID_METHOD' }) .expect(400); }); it('should return 404 for non-existent booking', async () => { await request(app.getHttpServer()) .post('/payments/initiate') .set('Authorization', `Bearer ${authToken}`) .send({ bookingId: 'non-existent-id', method: 'WALLET' }) .expect(404); }); }); describe('GET /payments/intents/:bookingId', () => { it('should get payment intent status', async () => { const response = await request(app.getHttpServer()) .get(`/payments/intents/${bookingId}`) .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body.intentId).toBeDefined(); expect(response.body.status).toBeDefined(); }); it('should return 404 for non-existent intent', async () => { await request(app.getHttpServer()) .get('/payments/intents/non-existent-booking') .set('Authorization', `Bearer ${authToken}`) .expect(404); }); }); describe('Webhook endpoints', () => { it('should handle Telebirr webhook', async () => { await request(app.getHttpServer()) .post('/payments/webhooks/telebirr') .send({ merch_order_id: 'TEST-ORDER-123', payment_order_id: 'PAY-123', trade_status: 'Completed', sign: 'mock-signature' }) .expect(200); }); it('should handle CBE Birr webhook', async () => { await request(app.getHttpServer()) .post('/payments/webhooks/cbe-birr') .send({ merchantId: 'TEST-MERCHANT', merchantOrderId: 'TEST-ORDER-123', orderId: 'CBE-ORDER-123', status: 'SUCCESS', signature: 'mock-signature' }) .expect(200); }); it('should handle eBirr webhook', async () => { await request(app.getHttpServer()) .post('/payments/webhooks/ebirr') .send({ merchantCode: 'TEST-MERCHANT', orderNo: 'TEST-ORDER-123', tradeStatus: 'TRADE_SUCCESS', timestamp: Date.now(), sign: 'mock-signature' }) .expect(200); }); it('should handle Card webhook', async () => { await request(app.getHttpServer()) .post('/payments/webhooks/card') .set('stripe-signature', 'mock-signature') .send({ id: 'evt_123', type: 'payment_intent.succeeded', data: { object: { id: 'pi_123', status: 'succeeded', amount: 50000, currency: 'ETB', metadata: { merchantOrderId: 'TEST-ORDER-123', bookingRef: 'TEST-BOOK-001' } } }, created: Math.floor(Date.now() / 1000) }) .expect(200); }); }); });