feat(billing): add payment history API route
Add GET /api/helcim/payment-history returning the authenticated
member's normalized Helcim card transactions (newest first, capped
at 50). Resolves helcimCustomerId -> customerCode via getHelcimCustomer
before calling listHelcimCustomerTransactions. Returns
{ transactions: [] } when the member has no helcimCustomerId, and
{ transactions: [], error: 'unavailable' } (HTTP 200) on Helcim
failures so the UI can render fallback copy.
Covered by unit tests at tests/server/api/helcim-payment-history.test.js
(auth, missing customer id, happy path, both Helcim failure paths,
missing customerCode).
This commit is contained in:
parent
6888663148
commit
4f9c11a755
2 changed files with 153 additions and 0 deletions
35
server/api/helcim/payment-history.get.js
Normal file
35
server/api/helcim/payment-history.get.js
Normal file
|
|
@ -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' }
|
||||||
|
}
|
||||||
|
})
|
||||||
118
tests/server/api/helcim-payment-history.test.js
Normal file
118
tests/server/api/helcim-payment-history.test.js
Normal file
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue