feat(helcim): add transaction list + card update helpers
- listHelcimCustomerTransactions(customerCode): GET /card-transactions/
with customerCode filter, sorts newest-first, caps at 50, normalizes
Helcim status (APPROVED/DECLINED) + type (refund) into
paid/refunded/failed/other.
- updateHelcimCustomerDefaultPaymentMethod(customerId, cardToken):
resolves cardToken -> cardId via /customers/{id}/cards, then PATCHes
/customers/{id}/cards/{cardId}/default.
- updateHelcimSubscriptionPaymentMethod(subscriptionId, cardToken):
wraps updateHelcimSubscription with a cardToken payload.
- helcimUpdateCardSchema: Zod schema { cardToken: string } for the
upcoming /api/helcim/update-card route.
- Unit tests for all three helpers (success + error paths).
This commit is contained in:
parent
b6f5ae8c5e
commit
6888663148
3 changed files with 305 additions and 0 deletions
194
tests/server/utils/helcim.test.js
Normal file
194
tests/server/utils/helcim.test.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import {
|
||||
listHelcimCustomerTransactions,
|
||||
updateHelcimCustomerDefaultPaymentMethod,
|
||||
updateHelcimSubscriptionPaymentMethod
|
||||
} from '../../../server/utils/helcim.js'
|
||||
|
||||
const mockFetch = vi.fn()
|
||||
vi.stubGlobal('fetch', mockFetch)
|
||||
|
||||
function okResponse(payload) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => (payload === null || payload === undefined ? '' : JSON.stringify(payload))
|
||||
}
|
||||
}
|
||||
|
||||
function errResponse(status = 500, body = 'boom') {
|
||||
return {
|
||||
ok: false,
|
||||
status,
|
||||
text: async () => body
|
||||
}
|
||||
}
|
||||
|
||||
describe('listHelcimCustomerTransactions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterEach(() => {
|
||||
mockFetch.mockReset()
|
||||
})
|
||||
|
||||
it('normalizes, sorts newest-first, and maps statuses', async () => {
|
||||
mockFetch.mockResolvedValue(okResponse([
|
||||
{
|
||||
transactionId: 1,
|
||||
dateCreated: '2024-01-10 10:00:00',
|
||||
amount: 10,
|
||||
currency: 'CAD',
|
||||
status: 'APPROVED',
|
||||
type: 'purchase'
|
||||
},
|
||||
{
|
||||
transactionId: 2,
|
||||
dateCreated: '2024-03-15 10:00:00',
|
||||
amount: 25,
|
||||
currency: 'CAD',
|
||||
status: 'APPROVED',
|
||||
type: 'refund'
|
||||
},
|
||||
{
|
||||
transactionId: 3,
|
||||
dateCreated: '2024-02-01 10:00:00',
|
||||
amount: 5,
|
||||
currency: 'CAD',
|
||||
status: 'DECLINED',
|
||||
type: 'purchase'
|
||||
},
|
||||
{
|
||||
transactionId: 4,
|
||||
dateCreated: '2024-02-15 10:00:00',
|
||||
amount: 1,
|
||||
currency: 'CAD',
|
||||
status: 'UNKNOWN',
|
||||
type: 'purchase'
|
||||
}
|
||||
]))
|
||||
|
||||
const result = await listHelcimCustomerTransactions('CST1044')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
const url = mockFetch.mock.calls[0][0]
|
||||
expect(url).toContain('/card-transactions/')
|
||||
expect(url).toContain('customerCode=CST1044')
|
||||
expect(url).toContain('limit=50')
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: '2', date: '2024-03-15 10:00:00', amount: 25, status: 'refunded', currency: 'CAD' },
|
||||
{ id: '4', date: '2024-02-15 10:00:00', amount: 1, status: 'other', currency: 'CAD' },
|
||||
{ id: '3', date: '2024-02-01 10:00:00', amount: 5, status: 'failed', currency: 'CAD' },
|
||||
{ id: '1', date: '2024-01-10 10:00:00', amount: 10, status: 'paid', currency: 'CAD' }
|
||||
])
|
||||
})
|
||||
|
||||
it('throws when Helcim returns non-2xx', async () => {
|
||||
mockFetch.mockResolvedValue(errResponse(500))
|
||||
|
||||
await expect(listHelcimCustomerTransactions('CST1044')).rejects.toMatchObject({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Transaction lookup failed'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateHelcimCustomerDefaultPaymentMethod', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterEach(() => {
|
||||
mockFetch.mockReset()
|
||||
})
|
||||
|
||||
it('resolves cardToken to cardId then PATCHes default endpoint', async () => {
|
||||
// First call: list cards filtered by token
|
||||
mockFetch.mockResolvedValueOnce(okResponse([
|
||||
{ id: 161462, cardToken: 'tok-abc', cardHolderName: 'Jane' }
|
||||
]))
|
||||
// Second call: PATCH default
|
||||
mockFetch.mockResolvedValueOnce(okResponse({ id: 2488717, customerCode: 'CST1200' }))
|
||||
|
||||
const result = await updateHelcimCustomerDefaultPaymentMethod('2488717', 'tok-abc')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2)
|
||||
const firstUrl = mockFetch.mock.calls[0][0]
|
||||
expect(firstUrl).toContain('/customers/2488717/cards')
|
||||
expect(firstUrl).toContain('cardToken=tok-abc')
|
||||
|
||||
const secondUrl = mockFetch.mock.calls[1][0]
|
||||
const secondOpts = mockFetch.mock.calls[1][1]
|
||||
expect(secondUrl).toContain('/customers/2488717/cards/161462/default')
|
||||
expect(secondOpts.method).toBe('PATCH')
|
||||
|
||||
expect(result).toEqual({ id: 2488717, customerCode: 'CST1200' })
|
||||
})
|
||||
|
||||
it('throws 404 when cardToken is not attached to the customer', async () => {
|
||||
mockFetch.mockResolvedValueOnce(okResponse([]))
|
||||
|
||||
await expect(
|
||||
updateHelcimCustomerDefaultPaymentMethod('2488717', 'nope')
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Card token not found on customer'
|
||||
})
|
||||
|
||||
// Only the lookup should have happened — no PATCH
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('propagates Helcim errors from the PATCH call', async () => {
|
||||
mockFetch.mockResolvedValueOnce(okResponse([
|
||||
{ id: 161462, cardToken: 'tok-abc' }
|
||||
]))
|
||||
mockFetch.mockResolvedValueOnce(errResponse(500))
|
||||
|
||||
await expect(
|
||||
updateHelcimCustomerDefaultPaymentMethod('2488717', 'tok-abc')
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Default card update failed'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateHelcimSubscriptionPaymentMethod', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterEach(() => {
|
||||
mockFetch.mockReset()
|
||||
})
|
||||
|
||||
it('PATCHes /subscriptions with cardToken wrapped in subscriptions array', async () => {
|
||||
mockFetch.mockResolvedValue(okResponse({ data: [{ id: 123456 }] }))
|
||||
|
||||
const result = await updateHelcimSubscriptionPaymentMethod('123456', 'tok-abc')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
const [url, opts] = mockFetch.mock.calls[0]
|
||||
expect(url).toContain('/subscriptions')
|
||||
expect(opts.method).toBe('PATCH')
|
||||
|
||||
const body = JSON.parse(opts.body)
|
||||
expect(body).toEqual({
|
||||
subscriptions: [{ id: 123456, cardToken: 'tok-abc' }]
|
||||
})
|
||||
|
||||
expect(result).toEqual({ data: [{ id: 123456 }] })
|
||||
})
|
||||
|
||||
it('throws when Helcim returns non-2xx', async () => {
|
||||
mockFetch.mockResolvedValue(errResponse(400))
|
||||
|
||||
await expect(
|
||||
updateHelcimSubscriptionPaymentMethod('123456', 'tok-abc')
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Subscription update failed'
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue