chore: add server/payments/authorizenet.ts

This commit is contained in:
notshop 2026-04-26 16:36:18 +00:00
parent 1f5d34aab5
commit f948547f03

View file

@ -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<any> {
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<ChargeResult> {
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<ChargeResult> {
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<ChargeResult> {
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 };
}
}