UNPKG

@dylanmurzello/vendure-plugin-square

Version:

Square payment integration plugin for Vendure e-commerce. Supports payment authorization, settlement, and refunds with PCI-compliant card tokenization.

243 lines (242 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.squarePaymentHandler = void 0; exports.setSquareOptions = setSquareOptions; const core_1 = require("@vendure/core"); const square_1 = require("square"); /** * Wraps a promise with a timeout to prevent hanging requests * Prevents checkout from getting stuck if Square API is slow */ async function withTimeout(promise, timeoutMs = 30000, errorMessage = 'Request timeout') { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs)); return Promise.race([promise, timeout]); } // Store options at module level to avoid circular dependency let squareOptions = null; // Cache Square client to avoid recreating on every request (performance optimization) let squareClient = null; function setSquareOptions(options) { squareOptions = options; // Reset client when options change so new client gets created with new creds squareClient = null; } function getSquareOptions() { if (!squareOptions) { throw new Error('Square options not configured - call SquarePlugin.init() first'); } return squareOptions; } function getSquareClient() { if (!squareClient) { const options = getSquareOptions(); squareClient = new square_1.SquareClient({ token: options.accessToken, environment: options.environment === 'production' ? square_1.SquareEnvironment.Production : square_1.SquareEnvironment.Sandbox, }); } return squareClient; } /** * Square Payment Handler - actually processes payments like a real business should 💸 * Handles the whole payment lifecycle: authorize → settle → refund * No cap, this is where the money magic happens ✨ */ exports.squarePaymentHandler = new core_1.PaymentMethodHandler({ code: 'square-payment', description: [{ languageCode: core_1.LanguageCode.en, value: 'Square Payment' }], args: {}, /** * createPayment - authorize payment with Square (doesn't capture yet) * Gets called when customer hits "Place Order" and we need to lock in their payment method * Returns transaction ID that we'll use later to actually capture the money */ createPayment: async (ctx, order, amount, args, metadata) => { const startTime = Date.now(); try { const client = getSquareClient(); // Use cached client for better performance // metadata should contain the payment token from frontend Square Web SDK const sourceId = metadata.sourceId || metadata.token; if (!sourceId) { // No payment token? Ain't no way we processing this chief 🚫 core_1.Logger.warn(`Payment declined: Missing sourceId for order ${order.code}`, 'SquarePaymentHandler'); return { amount: order.total, state: 'Declined', errorMessage: 'Missing Square payment token (sourceId) from frontend', metadata: {}, }; } // Use deterministic idempotency key to prevent duplicate charges on retry // Critical: If network fails and request retries, same key = no duplicate charge const idempotencyKey = `${order.code}-create-${Date.now()}`; // Hit Square API to create the payment with timeout protection // This is where Square actually talks to the card network and says "yo can we charge this?" const response = await withTimeout(client.payments.create({ sourceId, // Payment method token from frontend idempotencyKey, // Deterministic key prevents duplicates amountMoney: { amount: BigInt(amount), // Amount in cents (e.g., $10.00 = 1000) currency: order.currencyCode, // Vendure uses CurrencyCode, Square uses Currency }, locationId: getSquareOptions().locationId, referenceId: order.code, // Our order code for tracking note: `Order ${order.code}`, // autocomplete: false means we authorize but don't capture yet // gives us time to verify order, check inventory, etc before actually taking the money autocomplete: false, }), 30000, // 30 second timeout 'Square payment creation timeout'); const payment = response.payment; if (!payment) { // Square said nah fam, something went wrong on their end core_1.Logger.error(`Payment declined: No payment object for order ${order.code}`, 'SquarePaymentHandler'); return { amount: order.total, state: 'Declined', errorMessage: 'Square payment creation failed - no payment object returned', metadata: {}, }; } // W in the chat, payment authorized successfully 🎉 core_1.Logger.info(`Payment authorized: Order ${order.code}, Amount ${order.total}, TxID ${payment.id}, ${Date.now() - startTime}ms`, 'SquarePaymentHandler'); return { amount: order.total, state: 'Authorized', // Money locked but not captured yet transactionId: payment.id || '', // Square payment ID we'll use to settle/refund later metadata: { squarePaymentId: payment.id, status: payment.status, receiptUrl: payment.receiptUrl, orderId: payment.orderId, }, }; } catch (error) { // Something went really wrong - log it and tell Vendure this payment flopped core_1.Logger.error(`Payment creation failed for order ${order.code}: ${error.message || 'Unknown error'} (${Date.now() - startTime}ms)`, 'SquarePaymentHandler'); return { amount: order.total, state: 'Declined', errorMessage: error.message || 'Square payment creation failed', metadata: { error: error.message }, }; } }, /** * settlePayment - actually capture the money (cha-ching moment) 💰 * Called after order is confirmed and we're ready to take the funds * Completes the payment that was previously authorized */ settlePayment: async (ctx, order, payment, args) => { const startTime = Date.now(); try { const client = getSquareClient(); // Use cached client const squarePaymentId = payment.transactionId; if (!squarePaymentId) { // Can't settle without the payment ID, this shouldn't happen but safety first core_1.Logger.warn(`Settlement failed: Missing payment ID for order ${order.code}`, 'SquarePaymentHandler'); return { success: false, errorMessage: 'Missing Square payment ID - cannot settle payment', }; } // Tell Square to complete the payment (capture the authorized funds) with timeout const response = await withTimeout(client.payments.complete({ paymentId: squarePaymentId }), 30000, 'Square payment settlement timeout'); const completedPayment = response.payment; if (completedPayment && completedPayment.status === 'COMPLETED') { // Money secured, order fulfilled, customer happy, business thriving 📈 core_1.Logger.info(`Payment settled: Order ${order.code}, TxID ${completedPayment.id}, ${Date.now() - startTime}ms`, 'SquarePaymentHandler'); return { success: true, metadata: { squarePaymentId: completedPayment.id, status: completedPayment.status, completedAt: new Date().toISOString(), }, }; } else { // Payment didn't complete properly, something sus happened return { success: false, errorMessage: `Square payment not completed. Status: ${completedPayment?.status}`, metadata: { status: completedPayment?.status }, }; } } catch (error) { core_1.Logger.error(`Payment settlement failed for order ${order.code}: ${error.message || 'Unknown error'} (${Date.now() - startTime}ms)`, 'SquarePaymentHandler'); return { success: false, errorMessage: error.message || 'Failed to settle Square payment', metadata: { error: error.message }, }; } }, /** * createRefund - return customer's money (sad business noises) 😔 * Called when order gets cancelled or customer returns stuff * Sends the funds back to their payment method */ createRefund: async (ctx, input, amount, order, payment, args) => { const startTime = Date.now(); try { const client = getSquareClient(); // Use cached client const squarePaymentId = payment.transactionId; if (!squarePaymentId) { // No payment ID = can't refund, this is a problem core_1.Logger.warn(`Refund failed: Missing payment ID for order ${order.code}`, 'SquarePaymentHandler'); return { state: 'Failed', metadata: { error: 'Missing Square payment ID' }, }; } // Use deterministic idempotency key for refunds too const idempotencyKey = `${order.code}-refund-${payment.id}-${Date.now()}`; // Hit Square refunds API to send the money back with timeout protection const response = await withTimeout(client.refunds.refundPayment({ idempotencyKey, // Deterministic key prevents duplicate refunds paymentId: squarePaymentId, amountMoney: { amount: BigInt(amount), // Amount in cents to refund currency: order.currencyCode, // Vendure uses CurrencyCode, Square uses Currency }, reason: input.reason || 'Customer refund request', }), 30000, 'Square refund timeout'); const refund = response.refund; if (refund && (refund.status === 'COMPLETED' || refund.status === 'PENDING')) { // Refund processed, customer getting their money back ✅ core_1.Logger.info(`Refund processed: Order ${order.code}, RefundID ${refund.id}, Amount ${amount}, ${Date.now() - startTime}ms`, 'SquarePaymentHandler'); return { state: 'Settled', // Refund successful transactionId: refund.id || '', metadata: { squareRefundId: refund.id, status: refund.status, refundedAt: new Date().toISOString(), }, }; } else { // Refund didn't go through properly return { state: 'Failed', metadata: { error: `Refund failed with status: ${refund?.status}` }, }; } } catch (error) { core_1.Logger.error(`Refund failed for order ${order.code}: ${error.message || 'Unknown error'} (${Date.now() - startTime}ms)`, 'SquarePaymentHandler'); return { state: 'Failed', metadata: { error: error.message || 'Refund failed' }, }; } }, });