From f948547f0309a9a4ae50ee698582a52dc36856b9 Mon Sep 17 00:00:00 2001 From: notshop Date: Sun, 26 Apr 2026 16:36:18 +0000 Subject: [PATCH] chore: add server/payments/authorizenet.ts --- server/payments/authorizenet.ts | 255 ++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 server/payments/authorizenet.ts diff --git a/server/payments/authorizenet.ts b/server/payments/authorizenet.ts new file mode 100644 index 0000000..92c6da3 --- /dev/null +++ b/server/payments/authorizenet.ts @@ -0,0 +1,255 @@ +/** + * 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 }; + } +}