feat: Shopier payment integration with 3DS callback + unified checkout action

This commit is contained in:
kovakmedya
2026-04-30 21:57:51 +03:00
parent 196036c0d8
commit 00a8351f66
7 changed files with 466 additions and 12 deletions
+127
View File
@@ -0,0 +1,127 @@
import "server-only";
import { createHmac, timingSafeEqual } from "crypto";
export const SHOPIER_ENDPOINT = "https://www.shopier.com/ShowProduct/api_pay4.php";
const API_KEY = process.env.SHOPIER_API_KEY ?? "";
const API_SECRET = process.env.SHOPIER_API_SECRET ?? "";
const WEBSITE_INDEX = process.env.SHOPIER_WEBSITE_INDEX ?? "1";
// Callback base URL can be overridden with a tunnel URL for localhost testing.
// e.g. SHOPIER_CALLBACK_BASE_URL=https://abc123.trycloudflare.com
function callbackBaseUrl(): string {
return (
process.env.SHOPIER_CALLBACK_BASE_URL?.replace(/\/$/, "") ??
process.env.APP_URL?.replace(/\/$/, "") ??
"http://localhost:3000"
);
}
function appBaseUrl(): string {
return process.env.APP_URL?.replace(/\/$/, "") ?? "http://localhost:3000";
}
export type CurrencyCode = "TRY" | "USD" | "EUR";
const CURRENCY_INDEX: Record<CurrencyCode, "0" | "1" | "2"> = {
TRY: "0",
USD: "1",
EUR: "2",
};
// Signature over: platform_order_id + total_order_value + currency + random_nr
// Key: API_SECRET | Encoding: base64
function signRequest(orderId: string, amount: string, currency: string, randomNr: string): string {
const message = orderId + amount + currency + randomNr;
return createHmac("sha256", API_SECRET).update(message).digest("base64");
}
export type ShopierCheckoutParams = {
orderId: string;
amount: number;
currency?: CurrencyCode;
productName: string;
buyerName: string;
buyerSurname: string;
buyerEmail: string;
buyerId: string;
buyerAccountAgeDays?: number;
};
export type ShopierFormFields = Record<string, string>;
export function buildShopierFormFields(params: ShopierCheckoutParams): ShopierFormFields {
if (!API_KEY || !API_SECRET) {
throw new Error("SHOPIER_API_KEY ve SHOPIER_API_SECRET ortam değişkenleri eksik.");
}
const currency: CurrencyCode = params.currency ?? "TRY";
const currencyIndex = CURRENCY_INDEX[currency];
// Amount as string with exactly 2 decimal places ("299.00")
const amountStr = params.amount.toFixed(2);
// 8-digit random number
const randomNr = String(Math.floor(10000000 + Math.random() * 90000000));
const signature = signRequest(params.orderId, amountStr, currencyIndex, randomNr);
const base = callbackBaseUrl();
const appBase = appBaseUrl();
return {
API_key: API_KEY,
website_index: WEBSITE_INDEX,
platform_order_id: params.orderId,
product_name: params.productName,
product_type: "0", // 0 = digital/virtual
buyer_name: params.buyerName,
buyer_surname: params.buyerSurname,
buyer_email: params.buyerEmail,
buyer_account_age: String(params.buyerAccountAgeDays ?? 0),
buyer_id_nr: params.buyerId,
total_order_value: amountStr,
currency: currencyIndex,
current_language: "0", // 0 = TR
random_nr: randomNr,
signature,
// Server-to-server async callback — needs public URL (use cloudflared tunnel for localhost)
callbackUrl: `${base}/api/payments/shopier/callback`,
// User-facing redirect after 3DS
okUrl: `${appBase}/settings/billing/checkout/${params.orderId}/shopier/result?ok=1`,
failUrl: `${appBase}/settings/billing/checkout/${params.orderId}/shopier/result?ok=0`,
};
}
// ---------- Callback verification ----------
export type ShopierCallbackPayload = {
platform_order_id: string;
status: string; // "success" | "fail"
installment_count: string;
random_nr: string;
API_key: string;
signature: string;
};
// Signature over: platform_order_id + status + installment_count + random_nr
export function verifyCallbackSignature(payload: ShopierCallbackPayload): boolean {
if (!API_SECRET) return false;
const message =
payload.platform_order_id +
payload.status +
payload.installment_count +
payload.random_nr;
const expected = createHmac("sha256", API_SECRET).update(message).digest("base64");
try {
return timingSafeEqual(Buffer.from(expected), Buffer.from(payload.signature));
} catch {
return false;
}
}
export function isShopierEnabled(): boolean {
return (
process.env.PAYMENT_PROVIDER === "shopier" &&
Boolean(process.env.SHOPIER_API_KEY) &&
Boolean(process.env.SHOPIER_API_SECRET)
);
}