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
JavaScript
;
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