UNPKG

vendure-plugin-nowpayments

Version:

A cryptocurrency payment gateway plugin for Vendure that integrates with NOWPayments.io, enabling your store to accept payments in Bitcoin, Ethereum, and 100+ other cryptocurrencies.

482 lines (481 loc) 25.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "NOWPaymentsService", { enumerable: true, get: function() { return NOWPaymentsService; } }); const _extends = require("@swc/helpers/_/_extends"); const _interop_require_wildcard = require("@swc/helpers/_/_interop_require_wildcard"); const _ts_decorate = require("@swc/helpers/_/_ts_decorate"); const _ts_metadata = require("@swc/helpers/_/_ts_metadata"); const _ts_param = require("@swc/helpers/_/_ts_param"); const _common = require("@nestjs/common"); const _core = require("@vendure/core"); const _constants = require("./constants"); const _types = require("./types"); const _crypto = /*#__PURE__*/ _interop_require_wildcard._(require("crypto")); let NOWPaymentsService = class NOWPaymentsService { get useInvoices() { return this.options.useInvoices || false; } get apiKey() { return this.options.apiKey || ''; } get ipnSecret() { return this.options.ipnSecret || ''; } get invoicePrefix() { return this.options.invoicePrefix || 'VC-'; } get simpleTotal() { return this.options.simpleTotal || false; } get host() { return this.options.host || 'http://localhost:3000'; } get sandbox() { return this.options.sandbox || false; } get isFixedRate() { return this.options.is_fixed_rate || false; } get isFeePaidByUser() { return this.options.is_fee_paid_by_user || false; } /** * Creates a payment intent (payment URL or invoice URL) for the given order. * This is similar to Stripe's createPaymentIntent but returns a URL instead of a client secret. */ async createPaymentIntent(ctx, order) { const orderWithLines = await this.getOrderWithLines(ctx, order); if (this.useInvoices) { return this.generateInvoiceUrl(ctx, orderWithLines); } else { return this.generatePaymentUrl(ctx, orderWithLines); } } async generatePaymentUrl(ctx, order) { const paymentData = await this.getPaymentData(ctx, order); const jsonData = JSON.stringify(paymentData); const encodedData = encodeURIComponent(jsonData); const baseUrl = this.sandbox ? 'https://sandbox.nowpayments.io' : 'https://nowpayments.io'; return `${baseUrl}/payment?data=${encodedData}`; } async generateInvoiceUrl(ctx, order) { const invoiceData = await this.getInvoiceData(ctx, order); try { const baseUrl = this.sandbox ? 'https://api-sandbox.nowpayments.io' : 'https://api.nowpayments.io'; // Log the request data for debugging _core.Logger.info(`Creating NOWPayments invoice for order ${order.code} - URL: ${baseUrl}/v1/invoice, Sandbox: ${this.sandbox}, Data: ${JSON.stringify(invoiceData)}`, _constants.loggerCtx); // Validate required fields before sending const requiredFields = [ 'ipn_callback_url', 'price_currency', 'success_url', 'cancel_url', 'order_id', 'price_amount' ]; const missingFields = requiredFields.filter((field)=>!invoiceData[field]); if (missingFields.length > 0) { _core.Logger.error(`Missing required fields for invoice creation: ${missingFields.join(', ')} - Order: ${order.code}`, _constants.loggerCtx); throw new Error(`Missing required fields: ${missingFields.join(', ')}`); } // Log the exact request being sent _core.Logger.info(`Sending invoice request to NOWPayments - Headers: ${JSON.stringify({ 'Content-Type': 'application/json', 'X-Api-Key': this.apiKey ? '***' + this.apiKey.slice(-4) : 'missing' })} - Body: ${JSON.stringify(invoiceData)}`, _constants.loggerCtx); const response = await fetch(`${baseUrl}/v1/invoice`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Api-Key': this.apiKey }, body: JSON.stringify(invoiceData) }); if (!response.ok) { // Get the response body for detailed error information let errorBody = ''; try { errorBody = await response.text(); } catch (e) { errorBody = 'Unable to read response body'; } // Get response headers for debugging const responseHeaders = {}; response.headers.forEach((value, key)=>{ responseHeaders[key] = value; }); // Log detailed error information const apiKeySuffix = this.apiKey ? '***' + this.apiKey.slice(-4) : 'missing'; _core.Logger.error(`Invoice creation failed: ${response.status} ${response.statusText} - Response: ${errorBody} - Request: ${JSON.stringify(invoiceData)} - API Key: ${apiKeySuffix} - URL: ${baseUrl}/v1/invoice - Order: ${order.code} - Order ID: ${invoiceData.order_id} - Amount: ${invoiceData.price_amount} ${invoiceData.price_currency} - Response Headers: ${JSON.stringify(responseHeaders)}`, _constants.loggerCtx); throw new Error(`Invoice creation failed: ${response.status} ${response.statusText} - ${errorBody}`); } const data = await response.json(); // Log the full API response for debugging _core.Logger.info(`NOWPayments API response for order ${order.code}: ${JSON.stringify(data)}`, _constants.loggerCtx); // Try to extract invoice ID from different possible field names const invoiceId = data.invoice_id || data.id || data.payment_id || data.iid || 'unknown'; // Log successful invoice creation _core.Logger.info(`Successfully created NOWPayments invoice for order ${order.code} - Invoice URL: ${data.invoice_url} - Invoice ID: ${invoiceId}`, _constants.loggerCtx); return data.invoice_url; } catch (error) { _core.Logger.error(`Failed to create NOWPayments invoice: ${error.message} - Order: ${order.code} - Request Data: ${JSON.stringify(invoiceData)}`, _constants.loggerCtx); throw error; } } async getOrderWithLines(ctx, order) { const orderWithLines = await this.orderService.findOne(ctx, order.id, [ 'lines', 'lines.productVariant' ]); if (!orderWithLines) { throw new Error(`Order not found: ${order.id}`); } return orderWithLines; } async getOrderCustomer(ctx, order) { if (!order.customerId) { throw new Error('Order has no customer'); } const customer = await this.customerService.findOne(ctx, order.customerId); if (!customer) { throw new Error('Order customer not found'); } return customer; } async getPaymentData(ctx, order) { const customer = await this.getOrderCustomer(ctx, order); const orderId = this.invoicePrefix + order.code; const total = this.simpleTotal ? order.total : order.totalWithTax; const paymentData = { dataSource: 'vendure', ipnURL: this.getIpnUrl(), paymentCurrency: order.currencyCode, successURL: this.getSuccessUrl(order), cancelURL: this.getCancelUrl(order), orderID: orderId, apiKey: this.apiKey, customerName: customer.firstName || '', customerEmail: customer.emailAddress, paymentAmount: (total / 100).toFixed(8), tax: this.simpleTotal ? 0 : order.lines.reduce((sum, line)=>sum + (line.linePriceWithTax - line.linePrice), 0) / 100, shipping: order.shipping / 100, products: order.lines.map((line)=>({ name: line.productVariant.name, quantity: line.quantity, price: line.unitPrice / 100, total: line.linePriceWithTax / 100 })), is_fixed_rate: this.isFixedRate, is_fee_paid_by_user: this.isFeePaidByUser }; return paymentData; } async getInvoiceData(ctx, order) { const customer = await this.getOrderCustomer(ctx, order); const orderId = this.invoicePrefix + order.code; const total = this.simpleTotal ? order.total : order.totalWithTax; const description = { customerName: customer.firstName || '', customerEmail: customer.emailAddress, tax: this.simpleTotal ? 0 : order.lines.reduce((sum, line)=>sum + (line.linePriceWithTax - line.linePrice), 0) / 100, shipping: order.shipping / 100 }; const invoiceData = { // source: 'vendure', ipn_callback_url: this.getIpnUrl(), price_currency: order.currencyCode, success_url: this.getSuccessUrl(order), cancel_url: this.getCancelUrl(order), order_id: orderId, order_description: JSON.stringify(description), price_amount: (total / 100).toFixed(8), is_fixed_rate: this.isFixedRate, is_fee_paid_by_user: this.isFeePaidByUser }; return invoiceData; } getIpnUrl() { // This should be your server's IPN endpoint return `${this.host}/nowpayments/ipn`; } getSuccessUrl(order) { var _this_options_getSuccessUrl; const getSuccessUrl = (_this_options_getSuccessUrl = this.options.getSuccessUrl) != null ? _this_options_getSuccessUrl : (order, host)=>`${host}/checkout/confirmation/${order.code}`; return getSuccessUrl(order, this.host); } getCancelUrl(order) { var _this_options_getCancelUrl; const getCancelUrl = (_this_options_getCancelUrl = this.options.getCancelUrl) != null ? _this_options_getCancelUrl : (order, host)=>`${host}/checkout/cancel/${order.code}`; return getCancelUrl(order, this.host); } async processIpn(ctx, ipnData, signature) { try { // Verify HMAC signature if (!this.verifySignature(ipnData, signature)) { _core.Logger.error('Invalid IPN signature'); return false; } // Find the order by code (not ID) const orderCode = ipnData.order_id.replace(this.invoicePrefix, ''); const order = await this.orderService.findOneByCode(ctx, orderCode); if (!order) { _core.Logger.error(`Order not found: ${orderCode}`); return false; } const orderWithPayments = await this.orderService.findOne(ctx, order.id, [ 'payments' ]); // Load the order with payments relation using repository // const orderRepo = this.connection.getRepository(ctx, Order); // const orderWithPayments = await orderRepo.findOne({ // where: { id: order.id }, // relations: ['payments'] // }); if (!orderWithPayments) { _core.Logger.error(`Order with payments not found: ${order.id}`); return false; } // Process payment status await this.processPaymentStatus(ctx, orderWithPayments, ipnData); return true; } catch (error) { _core.Logger.error(`IPN processing error: ${error.message}`); return false; } } verifySignature(data, receivedSignature) { // Sort the data keys alphabetically const sortedData = Object.keys(data).sort().reduce((result, key)=>{ result[key] = data[key]; return result; }, {}); const sortedJson = JSON.stringify(sortedData); const calculatedSignature = _crypto.createHmac('sha512', this.ipnSecret).update(sortedJson).digest('hex'); return calculatedSignature === receivedSignature; } async processPaymentStatus(ctx, order, ipnData) { const paymentStatus = ipnData.payment_status; _core.Logger.info(`Processing payment status: ${paymentStatus}`, _constants.loggerCtx); const payment = order.payments.find((p)=>p.method === 'nowpayments' && p.state === 'Authorized'); if (!payment) { _core.Logger.error(`No NOWPayments payment found for order: ${order.code}`); return; } // Store the full IPN response in metadata const fullIpnData = _extends._({}, ipnData, { processed_at: new Date().toISOString() }); switch(paymentStatus){ case 'finished': { _core.Logger.info(`Processing 'finished' payment status for order ${order.code}`, _constants.loggerCtx); // Use PaymentService to properly settle the payment await this.paymentService.settlePayment(ctx, payment.id); // Reload the payment to get the updated state using PaymentService const settledPayment = await this.paymentService.findOneOrThrow(ctx, payment.id); // Update metadata on the reloaded payment settledPayment.metadata = _extends._({}, settledPayment.metadata, { settled: true, paymentStatus: 'finished', fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); // Save the updated metadata await this.connection.getRepository(ctx, _core.Payment).save(settledPayment); // Transition order to PaymentSettled state await this.orderService.transitionToState(ctx, order.id, 'PaymentSettled'); _core.Logger.info(`Payment settled successfully and order transitioned to PaymentSettled - Order: ${order.code}`, _constants.loggerCtx); break; } case 'partially_paid': _core.Logger.info(`Processing 'partially_paid' payment status for order ${order.code}`, _constants.loggerCtx); // Keep payment in Authorized state but update metadata payment.metadata = _extends._({}, payment.metadata, { paymentStatus: 'partially_paid', fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); _core.Logger.info(`Payment metadata updated for partially_paid status - Order: ${order.code}`, _constants.loggerCtx); break; case 'confirming': _core.Logger.info(`Processing 'confirming' payment status for order ${order.code}`, _constants.loggerCtx); // Keep payment in Authorized state but update metadata payment.metadata = _extends._({}, payment.metadata, { paymentStatus, fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); _core.Logger.info(`Payment metadata updated for confirming status - Order: ${order.code}`, _constants.loggerCtx); break; case 'confirmed': _core.Logger.info(`Processing 'confirmed' payment status for order ${order.code}`, _constants.loggerCtx); // Keep payment in Authorized state but update metadata payment.metadata = _extends._({}, payment.metadata, { paymentStatus, fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); _core.Logger.info(`Payment metadata updated for confirmed status - Order: ${order.code}`, _constants.loggerCtx); break; case 'sending': _core.Logger.info(`Processing 'sending' payment status for order ${order.code}`, _constants.loggerCtx); // Keep payment in Authorized state but update metadata payment.metadata = _extends._({}, payment.metadata, { paymentStatus, fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); _core.Logger.info(`Payment metadata updated for sending status - Order: ${order.code}`, _constants.loggerCtx); break; case 'waiting': _core.Logger.info(`Processing 'waiting' payment status for order ${order.code}`, _constants.loggerCtx); // Payment is waiting for user action (e.g., user needs to complete payment) // Keep payment in Authorized state but update metadata payment.metadata = _extends._({}, payment.metadata, { paymentStatus, fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); _core.Logger.info(`Payment metadata updated for waiting status - Order: ${order.code}`, _constants.loggerCtx); break; case 'expired': { _core.Logger.info(`Processing 'expired' payment status for order ${order.code}`, _constants.loggerCtx); // Payment has expired, cancel the payment await this.paymentService.cancelPayment(ctx, payment.id); // Reload the payment to get the updated state using PaymentService const expiredPayment = await this.paymentService.findOneOrThrow(ctx, payment.id); // Update metadata on the reloaded payment expiredPayment.metadata = _extends._({}, expiredPayment.metadata, { paymentStatus: 'expired', fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); // Save the updated metadata await this.connection.getRepository(ctx, _core.Payment).save(expiredPayment); // Cancel the order completely await this.orderService.transitionToState(ctx, order.id, 'Cancelled'); _core.Logger.info(`Payment cancelled due to expired status and order cancelled - Order: ${order.code}`, _constants.loggerCtx); break; } case 'failed': { _core.Logger.info(`Processing 'failed' payment status for order ${order.code}`, _constants.loggerCtx); // Use PaymentService to properly decline the payment await this.paymentService.cancelPayment(ctx, payment.id); // Reload the payment to get the updated state using PaymentService const failedPayment = await this.paymentService.findOneOrThrow(ctx, payment.id); // Update metadata on the reloaded payment failedPayment.metadata = _extends._({}, failedPayment.metadata, { paymentStatus: 'failed', fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); // Save the updated metadata await this.connection.getRepository(ctx, _core.Payment).save(failedPayment); // Cancel the order completely await this.orderService.transitionToState(ctx, order.id, 'Cancelled'); _core.Logger.info(`Payment cancelled due to failed status and order cancelled - Order: ${order.code}`, _constants.loggerCtx); break; } default: _core.Logger.warn(`Unknown payment status: ${paymentStatus} for order ${order.code}`, _constants.loggerCtx); // Still store the IPN data even for unknown statuses payment.metadata = _extends._({}, payment.metadata, { paymentStatus, fullIpnData, outcome_currency: ipnData.outcome_currency, outcome_amount: ipnData.outcome_amount, pay_currency: ipnData.pay_currency, pay_amount: ipnData.pay_amount, actually_paid: ipnData.actually_paid, payment_id: ipnData.payment_id, invoice_id: ipnData.invoice_id }); _core.Logger.info(`Payment metadata updated for unknown status: ${paymentStatus} - Order: ${order.code}`, _constants.loggerCtx); } // Only save metadata updates for payments that don't change state // State-changing payments (finished, expired, failed) are already saved above if (![ 'finished', 'expired', 'failed' ].includes(paymentStatus)) { await this.connection.getRepository(ctx, _core.Payment).save(payment); } } constructor(orderService, paymentService, customerService, connection, options){ this.orderService = orderService; this.paymentService = paymentService; this.customerService = customerService; this.connection = connection; this.options = options; } }; NOWPaymentsService = _ts_decorate._([ (0, _common.Injectable)(), _ts_param._(4, (0, _common.Inject)(_constants.NOWPAYMENTS_PLUGIN_OPTIONS)), _ts_metadata._("design:type", Function), _ts_metadata._("design:paramtypes", [ typeof _core.OrderService === "undefined" ? Object : _core.OrderService, typeof _core.PaymentService === "undefined" ? Object : _core.PaymentService, typeof _core.CustomerService === "undefined" ? Object : _core.CustomerService, typeof _core.TransactionalConnection === "undefined" ? Object : _core.TransactionalConnection, typeof _types.PluginInitOptions === "undefined" ? Object : _types.PluginInitOptions ]) ], NOWPaymentsService); //# sourceMappingURL=nowpayments.service.js.map