fix(helcim): skip HelcimPay verify when a card is already on file
Some checks failed
Test / vitest (push) Successful in 11m5s
Test / playwright (push) Failing after 9m18s
Test / visual (push) Failing after 9m24s
Test / Notify on failure (push) Successful in 2s

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:
Jennie Robinson Faber 2026-04-26 17:27:40 +01:00
parent e3410c52a5
commit 0f841912e2
4 changed files with 201 additions and 42 deletions

View file

@ -25,45 +25,53 @@ export const useMemberPayment = () => {
paymentSuccess.value = false
try {
// Step 1: Get or create Helcim customer
await getOrCreateCustomer()
// Skip HelcimPay verify if a card's already on file — Helcim refuses
// to re-save it, breaking retries after a partial-failed signup.
const [, existing] = await Promise.all([
getOrCreateCustomer(),
$fetch('/api/helcim/existing-card').catch((err) => {
console.warn('[payment] existing-card lookup failed, falling back to verify flow:', err)
return null
}),
])
// Step 2: Initialize Helcim payment with $0 for card verification
await initializeHelcimPay(
customerId.value,
customerCode.value,
0,
)
let cardToken = existing?.cardToken || null
// Step 3: Show payment modal and get payment result
const paymentResult = await verifyPayment()
console.log('Payment result:', paymentResult)
if (!cardToken) {
await initializeHelcimPay(
customerId.value,
customerCode.value,
0,
)
if (!paymentResult.success) {
throw new Error('Payment verification failed')
const paymentResult = await verifyPayment()
if (!paymentResult.success) {
throw new Error('Payment verification failed')
}
const verifyResult = await $fetch('/api/helcim/verify-payment', {
method: 'POST',
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value,
},
})
if (!verifyResult.success) {
throw new Error('Payment verification failed on backend')
}
cardToken = paymentResult.cardToken
}
// Step 4: Verify payment on backend
const verifyResult = await $fetch('/api/helcim/verify-payment', {
method: 'POST',
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value,
},
})
if (!verifyResult.success) {
throw new Error('Payment verification failed on backend')
}
// Step 5: Create subscription with proper contribution tier
const subscriptionResponse = await $fetch('/api/helcim/subscription', {
method: 'POST',
body: {
customerId: customerId.value,
customerCode: customerCode.value,
contributionAmount: memberData.value?.contributionAmount ?? 5,
cardToken: paymentResult.cardToken,
cardToken,
},
})
@ -71,7 +79,6 @@ export const useMemberPayment = () => {
throw new Error('Subscription creation failed')
}
// Step 6: Payment successful - refresh member data
paymentSuccess.value = true
await checkMemberStatus()

View file

@ -72,6 +72,7 @@ const errorMessage = ref('');
const isProcessing = ref(false);
const customerId = ref('');
const customerCode = ref('');
const hasExistingCard = ref(false);
const initialize = async () => {
errorMessage.value = '';
@ -84,13 +85,22 @@ const initialize = async () => {
}
try {
const customer = await $fetch('/api/helcim/get-or-create-customer', {
method: 'POST',
});
// Skip HelcimPay verify if a card's already on file Helcim refuses
// to re-save it, breaking retries after a partial-failed signup.
const [customer, existing] = await Promise.all([
$fetch('/api/helcim/get-or-create-customer', { method: 'POST' }),
$fetch('/api/helcim/existing-card').catch((err) => {
console.warn('[payment-setup] existing-card lookup failed, falling back to verify flow:', err);
return null;
}),
]);
customerId.value = customer.customerId;
customerCode.value = customer.customerCode;
hasExistingCard.value = Boolean(existing?.cardToken);
await initializeHelcimPay(customerId.value, customerCode.value, 0);
if (!hasExistingCard.value) {
await initializeHelcimPay(customerId.value, customerCode.value, 0);
}
step.value = 'ready';
} catch (err) {
console.error('Payment setup init failed:', err);
@ -106,16 +116,18 @@ const openModal = async () => {
errorMessage.value = '';
try {
const result = await verifyPayment();
if (!result?.success) throw new Error('Payment was not completed.');
if (!hasExistingCard.value) {
const result = await verifyPayment();
if (!result?.success) throw new Error('Payment was not completed.');
await $fetch('/api/helcim/verify-payment', {
method: 'POST',
body: {
cardToken: result.cardToken,
customerId: customerId.value,
},
});
await $fetch('/api/helcim/verify-payment', {
method: 'POST',
body: {
cardToken: result.cardToken,
customerId: customerId.value,
},
});
}
// Update circle first if it changed update-contribution only touches tier.
if (targetCircle.value && targetCircle.value !== memberData.value?.circle) {