CognxSafeTrack
feat(billing): complete billing system, push notifications, and tech debt fixes
8280d7d | import { describe, it, expect, vi, beforeEach } from 'vitest'; | |
| // Mock infrastructure | |
| vi.mock('../src/services/prisma', () => ({ | |
| prisma: { | |
| payment: { findFirst: vi.fn(), update: vi.fn(), create: vi.fn() }, | |
| enrollment: { create: vi.fn(), findFirst: vi.fn() }, | |
| organization: { findUnique: vi.fn() }, | |
| }, | |
| })); | |
| vi.mock('../src/services/queue', () => ({ | |
| scheduleEmail: vi.fn(), | |
| whatsappQueue: { add: vi.fn() }, | |
| notificationQueue: { add: vi.fn() }, | |
| })); | |
| import { prisma } from '../src/services/prisma'; | |
| // βββ Webhook payload normalization ββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Payment webhook β payload parsing', () => { | |
| beforeEach(() => vi.clearAllMocks()); | |
| it('Wave: extracts sessionId from client_reference', () => { | |
| const payload = { id: 'sess_123', status: 'complete', client_reference: 'sess_123' }; | |
| const sessionId = payload.client_reference ?? payload.id; | |
| const isSuccess = payload.status === 'complete'; | |
| expect(sessionId).toBe('sess_123'); | |
| expect(isSuccess).toBe(true); | |
| }); | |
| it('Wave: marks failed payment as FAILED', () => { | |
| const payload = { id: 'sess_456', status: 'failed', client_reference: 'sess_456' }; | |
| const isSuccess = payload.status === 'complete'; | |
| expect(isSuccess).toBe(false); | |
| }); | |
| it('Orange Money: extracts sessionId from reference', () => { | |
| const payload = { status: 'SUCCESS', txnid: 'om_789', reference: 'om_789' }; | |
| const sessionId = payload.reference ?? payload.txnid; | |
| const isSuccess = payload.status === 'SUCCESS'; | |
| expect(sessionId).toBe('om_789'); | |
| expect(isSuccess).toBe(true); | |
| }); | |
| it('Orange Money: marks FAILED status as not success', () => { | |
| const payload = { status: 'FAILED', txnid: 'om_999', reference: 'om_999' }; | |
| const isSuccess = payload.status === 'SUCCESS'; | |
| expect(isSuccess).toBe(false); | |
| }); | |
| }); | |
| // βββ Payment state transitions ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Payment β state machine', () => { | |
| it('PENDING β COMPLETED on successful webhook', () => { | |
| const payment = { id: 'pay_1', status: 'PENDING', userId: 'user_1', trackId: 'track_1' }; | |
| const updated = { ...payment, status: 'COMPLETED' }; | |
| expect(updated.status).toBe('COMPLETED'); | |
| }); | |
| it('PENDING β FAILED on failed webhook', () => { | |
| const payment = { id: 'pay_2', status: 'PENDING' }; | |
| const updated = { ...payment, status: 'FAILED' }; | |
| expect(updated.status).toBe('FAILED'); | |
| }); | |
| it('does not re-process already COMPLETED payment', () => { | |
| const payment = { status: 'COMPLETED' }; | |
| // Guard: if payment is already COMPLETED, skip | |
| const shouldProcess = payment.status === 'PENDING'; | |
| expect(shouldProcess).toBe(false); | |
| }); | |
| }); | |
| // βββ Enrollment creation logic ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Payment β auto enrollment on success', () => { | |
| beforeEach(() => vi.clearAllMocks()); | |
| it('creates enrollment when payment succeeds and userId + trackId exist', async () => { | |
| const paymentData = { | |
| id: 'pay_1', | |
| status: 'PENDING', | |
| userId: 'user_1', | |
| trackId: 'track_1', | |
| organizationId: 'org_1', | |
| }; | |
| (prisma.payment.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue(paymentData); | |
| (prisma.payment.update as ReturnType<typeof vi.fn>).mockResolvedValue({ ...paymentData, status: 'COMPLETED' }); | |
| (prisma.enrollment.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue(null); | |
| (prisma.enrollment.create as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 'enroll_1' }); | |
| // Simulate webhook handler logic | |
| const payment = await prisma.payment.findFirst({ where: { paymentSessionId: 'sess_1' } as any }); | |
| expect(payment).not.toBeNull(); | |
| await prisma.payment.update({ where: { id: payment!.id }, data: { status: 'COMPLETED' } as any }); | |
| const existingEnrollment = await prisma.enrollment.findFirst({ where: { userId: payment!.userId } as any }); | |
| if (!existingEnrollment && payment!.userId && payment!.trackId) { | |
| await prisma.enrollment.create({ data: { userId: payment!.userId } as any }); | |
| } | |
| expect(prisma.enrollment.create).toHaveBeenCalledTimes(1); | |
| }); | |
| it('skips enrollment creation when userId is missing', async () => { | |
| const paymentData = { id: 'pay_2', status: 'PENDING', userId: null, trackId: 'track_1' }; | |
| (prisma.payment.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue(paymentData); | |
| // Simulate: no userId β no enrollment | |
| const payment = await prisma.payment.findFirst({ where: {} as any }); | |
| const shouldEnroll = !!payment?.userId && !!payment?.trackId; | |
| expect(shouldEnroll).toBe(false); | |
| expect(prisma.enrollment.create).not.toHaveBeenCalled(); | |
| }); | |
| it('skips enrollment if one already exists', async () => { | |
| const paymentData = { id: 'pay_3', status: 'PENDING', userId: 'user_2', trackId: 'track_1' }; | |
| (prisma.payment.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue(paymentData); | |
| (prisma.enrollment.findFirst as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 'existing_enroll' }); | |
| const payment = await prisma.payment.findFirst({ where: {} as any }); | |
| const existingEnrollment = await prisma.enrollment.findFirst({ where: {} as any }); | |
| const shouldCreate = !existingEnrollment; | |
| expect(shouldCreate).toBe(false); | |
| expect(prisma.enrollment.create).not.toHaveBeenCalled(); | |
| }); | |
| }); | |
| // βββ Provider amount validation βββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Payment β provider validation', () => { | |
| it('rejects amount below minimum (1 FCFA)', () => { | |
| const amount = 0; | |
| const isValid = amount > 0; | |
| expect(isValid).toBe(false); | |
| }); | |
| it('accepts valid FCFA amount', () => { | |
| const amount = 20000; | |
| const isValid = amount > 0; | |
| expect(isValid).toBe(true); | |
| }); | |
| it('validates supported providers', () => { | |
| const SUPPORTED = ['WAVE', 'ORANGE_MONEY']; | |
| expect(SUPPORTED.includes('WAVE')).toBe(true); | |
| expect(SUPPORTED.includes('STRIPE')).toBe(false); // deprecated | |
| }); | |
| }); | |