// Helcim API helper — centralized fetcher and typed wrappers for Helcim v2 endpoints. // Auto-imported by Nitro under server/utils, but server API handlers in this codebase // use explicit imports (matching the existing `../../utils/auth.js` pattern). import { randomBytes } from 'node:crypto' const HELCIM_API_BASE = 'https://api.helcim.com/v2' const IDEMPOTENCY_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' /** * Generic Helcim API fetcher. * * Reads the API token from runtime config on every call (do not hoist — Nitro * runtime config is not guaranteed to be available at module import time). * Throws `createError` with the upstream status on non-2xx responses. Network * errors (fetch itself rejecting) propagate naturally so callers can decide * how to handle them. * * @param {string} path - Path relative to the Helcim v2 base (must start with `/`). * @param {object} [options] * @param {string} [options.method='GET'] * @param {object} [options.body] - JSON-serializable request body. * @param {string} [options.idempotencyKey] - Sent as `idempotency-key` header. * @param {string} [options.errorMessage] - statusMessage used when the upstream returns non-OK. * @returns {Promise} Parsed JSON response. */ export async function helcimFetch(path, { method = 'GET', body, idempotencyKey, errorMessage } = {}) { const helcimToken = useRuntimeConfig().helcimApiToken if (!helcimToken) { throw createError({ statusCode: 500, statusMessage: 'Helcim API token not configured' }) } const headers = { accept: 'application/json', 'api-token': helcimToken } if (body !== undefined) { headers['content-type'] = 'application/json' } if (idempotencyKey) { headers['idempotency-key'] = idempotencyKey } const response = await fetch(`${HELCIM_API_BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }) if (!response.ok) { const errorText = await response.text() console.error(`[helcim] ${method} ${path} ${response.status} ${errorText}`) throw createError({ statusCode: response.status, statusMessage: errorMessage || 'Helcim API error' }) } return await response.json() } // ---- Customers ---- export const getHelcimCustomer = (id) => helcimFetch(`/customers/${id}`, { errorMessage: 'Customer lookup failed' }) export const findHelcimCustomerByEmail = (email) => helcimFetch(`/customers?search=${encodeURIComponent(email)}`, { errorMessage: 'Customer search failed' }) export const createHelcimCustomer = (payload) => helcimFetch('/customers', { method: 'POST', body: payload, errorMessage: 'Customer creation failed' }) export const updateHelcimCustomer = (id, payload) => helcimFetch(`/customers/${id}`, { method: 'PATCH', body: payload, errorMessage: 'Billing update failed' }) export const listHelcimCustomerCards = (id) => helcimFetch(`/customers/${id}/cards`, { errorMessage: 'Card lookup failed' }) // ---- Subscriptions ---- export const createHelcimSubscription = (subscription, idempotencyKey) => helcimFetch('/subscriptions', { method: 'POST', body: { subscriptions: [subscription] }, idempotencyKey, errorMessage: 'Subscription creation failed' }) export const cancelHelcimSubscription = (id) => helcimFetch(`/subscriptions/${id}`, { method: 'DELETE', errorMessage: 'Subscription cancellation failed' }) export const updateHelcimSubscription = (id, payload) => helcimFetch(`/subscriptions/${id}`, { method: 'PATCH', body: payload, errorMessage: 'Subscription update failed' }) // ---- Payment plans (admin) ---- export const listHelcimPlans = () => helcimFetch('/payment-plans', { errorMessage: 'Failed to fetch payment plans' }) export const createHelcimPlan = (payload) => helcimFetch('/payment-plans', { method: 'POST', body: payload, errorMessage: 'Payment plan creation failed' }) export const listHelcimSubscriptions = () => helcimFetch('/subscriptions', { errorMessage: 'Failed to fetch subscriptions' }) // ---- HelcimPay.js ---- export const initializeHelcimPaySession = (payload) => helcimFetch('/helcim-pay/initialize', { method: 'POST', body: payload, errorMessage: 'Payment initialization failed' }) /** * Generate a 25-character idempotency key using `crypto.randomBytes`. * * Each byte is mapped to the 62-character alphabet via modulo. The modulo * introduces a slight bias (256 % 62 = 8), but entropy from `randomBytes` — * not perfect uniformity — is what matters for an idempotency key. * * @returns {string} 25-character key. */ export function generateIdempotencyKey() { const bytes = randomBytes(25) let key = '' for (let i = 0; i < 25; i++) { key += IDEMPOTENCY_ALPHABET[bytes[i] % IDEMPOTENCY_ALPHABET.length] } return key } /** * Legacy stub — kept alive ONLY so `server/api/events/[id]/payment.post.js` * still imports cleanly. The direct purchase API was never implemented. * Always returns `{ success: false }`; callers surface the message to the user. */ export async function processHelcimPayment(_paymentData) { return { success: false, message: 'Direct purchase API not implemented; use HelcimPay.js flow' } }