File size: 6,823 Bytes
8280d7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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
    });
});