diff --git a/server/api/helcim/payment-history.get.js b/server/api/helcim/payment-history.get.js new file mode 100644 index 0000000..bbf7075 --- /dev/null +++ b/server/api/helcim/payment-history.get.js @@ -0,0 +1,35 @@ +// Return the authenticated member's recent Helcim card transactions. +// No status gate — historical reads are safe for any auth'd status. +// On Helcim errors, returns { transactions: [], error: 'unavailable' } (HTTP 200) +// so the client can render fallback copy without a crash. +import { requireAuth } from '../../utils/auth.js' +import { getHelcimCustomer, listHelcimCustomerTransactions } from '../../utils/helcim.js' + +export default defineEventHandler(async (event) => { + const member = await requireAuth(event) + + if (!member.helcimCustomerId) { + return { transactions: [] } + } + + try { + const customerData = await getHelcimCustomer(member.helcimCustomerId) + const customerCode = customerData?.customerCode + + if (!customerCode) { + console.error('[payment-history] Helcim customer missing customerCode', { + helcimCustomerId: member.helcimCustomerId + }) + return { transactions: [], error: 'unavailable' } + } + + const transactions = await listHelcimCustomerTransactions(customerCode) + return { transactions } + } catch (error) { + console.error('[payment-history] Helcim lookup failed', { + helcimCustomerId: member.helcimCustomerId, + error: error?.message || error + }) + return { transactions: [], error: 'unavailable' } + } +}) diff --git a/tests/server/api/helcim-payment-history.test.js b/tests/server/api/helcim-payment-history.test.js new file mode 100644 index 0000000..d4ddebf --- /dev/null +++ b/tests/server/api/helcim-payment-history.test.js @@ -0,0 +1,118 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +import { requireAuth } from '../../../server/utils/auth.js' +import { getHelcimCustomer, listHelcimCustomerTransactions } from '../../../server/utils/helcim.js' +import paymentHistoryHandler from '../../../server/api/helcim/payment-history.get.js' +import { createMockEvent } from '../helpers/createMockEvent.js' + +vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() })) +vi.mock('../../../server/utils/helcim.js', () => ({ + getHelcimCustomer: vi.fn(), + listHelcimCustomerTransactions: vi.fn() +})) + +describe('helcim payment-history endpoint', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns 401 when unauthenticated', async () => { + requireAuth.mockRejectedValue( + createError({ statusCode: 401, statusMessage: 'Unauthorized' }) + ) + + const event = createMockEvent({ + method: 'GET', + path: '/api/helcim/payment-history' + }) + + await expect(paymentHistoryHandler(event)).rejects.toMatchObject({ + statusCode: 401 + }) + + expect(getHelcimCustomer).not.toHaveBeenCalled() + expect(listHelcimCustomerTransactions).not.toHaveBeenCalled() + }) + + it('returns empty transactions when member has no helcimCustomerId', async () => { + requireAuth.mockResolvedValue({ helcimCustomerId: null }) + + const event = createMockEvent({ + method: 'GET', + path: '/api/helcim/payment-history' + }) + + const result = await paymentHistoryHandler(event) + + expect(result).toEqual({ transactions: [] }) + expect(getHelcimCustomer).not.toHaveBeenCalled() + expect(listHelcimCustomerTransactions).not.toHaveBeenCalled() + }) + + it('returns normalized transactions for authenticated member with helcimCustomerId', async () => { + requireAuth.mockResolvedValue({ helcimCustomerId: 9876 }) + getHelcimCustomer.mockResolvedValue({ id: 9876, customerCode: 'CST1044' }) + + const transactions = [ + { id: 't1', date: '2026-04-18T00:00:00Z', amount: 15, status: 'paid', currency: 'CAD' }, + { id: 't2', date: '2026-03-18T00:00:00Z', amount: 15, status: 'paid', currency: 'CAD' } + ] + listHelcimCustomerTransactions.mockResolvedValue(transactions) + + const event = createMockEvent({ + method: 'GET', + path: '/api/helcim/payment-history' + }) + + const result = await paymentHistoryHandler(event) + + expect(result).toEqual({ transactions }) + expect(getHelcimCustomer).toHaveBeenCalledWith(9876) + expect(listHelcimCustomerTransactions).toHaveBeenCalledWith('CST1044') + }) + + it('returns { transactions: [], error: "unavailable" } when listHelcimCustomerTransactions throws', async () => { + requireAuth.mockResolvedValue({ helcimCustomerId: 9876 }) + getHelcimCustomer.mockResolvedValue({ id: 9876, customerCode: 'CST1044' }) + listHelcimCustomerTransactions.mockRejectedValue(new Error('Transaction lookup failed')) + + const event = createMockEvent({ + method: 'GET', + path: '/api/helcim/payment-history' + }) + + const result = await paymentHistoryHandler(event) + + expect(result).toEqual({ transactions: [], error: 'unavailable' }) + }) + + it('returns { transactions: [], error: "unavailable" } when getHelcimCustomer throws', async () => { + requireAuth.mockResolvedValue({ helcimCustomerId: 9876 }) + getHelcimCustomer.mockRejectedValue(new Error('Customer lookup failed')) + + const event = createMockEvent({ + method: 'GET', + path: '/api/helcim/payment-history' + }) + + const result = await paymentHistoryHandler(event) + + expect(result).toEqual({ transactions: [], error: 'unavailable' }) + expect(listHelcimCustomerTransactions).not.toHaveBeenCalled() + }) + + it('returns unavailable when customerCode is missing from Helcim response', async () => { + requireAuth.mockResolvedValue({ helcimCustomerId: 9876 }) + getHelcimCustomer.mockResolvedValue({ id: 9876 }) + + const event = createMockEvent({ + method: 'GET', + path: '/api/helcim/payment-history' + }) + + const result = await paymentHistoryHandler(event) + + expect(result).toEqual({ transactions: [], error: 'unavailable' }) + expect(listHelcimCustomerTransactions).not.toHaveBeenCalled() + }) +})