// Update the authenticated member's default card in Helcim. // Flow: // 1. requireAuth + status gate (active | pending_payment). // 2. Validate body (cardToken). // 3. Verify cardToken belongs to this customer via listHelcimCustomerCards. // 4. Capture current default card token for possible revert. // 5. Update customer default payment method (authoritative — Helcim bills the customer default). // 6. Update subscription payment method (best-effort belt-and-suspenders). // - On 4xx from step 6 (likely "unknown field" since Helcim's subscription // PATCH schema does not publicly document a cardToken field — see plan // Open Question #3): log and treat as success, since the customer // default already changed and is the real billing driver. // - On 5xx/network from step 6: attempt revert of step 5 and return 500. // 7. logActivity on success. import { requireAuth } from '../../utils/auth.js' import { validateBody } from '../../utils/validateBody.js' import { helcimUpdateCardSchema } from '../../utils/schemas.js' import { listHelcimCustomerCards, updateHelcimCustomerDefaultPaymentMethod, updateHelcimSubscriptionPaymentMethod } from '../../utils/helcim.js' import { logActivity } from '../../utils/activityLog.js' const ALLOWED_STATUSES = new Set(['active', 'pending_payment']) export default defineEventHandler(async (event) => { const member = await requireAuth(event) if (!ALLOWED_STATUSES.has(member.status)) { throw createError({ statusCode: 403, statusMessage: 'Card updates are only available for active subscriptions' }) } if (!member.helcimCustomerId || !member.helcimSubscriptionId) { throw createError({ statusCode: 400, statusMessage: 'No active subscription on file' }) } const body = await validateBody(event, helcimUpdateCardSchema) const { cardToken } = body // Step 3: verify the submitted token is attached to this member's customer const cards = await listHelcimCustomerCards(member.helcimCustomerId) const matchingCard = cards.find((c) => c?.cardToken === cardToken) if (!matchingCard) { throw createError({ statusCode: 403, statusMessage: 'Card token does not belong to this customer' }) } // Step 4: capture prior default card token for possible revert const priorDefault = cards.find((c) => c?.default === true || c?.isDefault === true) const priorCardToken = priorDefault?.cardToken || null // Step 5: authoritative change — update customer default try { await updateHelcimCustomerDefaultPaymentMethod(member.helcimCustomerId, cardToken) } catch (error) { console.error('[update-card] Customer default update failed', { memberId: String(member._id), error: error?.statusMessage || error?.message || error }) throw createError({ statusCode: 500, statusMessage: 'Failed to update payment method' }) } // Step 6: best-effort subscription payment-method update. // Plan Open Question #3: Helcim's subscription PATCH does not publicly // document a cardToken field. We tolerate 4xx (unknown field / schema // rejection) because step 5 — the customer default — is authoritative. // On 5xx/network errors we treat state as untrusted and attempt revert. try { await updateHelcimSubscriptionPaymentMethod(member.helcimSubscriptionId, cardToken) } catch (error) { const status = Number(error?.statusCode) || 0 const isClientError = status >= 400 && status < 500 if (isClientError) { console.warn('[update-card] Subscription PATCH rejected (4xx); treating as best-effort', { memberId: String(member._id), status, message: error?.statusMessage || error?.message }) // fall through to success — customer default is the billing driver } else { // 5xx / network / unknown — revert customer default if we can console.error('[update-card] Subscription update failed with server error; attempting revert', { memberId: String(member._id), status, error: error?.statusMessage || error?.message || error }) if (priorCardToken) { try { await updateHelcimCustomerDefaultPaymentMethod(member.helcimCustomerId, priorCardToken) } catch (revertError) { console.error('[update-card] Revert also failed — INCONSISTENT STATE', { memberId: String(member._id), helcimCustomerId: member.helcimCustomerId, revertError: revertError?.statusMessage || revertError?.message || revertError }) } } else { console.error('[update-card] No prior default card to revert to', { memberId: String(member._id) }) } throw createError({ statusCode: 500, statusMessage: 'Failed to update payment method' }) } } // Step 7: success await logActivity(member._id, 'billing_card_updated', {}, { visibility: 'member' }) return { success: true } })