Spaces:
Running
Running
Download src/utils/payoneerCheckout.js from AndreaNotes1/auth-app-api: direct link, hf CLI and curl.
- Browser
- Download file 7.23 kB
-
https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/utils/payoneerCheckout.js
- Command line
-
hf download hf://spaces/AndreaNotes1/auth-app-api/src/utils/payoneerCheckout.js
-
curl -L -o payoneerCheckout.js https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/utils/payoneerCheckout.js
7.23 kB
| /** | |
| * Adapter around Payoneer Checkout (the hosted-payment product that | |
| * lets a webstore accept card/wallet payments that settle directly | |
| * into a Payoneer account) β https://checkoutdocs.payoneer.com | |
| * | |
| * IMPORTANT β READ BEFORE GOING LIVE: | |
| * Payoneer Checkout is not a self-serve API like Stripe. Per Payoneer's | |
| * own documentation, credentials and the exact request/response | |
| * contract (endpoint paths, field names, webhook signature scheme) are | |
| * issued directly to you by Payoneer's integration team only after | |
| * your business applies and is approved as a Payoneer Checkout | |
| * merchant. That contract is account-specific, so it isn't something | |
| * that can be correctly guessed or hard-coded here in advance. | |
| * | |
| * What IS real and finished in this file: | |
| * - The env-driven config wiring (config/env.js -> payoneerCheckout) | |
| * - The calling convention the rest of the app uses (see | |
| * controllers/checkout.controller.js and webhook.controller.js) | |
| * - A standard HMAC-SHA256 webhook-signature check, which is the | |
| * common pattern most hosted-checkout providers use (Stripe, | |
| * Paysera, PayMongo, etc. all verify webhooks this way) β but you | |
| * MUST confirm the header name and signing scheme against your own | |
| * Payoneer Checkout webhook docs once you have them, since generic | |
| * industry convention isn't a guarantee it matches Payoneer's | |
| * specific implementation. | |
| * | |
| * What's a clearly-marked PLACEHOLDER, pending your approved | |
| * integration docs from Payoneer: | |
| * - The exact endpoint path for creating a checkout session | |
| * - The exact request body field names Payoneer's API expects | |
| * - The exact response field names for the session id / redirect URL | |
| * - The exact webhook payload field names / event-type values | |
| * - The exact webhook signature header name | |
| * | |
| * Search for "PAYONEER-DOCS-NEEDED" below for every spot that needs a | |
| * one-line fix once you have that contract in hand β the surrounding | |
| * logic (error handling, order matching, idempotency) doesn't need to | |
| * change. | |
| */ | |
| const crypto = require("crypto"); | |
| const { env } = require("../config/env"); | |
| function assertConfigured() { | |
| if (!env.payoneerCheckout.apiBase || !env.payoneerCheckout.apiKey || !env.payoneerCheckout.apiSecret) { | |
| throw Object.assign( | |
| new Error( | |
| "Payoneer Checkout is not configured yet. Set PAYONEER_CHECKOUT_API_BASE, " + | |
| "PAYONEER_CHECKOUT_API_KEY and PAYONEER_CHECKOUT_API_SECRET once your Payoneer " + | |
| "Checkout merchant account is approved." | |
| ), | |
| { status: 503 } | |
| ); | |
| } | |
| } | |
| /** | |
| * Creates a hosted checkout session for one order and returns where to | |
| * send the buyer to actually pay, plus any tax Payoneer's session | |
| * response reports (so the buyer can be shown a clear amount + tax + | |
| * total before being redirected β see checkout.controller.js). | |
| * | |
| * @param {object} params | |
| * @param {object} params.order - the just-created Order document | |
| * @param {object} params.course - the Course being purchased | |
| * @param {string} params.customerEmail | |
| * @param {string} params.customerName | |
| * @param {string} params.successUrl - where Payoneer redirects after a successful payment | |
| * @param {string} params.cancelUrl - where Payoneer redirects after a cancelled/failed payment | |
| */ | |
| async function createCheckoutSession({ order, course, customerEmail, customerName, successUrl, cancelUrl }) { | |
| assertConfigured(); | |
| // PAYONEER-DOCS-NEEDED: confirm this path against your approved | |
| // Payoneer Checkout API reference. | |
| const endpoint = `${env.payoneerCheckout.apiBase}/v1/checkout-sessions`; | |
| const response = await fetch(endpoint, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| // PAYONEER-DOCS-NEEDED: confirm the auth header/scheme Payoneer | |
| // issues you (this assumes a bearer API key, a common pattern, | |
| // but Payoneer may use a different header or OAuth2 client- | |
| // credentials flow instead). | |
| Authorization: `Bearer ${env.payoneerCheckout.apiKey}`, | |
| }, | |
| body: JSON.stringify({ | |
| // PAYONEER-DOCS-NEEDED: field names below are our best-guess | |
| // shape for a hosted checkout session, modelled on the common | |
| // pattern across similar providers β confirm every field name | |
| // against Payoneer's reference before relying on this in | |
| // production. | |
| amount: order.amount, | |
| currency: order.currency, | |
| order_id: order.orderNumber, | |
| description: course.title, | |
| customer: { email: customerEmail, name: customerName }, | |
| success_url: successUrl, | |
| cancel_url: cancelUrl, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const body = await response.text().catch(() => ""); | |
| throw new Error(`Payoneer Checkout session creation failed (${response.status}): ${body}`); | |
| } | |
| const data = await response.json(); | |
| // PAYONEER-DOCS-NEEDED: confirm these response field names. | |
| return { | |
| sessionId: data.id || data.session_id, | |
| checkoutUrl: data.checkout_url || data.redirect_url, | |
| tax: data.tax | |
| ? { amount: data.tax.amount, currency: data.tax.currency || order.currency } | |
| : null, | |
| }; | |
| } | |
| /** | |
| * Verifies an incoming webhook's signature and normalizes it into | |
| * { sessionId, status, providerOrderId }. Throws if the signature is | |
| * missing or invalid β the caller (webhook.controller.js) treats a | |
| * throw as "reject this request," never as "treat it as paid." | |
| * | |
| * `rawBody` must be the exact, unparsed request body bytes/string used | |
| * to compute the signature β see routes/webhook.routes.js, which | |
| * registers this route with a raw body parser specifically so the | |
| * bytes here match exactly what Payoneer signed. | |
| */ | |
| function verifyAndParseWebhook({ rawBody, signatureHeader }) { | |
| if (!env.payoneerCheckout.webhookSecret) { | |
| throw Object.assign(new Error("PAYONEER_CHECKOUT_WEBHOOK_SECRET is not configured."), { status: 503 }); | |
| } | |
| if (!signatureHeader) { | |
| throw Object.assign(new Error("Missing webhook signature."), { status: 400 }); | |
| } | |
| const expectedSignature = crypto | |
| .createHmac("sha256", env.payoneerCheckout.webhookSecret) | |
| .update(rawBody) | |
| .digest("hex"); | |
| // PAYONEER-DOCS-NEEDED: confirm Payoneer's actual signature encoding | |
| // (hex vs base64) and whether the header carries just the signature | |
| // or a prefixed/versioned value (e.g. Stripe's "t=...,v1=..."). | |
| const signatureValid = | |
| signatureHeader.length === expectedSignature.length && | |
| crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expectedSignature)); | |
| if (!signatureValid) { | |
| throw Object.assign(new Error("Invalid webhook signature."), { status: 400 }); | |
| } | |
| const payload = JSON.parse(rawBody.toString("utf8")); | |
| // PAYONEER-DOCS-NEEDED: confirm the actual field/event names Payoneer | |
| // sends (this assumes something like { event: "payment.completed", | |
| // session_id, order_id }). | |
| const eventType = payload.event || payload.type; | |
| const status = eventType && eventType.includes("completed") ? "paid" : "other"; | |
| return { | |
| sessionId: payload.session_id || payload.id, | |
| providerOrderId: payload.order_id, | |
| status, | |
| raw: payload, | |
| }; | |
| } | |
| module.exports = { createCheckoutSession, verifyAndParseWebhook }; | |