import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { StripeEvent } from '../../src/types/payments'; const { mockPool } = vi.hoisted(() => ({ mockPool: { query: vi.fn(), }, })); vi.mock('../../src/infra/database/database.manager', () => ({ DatabaseManager: { getInstance: () => ({ getPool: () => mockPool, }), }, })); import { StripeWebhookService } from '../../src/services/payments/stripe/webhook.service'; describe('StripeWebhookService', () => { beforeEach(() => { vi.clearAllMocks(); mockPool.query.mockReset(); }); it('does not reprocess duplicate webhook events that are still pending', async () => { const freshPendingUpdatedAt = new Date(); mockPool.query .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [ { environment: 'test', eventId: 'evt_123', eventType: 'checkout.session.completed', livemode: false, accountId: null, objectType: 'checkout.session', objectId: 'cs_test_123', processingStatus: 'pending', attemptCount: 1, lastError: null, receivedAt: new Date('2026-04-30T00:00:00.000Z'), processedAt: null, createdAt: new Date('2026-04-30T00:00:00.000Z'), updatedAt: freshPendingUpdatedAt, }, ], }); const result = await StripeWebhookService.getInstance().recordWebhookEventStart('test', { id: 'evt_123', type: 'checkout.session.completed', livemode: false, account: null, data: { object: { id: 'cs_test_123', object: 'checkout.session', }, }, } as StripeEvent); expect(result).toMatchObject({ shouldProcess: false, row: { eventId: 'evt_123', processingStatus: 'pending', }, }); expect( mockPool.query.mock.calls.some(([sql]) => /UPDATE payments\.webhook_events/i.test(String(sql)) ) ).toBe(true); expect(mockPool.query).toHaveBeenNthCalledWith( 2, expect.stringMatching( /UPDATE payments\.webhook_events[\s\S]*processing_status = 'failed'[\s\S]*OR \(processing_status = 'pending' AND updated_at < \$5\)/i ), ['stripe', 'test', 'evt_123', expect.any(Object), expect.any(Date)] ); }); it('reclaims stale pending webhook events for retry', async () => { const reclaimedUpdatedAt = new Date(); mockPool.query.mockResolvedValueOnce({ rows: [] }).mockResolvedValueOnce({ rows: [ { environment: 'test', eventId: 'evt_123', eventType: 'checkout.session.completed', livemode: false, accountId: null, objectType: 'checkout.session', objectId: 'cs_test_123', processingStatus: 'pending', attemptCount: 2, lastError: null, receivedAt: new Date('2026-04-30T00:00:00.000Z'), processedAt: null, createdAt: new Date('2026-04-30T00:00:00.000Z'), updatedAt: reclaimedUpdatedAt, }, ], }); const result = await StripeWebhookService.getInstance().recordWebhookEventStart('test', { id: 'evt_123', type: 'checkout.session.completed', livemode: false, account: null, data: { object: { id: 'cs_test_123', object: 'checkout.session', }, }, } as StripeEvent); expect(result).toMatchObject({ shouldProcess: true, row: { eventId: 'evt_123', processingStatus: 'pending', attemptCount: 2, }, }); expect(mockPool.query).toHaveBeenCalledTimes(2); }); });