diff --git a/src/core/payments/index.js b/src/core/payments/index.js index 074977c..bed77f9 100644 --- a/src/core/payments/index.js +++ b/src/core/payments/index.js @@ -1,7 +1 @@ -/** - * Payments Module Entry Point - * Re-exports all payment utilities - */ - export * from './stripe.js'; -export { default as stripe } from './stripe.js'; diff --git a/src/core/payments/stripe.js b/src/core/payments/stripe.js index 9e7139e..0f285d0 100644 --- a/src/core/payments/stripe.js +++ b/src/core/payments/stripe.js @@ -1,72 +1,33 @@ -/** - * Stripe Payment Utilities - * Generic Stripe integration for payment processing - * - * Usage in modules: - * import { createCheckoutSession, isEnabled } from '@zen/core/stripe'; - */ +import { createHash } from 'crypto'; +import Stripe from 'stripe'; -/** - * Get Stripe instance - * @returns {Promise} Stripe instance - */ -export async function getStripe() { +let _stripe = null; + +export function getStripe() { const secretKey = process.env.STRIPE_SECRET_KEY; - + if (!secretKey) { throw new Error('Stripe is not configured. Please set STRIPE_SECRET_KEY environment variable.'); } - - const Stripe = (await import('stripe')).default; - return new Stripe(secretKey, { - apiVersion: '2023-10-16', - }); + + if (!_stripe) { + _stripe = new Stripe(secretKey, { apiVersion: '2023-10-16' }); + } + + return _stripe; } -/** - * Check if Stripe is enabled - * @returns {boolean} - */ export function isEnabled() { return !!(process.env.STRIPE_SECRET_KEY && process.env.STRIPE_PUBLISHABLE_KEY); } -/** - * Get Stripe publishable key (for client-side) - * @returns {string|null} - */ export function getPublishableKey() { return process.env.STRIPE_PUBLISHABLE_KEY || null; } -/** - * Create a checkout session - * @param {Object} options - Checkout options - * @param {Array} options.lineItems - Line items for checkout - * @param {string} options.successUrl - Success redirect URL - * @param {string} options.cancelUrl - Cancel redirect URL - * @param {string} options.customerEmail - Customer email - * @param {Object} options.metadata - Additional metadata - * @param {string} options.mode - Payment mode (default: 'payment') - * @returns {Promise} Stripe session object - * - * @example - * const session = await createCheckoutSession({ - * lineItems: [{ - * price_data: { - * currency: 'usd', - * product_data: { name: 'Product' }, - * unit_amount: 1000, - * }, - * quantity: 1, - * }], - * successUrl: 'https://example.com/success', - * cancelUrl: 'https://example.com/cancel', - * }); - */ export async function createCheckoutSession(options) { - const stripe = await getStripe(); - + const stripe = getStripe(); + const { lineItems, successUrl, @@ -74,49 +35,34 @@ export async function createCheckoutSession(options) { customerEmail, metadata = {}, mode = 'payment', - paymentMethodTypes = ['card'], clientReferenceId, } = options; - + const sessionConfig = { - payment_method_types: paymentMethodTypes, line_items: lineItems, mode, success_url: successUrl, cancel_url: cancelUrl, metadata, }; - - if (customerEmail) { - sessionConfig.customer_email = customerEmail; - } - - if (clientReferenceId) { - sessionConfig.client_reference_id = clientReferenceId; - } - - return await stripe.checkout.sessions.create(sessionConfig); + + if (customerEmail) sessionConfig.customer_email = customerEmail; + if (clientReferenceId) sessionConfig.client_reference_id = clientReferenceId; + + return stripe.checkout.sessions.create(sessionConfig); } -/** - * Create a payment intent - * @param {Object} options - Payment options - * @param {number} options.amount - Amount in cents - * @param {string} options.currency - Currency code - * @param {Object} options.metadata - Additional metadata - * @returns {Promise} Stripe payment intent - */ export async function createPaymentIntent(options) { - const stripe = await getStripe(); - + const stripe = getStripe(); + const { amount, currency = process.env.ZEN_CURRENCY?.toLowerCase() || 'cad', metadata = {}, automaticPaymentMethods = { enabled: true }, } = options; - - return await stripe.paymentIntents.create({ + + return stripe.paymentIntents.create({ amount, currency, metadata, @@ -124,159 +70,59 @@ export async function createPaymentIntent(options) { }); } -/** - * Retrieve a checkout session - * @param {string} sessionId - Session ID - * @returns {Promise} Stripe session - */ export async function getCheckoutSession(sessionId) { - const stripe = await getStripe(); - return await stripe.checkout.sessions.retrieve(sessionId); + return getStripe().checkout.sessions.retrieve(sessionId); } -/** - * Retrieve a payment intent - * @param {string} paymentIntentId - Payment intent ID - * @returns {Promise} Stripe payment intent - */ export async function getPaymentIntent(paymentIntentId) { - const stripe = await getStripe(); - return await stripe.paymentIntents.retrieve(paymentIntentId); + return getStripe().paymentIntents.retrieve(paymentIntentId); } -/** - * Verify webhook signature - * @param {string} payload - Raw request body - * @param {string} signature - Stripe-Signature header - * @param {string} secret - Webhook secret (optional, uses env if not provided) - * @returns {Promise} Verified event - */ -export async function verifyWebhookSignature(payload, signature, secret = null) { - const stripe = await getStripe(); - const webhookSecret = secret || process.env.STRIPE_WEBHOOK_SECRET; - +export async function verifyWebhookSignature(payload, signature) { + const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; + if (!webhookSecret) { throw new Error('Stripe webhook secret is not configured'); } - - return stripe.webhooks.constructEvent(payload, signature, webhookSecret); + + return getStripe().webhooks.constructEvent(payload, signature, webhookSecret); } -/** - * Create a customer - * @param {Object} options - Customer options - * @param {string} options.email - Customer email - * @param {string} options.name - Customer name - * @param {Object} options.metadata - Additional metadata - * @returns {Promise} Stripe customer - */ export async function createCustomer(options) { - const stripe = await getStripe(); - const { email, name, metadata = {} } = options; - - return await stripe.customers.create({ - email, - name, - metadata, - }); + return getStripe().customers.create({ email, name, metadata }); } /** * Get or create a customer by email. * - * TOCTOU note: Stripe does not provide a native atomic upsert. To minimise + * TOCTOU note: Stripe does not provide a native atomic upsert. To minimise * the race-condition window where two concurrent calls both create a customer * for the same email, we use an idempotency key derived from the email address. * If a duplicate is created despite this guard (extremely unlikely), operators * should merge duplicates via the Stripe dashboard or a reconciliation job. - * - * @param {string} email - Customer email - * @param {Object} defaultData - Default data if creating new customer - * @returns {Promise} Stripe customer */ export async function getOrCreateCustomer(email, defaultData = {}) { - const stripe = await getStripe(); + const stripe = getStripe(); - // Search for existing customer first. - const existing = await stripe.customers.list({ - email, - limit: 1, - }); + const existing = await stripe.customers.list({ email, limit: 1 }); + if (existing.data.length > 0) return existing.data[0]; - if (existing.data.length > 0) { - return existing.data[0]; - } - - // Use a deterministic idempotency key so that concurrent requests for the - // same email address result in a single Stripe customer even if both pass - // the list-check above before either create completes. - const { createHash } = await import('crypto'); const idempotencyKey = 'get-or-create-customer-' + createHash('sha256').update(email).digest('hex'); - - return await stripe.customers.create( - { email, ...defaultData }, - { idempotencyKey } - ); + return stripe.customers.create({ email, ...defaultData }, { idempotencyKey }); } -/** - * List customer's payment methods - * @param {string} customerId - Customer ID - * @param {string} type - Payment method type (default: 'card') - * @returns {Promise} List of payment methods - */ export async function listPaymentMethods(customerId, type = 'card') { - const stripe = await getStripe(); - - const methods = await stripe.paymentMethods.list({ - customer: customerId, - type, - }); - + const methods = await getStripe().paymentMethods.list({ customer: customerId, type }); return methods.data; } -/** - * Create a refund - * @param {Object} options - Refund options - * @param {string} options.paymentIntentId - Payment intent to refund - * @param {number} options.amount - Amount to refund in cents (optional, full refund if not specified) - * @param {string} options.reason - Reason for refund - * @returns {Promise} Stripe refund - */ export async function createRefund(options) { - const stripe = await getStripe(); - const { paymentIntentId, amount, reason } = options; - - const refundConfig = { - payment_intent: paymentIntentId, - }; - - if (amount) { - refundConfig.amount = amount; - } - - if (reason) { - refundConfig.reason = reason; - } - - return await stripe.refunds.create(refundConfig); -} -// Default export for convenience -export default { - getStripe, - isEnabled, - getPublishableKey, - createCheckoutSession, - createPaymentIntent, - getCheckoutSession, - getPaymentIntent, - verifyWebhookSignature, - createCustomer, - getOrCreateCustomer, - listPaymentMethods, - createRefund, -}; + const refundConfig = { payment_intent: paymentIntentId }; + if (amount) refundConfig.amount = amount; + if (reason) refundConfig.reason = reason; + + return getStripe().refunds.create(refundConfig); +}