fix(helcim): skip HelcimPay verify when a card is already on file
Helcim refuses paymentType:'verify' for cards already saved on a
customer ("A new card must be entered for saving the payment method"),
breaking every "Complete Payment" retry after a partial-failed signup.
Add GET /api/helcim/existing-card and short-circuit HelcimPay verify in
useMemberPayment + payment-setup.vue when a saved card is found, going
straight to subscription creation. The two existence-check fetches run
in parallel with get-or-create-customer so no extra round-trip latency
in the common path.
This commit is contained in:
parent
e3410c52a5
commit
0f841912e2
4 changed files with 201 additions and 42 deletions
115
tests/server/api/helcim-existing-card.test.js
Normal file
115
tests/server/api/helcim-existing-card.test.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { requireAuth } from '../../../server/utils/auth.js'
|
||||
import { listHelcimCustomerCards } from '../../../server/utils/helcim.js'
|
||||
import existingCardHandler from '../../../server/api/helcim/existing-card.get.js'
|
||||
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||
|
||||
vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() }))
|
||||
vi.mock('../../../server/utils/helcim.js', () => ({
|
||||
listHelcimCustomerCards: vi.fn()
|
||||
}))
|
||||
|
||||
const newEvent = () =>
|
||||
createMockEvent({ method: 'GET', path: '/api/helcim/existing-card' })
|
||||
|
||||
describe('helcim existing-card endpoint', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('requires auth', async () => {
|
||||
requireAuth.mockRejectedValue(
|
||||
createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
||||
)
|
||||
|
||||
await expect(existingCardHandler(newEvent())).rejects.toMatchObject({ statusCode: 401 })
|
||||
expect(listHelcimCustomerCards).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns { cardToken: null } when member has no helcimCustomerId', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: null })
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result).toEqual({ cardToken: null })
|
||||
expect(listHelcimCustomerCards).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns { cardToken: null } when customer has no cards', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue([])
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result).toEqual({ cardToken: null })
|
||||
expect(listHelcimCustomerCards).toHaveBeenCalledWith(9876)
|
||||
})
|
||||
|
||||
it('returns the default card when one is flagged default:true', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue([
|
||||
{ cardToken: 'tok-old' },
|
||||
{ cardToken: 'tok-default', default: true }
|
||||
])
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result).toEqual({ cardToken: 'tok-default' })
|
||||
})
|
||||
|
||||
it('falls back to first card when no card is flagged default', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue([
|
||||
{ cardToken: 'tok-first' },
|
||||
{ cardToken: 'tok-second' }
|
||||
])
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result).toEqual({ cardToken: 'tok-first' })
|
||||
})
|
||||
|
||||
it('handles isDefault flag (alternative shape)', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue([
|
||||
{ cardToken: 'tok-a' },
|
||||
{ cardToken: 'tok-b', isDefault: true }
|
||||
])
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result.cardToken).toBe('tok-b')
|
||||
})
|
||||
|
||||
it('unwraps a { cards: [...] } response envelope', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue({
|
||||
cards: [{ cardToken: 'tok-only' }]
|
||||
})
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result.cardToken).toBe('tok-only')
|
||||
})
|
||||
|
||||
it('unwraps a { data: [...] } response envelope', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue({
|
||||
data: [{ cardToken: 'tok-only' }]
|
||||
})
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result.cardToken).toBe('tok-only')
|
||||
})
|
||||
|
||||
it('returns { cardToken: null } if the resolved card has no cardToken', async () => {
|
||||
requireAuth.mockResolvedValue({ _id: 'm1', helcimCustomerId: 9876 })
|
||||
listHelcimCustomerCards.mockResolvedValue([{ default: true }])
|
||||
|
||||
const result = await existingCardHandler(newEvent())
|
||||
|
||||
expect(result).toEqual({ cardToken: null })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue