/** * Authorize.net payment integration * * Supports: * - Accept.js nonce-based direct charge (primary, seamless checkout) * - Customer profile + payment profile creation (card vaulting) * - Refunds via original transaction ID * * Required env vars: * AUTHORIZE_NET_API_LOGIN_ID * AUTHORIZE_NET_TRANSACTION_KEY * AUTHORIZE_NET_SANDBOX=true (set to false for production) */ const API_LOGIN_ID = process.env.AUTHORIZE_NET_API_LOGIN_ID!; const TRANSACTION_KEY = process.env.AUTHORIZE_NET_TRANSACTION_KEY!; const SANDBOX = process.env.AUTHORIZE_NET_SANDBOX !== "false"; const ENDPOINT = SANDBOX ? "https://apitest.authorize.net/xml/v1/request.api" : "https://api.authorize.net/xml/v1/request.api"; interface ANetCredentials { merchantAuthentication: { name: string; transactionKey: string; }; } function auth(): ANetCredentials { return { merchantAuthentication: { name: API_LOGIN_ID, transactionKey: TRANSACTION_KEY, }, }; } async function post(body: object): Promise { const res = await fetch(ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const text = await res.text(); // Authorize.net responses begin with a BOM — strip it return JSON.parse(text.replace(/^\uFEFF/, "")); } export interface ChargeResult { success: boolean; transactionId?: string; authCode?: string; error?: string; rawResponse?: any; } /** * Charge using an Accept.js opaque data nonce (dataValue + dataDescriptor). * This is the preferred seamless checkout path — card data never touches your server. */ export async function chargeOpaqueData(params: { dataDescriptor: string; dataValue: string; amountCents: number; email: string; description?: string; billingAddress?: { firstName?: string; lastName?: string; address?: string; city?: string; state?: string; zip?: string; country?: string; }; }): Promise { const amountStr = (params.amountCents / 100).toFixed(2); const body = { createTransactionRequest: { ...auth(), transactionRequest: { transactionType: "authCaptureTransaction", amount: amountStr, payment: { opaqueData: { dataDescriptor: params.dataDescriptor, dataValue: params.dataValue, }, }, order: { description: params.description || "Order" }, customer: { email: params.email }, ...(params.billingAddress && { billTo: { firstName: params.billingAddress.firstName, lastName: params.billingAddress.lastName, address: params.billingAddress.address, city: params.billingAddress.city, state: params.billingAddress.state, zip: params.billingAddress.zip, country: params.billingAddress.country || "US", }, }), }, }, }; try { const data = await post(body); const result = data?.transactionResponse; const approved = result?.responseCode === "1"; if (approved) { return { success: true, transactionId: result.transId, authCode: result.authCode, rawResponse: data, }; } const errMsg = result?.errors?.[0]?.errorText || result?.messages?.[0]?.description || "Transaction declined"; return { success: false, error: errMsg, rawResponse: data }; } catch (err: any) { return { success: false, error: err.message }; } } /** * Refund a captured transaction (must supply last 4 card digits). */ export async function refundTransaction(params: { transactionId: string; amountCents: number; cardLast4: string; }): Promise { const body = { createTransactionRequest: { ...auth(), transactionRequest: { transactionType: "refundTransaction", amount: (params.amountCents / 100).toFixed(2), payment: { creditCard: { cardNumber: params.cardLast4, expirationDate: "XXXX", }, }, refTransId: params.transactionId, }, }, }; try { const data = await post(body); const result = data?.transactionResponse; const ok = result?.responseCode === "1"; return { success: ok, transactionId: result?.transId, error: ok ? undefined : result?.errors?.[0]?.errorText, rawResponse: data, }; } catch (err: any) { return { success: false, error: err.message }; } } /** * Create an Authorize.net customer profile + payment profile from an opaque nonce. * Returns profileId and paymentProfileId for future charges. */ export async function createCustomerProfile(params: { email: string; dataDescriptor: string; dataValue: string; description?: string; }): Promise<{ success: boolean; profileId?: string; paymentProfileId?: string; error?: string }> { const body = { createCustomerProfileRequest: { ...auth(), profile: { merchantCustomerId: params.email, email: params.email, description: params.description, paymentProfiles: { customerType: "individual", payment: { opaqueData: { dataDescriptor: params.dataDescriptor, dataValue: params.dataValue, }, }, }, }, validationMode: SANDBOX ? "testMode" : "liveMode", }, }; try { const data = await post(body); const ok = data?.messages?.resultCode === "Ok"; if (ok) { return { success: true, profileId: data.customerProfileId, paymentProfileId: data.customerPaymentProfileIdList?.[0], }; } return { success: false, error: data?.messages?.message?.[0]?.text || "Profile creation failed", }; } catch (err: any) { return { success: false, error: err.message }; } } /** * Charge a vaulted customer payment profile. */ export async function chargeCustomerProfile(params: { profileId: string; paymentProfileId: string; amountCents: number; description?: string; }): Promise { const body = { createTransactionRequest: { ...auth(), transactionRequest: { transactionType: "authCaptureTransaction", amount: (params.amountCents / 100).toFixed(2), profile: { customerProfileId: params.profileId, paymentProfile: { paymentProfileId: params.paymentProfileId }, }, order: { description: params.description || "Order" }, }, }, }; try { const data = await post(body); const result = data?.transactionResponse; const ok = result?.responseCode === "1"; return { success: ok, transactionId: result?.transId, authCode: result?.authCode, error: ok ? undefined : result?.errors?.[0]?.errorText, rawResponse: data, }; } catch (err: any) { return { success: false, error: err.message }; } }