paypal-handler
Version:
890 lines (846 loc) • 27.1 kB
JavaScript
const paypal = require("paypal-rest-sdk");
const {
validateConfigurePaypal,
validateOneTimePaymentPlan,
validateRecurringPaymentPlan,
validateFixedRecurringPayment,
validateInstallmentsPayment,
validateExecutePayment,
validateBillingAgreementExecute,
} = require("../validation/paypal");
const { GET_DISCOUNTED_AMOUNTS } = require("./helpers/general_helpers");
/**
*
* @param {
* mode: "sandbox" | "live",
* client_id: string,
* client_secret: string
* } body
*
* @description This function is used to configure the paypal sdk with the given credentials
* @returns
*/
/************************************************** Configure PayPal ******************************** */
const configurePaypal = async (body) => {
let { error, message } = validateConfigurePaypal(body);
if (error) {
return { error: true, message: message, response: null };
}
try {
paypal.configure({
mode: body.mode ?? "sandbox", // 'sandbox' or 'live'
client_id: body.client_id,
client_secret: body.client_secret,
});
return { error: false, message: "", response: paypal };
} catch (error) {
return { error: true, message: error.message, response: null };
}
};
/*****************************************************************************************************/
/**
@param {
amount: number,
currency: string,
discount_type: string,
discount: number,
tax: number,
return_url: string
} body
@description This function is used to create a one-time payment link
@returns { error: boolean, message: string, response:
{ payment: object, link: string } | null }
*/
/************************************************ Create One Time Payment Plan ******************************/
const createPaymentPlanOneTime = (body, paypal) => {
//validate the body with joi
let { error, message } = validateOneTimePaymentPlan(body);
if (error) {
return { error: true, message: message, response: null };
}
body.currency = body.currency.toUpperCase();
if (body.discount > 0) {
if (body.discount_type == "percentage") {
let discount_amount = (body.discount / 100) * body.amount;
body.amount = (body.amount - discount_amount).toFixed(1);
body.amount = parseFloat(body.amount);
} else {
body.amount = (body.amount - body.discount).toFixed(1);
body.amount = parseFloat(body.amount);
}
}
if (body.tax > 0) {
tax = body.amount * (body.tax / 100);
body.amount = tax + body.amount;
}
return new Promise((resolve) => {
const create_payment_json = {
intent: "sale",
payer: {
payment_method: "paypal",
},
redirect_urls: {
return_url: body.return_url,
cancel_url: body.cancel_url,
},
transactions: [
{
item_list: {
items: [
{
name: "item",
sku: "item",
price: body.amount,
currency: body.currency,
quantity: 1,
},
],
},
amount: {
currency: body.currency,
total: body.amount,
details: {
subtotal: body.amount,
tax: "0.00", // Include tax if applicable
shipping: "0.00", // Include shipping if applicable
},
},
description: body.description,
},
],
};
paypal.payment.create(create_payment_json, (error, payment) => {
if (error) {
console.log(error, "error in one-time PayPal payment..............");
resolve({
error: true,
message: error.response
? error.response.details
: "Could not make payment",
response: null,
});
} else {
let link = "";
console.log("Create Payment Response", payment);
for (let i = 0; i < payment.links.length; i++) {
if (payment.links[i].rel == "approval_url") {
link = payment.links[i];
}
}
resolve({ error: false, message: "", response: { payment, link } });
}
});
});
};
/***********************************************************************************************************/
/**
* @param {
* amount: number,
* currency: string,
* frequency: string,
* plan_name: string,
* trial_period_days: number,
* return_url: string,
* discount_type: string,
* discount: number,
* tax: number,
* custom_days: number
* } body
*
* @description This function is used to create a recurring payment plan
* @returns { error: boolean, message: string, response:
* { billingAgreement: object, link: string } | null }
*
*/
const createPaymentPlanRecurring = async (body, paypal) => {
//validate the body with joi
let { error, message } = validateRecurringPaymentPlan(body);
if (error) {
return { error: true, message: message, response: null };
}
let link = "";
body.currency = body.currency.toUpperCase();
if (body.discount > 0) {
if (body.discount_type == "percentage") {
let discount_amount = (body.discount / 100) * body.amount;
body.amount = (body.amount - discount_amount).toFixed(1);
body.amount = parseFloat(body.amount);
} else {
body.amount = (body.amount - body.discount).toFixed(1);
body.amount = parseFloat(body.amount);
}
}
if (body.tax > 0) {
tax = body.amount * (body.tax / 100);
body.amount = tax + body.amount;
}
let custom_days = 1;
if (body.frequency == "custom") {
body.frequency = "DAY";
custom_days = body.custom_days;
}
return new Promise(async (resolve) => {
try {
let UppercaseFrequency = body.frequency.toUpperCase();
let payment_def = [
{
name: "Regular Payments",
type: "REGULAR",
frequency: UppercaseFrequency,
frequency_interval: String(custom_days),
cycles: "0",
amount: {
currency: body.currency,
value: body.amount,
},
},
];
// in case of trial period
if (body.trial_period_days > 0) {
payment_def.unshift({
// Use unshift to place the trial period first
name: "Trial Period",
type: "TRIAL",
frequency: "DAY",
frequency_interval: "1",
cycles: body.trial_period_days.toString(), // Convert trial period days to string
amount: {
currency: body.currency,
value: "0.00", // Free trial
},
});
}
// Define the payment plan
const create_payment_json = {
name: body.plan_name,
description: "Monthly subscription",
type: "INFINITE",
payment_definitions: payment_def,
merchant_preferences: {
setup_fee: {
currency: body.currency,
value: "0.00",
},
cancel_url: body.cancel_url,
return_url: body.return_url,
max_fail_attempts: "1",
auto_bill_amount: "YES",
initial_fail_amount_action: "CONTINUE",
},
};
// Create the payment plan
const payment = await new Promise((resolve, reject) => {
paypal.billingPlan.create(create_payment_json, (error, payment) => {
if (error) {
reject(
error.response ? error.response.details : "Could not make payment"
);
} else {
resolve(payment);
}
});
});
// Activate the payment plan
const billing_plan_update_attributes = [
{
op: "replace",
path: "/",
value: {
state: "ACTIVE",
},
},
];
await new Promise((resolve, reject) => {
paypal.billingPlan.update(
payment.id,
billing_plan_update_attributes,
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
}
);
});
let start_date = new Date(Date.now() + 60000).toISOString();
if (body.start_date) {
start_date = body.start_date;
}
// Define the billing agreement
const billing_agreement_attributes = {
name: "Recurring Agreement Name",
description: "Recurring Agreement Description",
start_date: start_date, // Start date of the agreement
plan: {
id: payment.id,
},
payer: {
payment_method: "paypal",
},
};
// Create the billing agreement
const billingAgreement = await new Promise((resolve, reject) => {
paypal.billingAgreement.create(
billing_agreement_attributes,
(error, billingAgreement) => {
if (error) {
reject(
error.response
? error.response.details
: "Could not create billing agreement"
);
} else {
resolve(billingAgreement);
}
}
);
});
for (let i = 0; i < billingAgreement.links.length; i++) {
if (billingAgreement.links[i].rel == "approval_url") {
link = billingAgreement.links[i];
}
}
// Resolve the billing agreement
resolve({
error: false,
message: "",
response: { billingAgreement, link },
});
} catch (error) {
resolve({ error: true, message: error.message, response: null });
}
});
};
/**
* @param {
* amount: number,
* currency: string,
* frequency: string,
* plan_name: string,
* trial_period_days: number,
* cycles: number,
* return_url: string,
* discount_type: string,
* discount: number,
* tax: number,
* custom_days: number
* } body
*
* @description This function is used to create a fixed recurring payment plan
* @returns { error: boolean, message: string, response:
* { billingAgreement: object, link: string } | null }
* */
const createPaymentFixedRecurring = async (body, paypal) => {
//validate the body with joi
let { error, message } = validateFixedRecurringPayment(body);
if (error) {
return { error: true, message: message, response: null };
}
let link = "";
body.currency = body.currency.toUpperCase();
if (body.discount > 0) {
if (body.discount_type == "percentage") {
let discount_amount = (body.discount / 100) * body.amount;
body.amount = (body.amount - discount_amount).toFixed(1);
body.amount = parseFloat(body.amount);
} else {
body.amount = (body.amount - body.discount).toFixed(1);
body.amount = parseFloat(body.amount);
}
}
if (body.tax > 0) {
tax = body.amount * (body.tax / 100);
body.amount = tax + body.amount;
}
if (body.cycles > 0) {
body.amount = body.amount / body.cycles;
}
let custom_days = 1;
if (body.frequency == "custom") {
body.frequency = "DAY";
custom_days = body.custom_days;
}
return new Promise(async (resolve) => {
try {
let UppercaseFrequency = body.frequency.toUpperCase();
let payment_def = [
{
name: "Regular Payments",
type: "REGULAR",
frequency: UppercaseFrequency,
frequency_interval: String(custom_days),
cycles: body.cycles,
amount: {
currency: body.currency,
value: body.amount,
},
},
];
// in case of trial period
if (body.trial_period_days > 0) {
payment_def.unshift({
// Use unshift to place the trial period first
name: "Trial Period",
type: "TRIAL",
frequency: "DAY",
frequency_interval: "1",
cycles: body.trial_period_days.toString(), // Convert trial period days to string
amount: {
currency: body.currency,
value: "0.00", // Free trial
},
});
}
const create_payment_json = {
name: body.plan_name,
description: "Monthly subscription plan description",
type: "FIXED",
payment_definitions: payment_def,
merchant_preferences: {
setup_fee: {
currency: body.currency,
value: "0.00",
},
cancel_url: body.return_url,
return_url: body.return_url,
max_fail_attempts: "1",
auto_bill_amount: "YES",
initial_fail_amount_action: "CONTINUE",
},
};
const payment = await new Promise((resolve, reject) => {
paypal.billingPlan.create(create_payment_json, (error, payment) => {
if (error) {
reject(
error.response ? error.response.details : "Could not make payment"
);
} else {
resolve(payment);
}
});
});
const billing_plan_update_attributes = [
{
op: "replace",
path: "/",
value: {
state: "ACTIVE", // Activate the billing plan
},
},
];
await new Promise((resolve, reject) => {
paypal.billingPlan.update(
payment.id,
billing_plan_update_attributes,
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
}
);
});
let start_date = new Date(Date.now() + 60000).toISOString();
if (body.start_date) {
start_date = body.start_date;
}
const billing_agreement_attributes = {
name: "Recurring Agreement Name",
description: "Recurring Agreement Description",
start_date: start_date, // Start date of the agreement (1 minute in the future)
plan: {
id: payment.id,
},
payer: {
payment_method: "paypal",
},
};
const billingAgreement = await new Promise((resolve, reject) => {
paypal.billingAgreement.create(
billing_agreement_attributes,
(error, billingAgreement) => {
if (error) {
reject(
error.response
? error.response.details
: "Could not create billing agreement"
);
} else {
resolve(billingAgreement);
}
}
);
});
for (let i = 0; i < billingAgreement.links.length; i++) {
if (billingAgreement.links[i].rel == "approval_url") {
link = billingAgreement.links[i];
}
}
resolve({
error: false,
message: "",
response: { billingAgreement, link },
});
} catch (error) {
resolve({ error: true, message: error.message, response: null });
}
});
};
/********************************************* Create Installments Payment Plan ******************************/
const createPaymentInstallments = async (body, paypal) => {
let { error, message } = validateInstallmentsPayment(body);
if (error) {
console.log(error);
return { error: true, message: message, response: null };
}
body.currency = body.currency.toUpperCase();
// Apply Discount ...
if (body.discount > 0) {
const { discounted_initial_amount, discounted_installment_amount } =
GET_DISCOUNTED_AMOUNTS(
body.discount,
body.discount_type,
body.initial_amount,
body.amount
);
body.initial_amount = discounted_initial_amount;
body.amount = discounted_installment_amount;
}
// Apply Tax ...
let tax = 0;
if (body.tax > 0) {
tax = body.amount * (body.tax / 100);
body.amount = tax + body.amount;
body.amount = body.amount.toFixed(2);
tax = body.initial_amount * (body.tax / 100);
body.initial_amount = tax + body.initial_amount;
body.initial_amount = body.initial_amount.toFixed(2);
}
if (body.cycles > 0) {
body.amount = body.amount / body.cycles;
body.amount = body.amount.toFixed(2);
}
let custom_days = 1;
if (body.frequency === "custom") {
body.frequency = "DAY";
custom_days = body.custom_days;
}
return new Promise(async (resolve) => {
try {
const UppercaseFrequency = body.frequency.toUpperCase();
const payment_definitions = [];
// Conditionally add a TRIAL phase if trial_period_days > 0
if (body.trial_period_days > 0) {
payment_definitions.push({
name: body.plan_name + " Trial Period",
type: "TRIAL",
frequency: "DAY",
frequency_interval: "1",
cycles: body.trial_period_days.toString(), // Number of days for trial
amount: {
currency: body.currency,
value: "0.00", // No charge during the trial period
},
});
}
// Add the REGULAR phase for the initial payment
payment_definitions.push({
name:
body.plan_name +
(body.trial_period_days > 0
? " Initial Charge"
: " Regular Payments"),
type: "REGULAR",
frequency: UppercaseFrequency,
frequency_interval: String(custom_days), // Number of days for each billing cycle
cycles: body.cycles.toString(), // Number of regular payments
amount: {
currency: body.currency,
value: body.amount, // Regular payment amount
},
});
const create_payment_json = {
name: body.plan_name,
description:
body.trial_period_days > 0
? "Subscription plan with trial, initial, and recurring payments"
: "Plan with initial payment and recurring payments",
type: "FIXED",
payment_definitions: payment_definitions,
merchant_preferences: {
setup_fee: {
currency: body.currency,
value: body.initial_amount, // Charge the setup fee immediately
},
cancel_url: body.cancel_url ? body.cancel_url : body.return_url,
return_url: body.return_url,
max_fail_attempts: "1",
auto_bill_amount: "YES",
initial_fail_amount_action: "CONTINUE",
},
};
// Create the payment plan
const payment = await new Promise((resolve, reject) => {
paypal.billingPlan.create(create_payment_json, (error, payment) => {
if (error) {
console.log(
JSON.stringify(error, null, 2),
"error in creating payment plan.............."
);
reject(
error.response
? error.response.details
: "Could not create payment plan"
);
} else {
resolve(payment);
}
});
});
// Activate the payment plan
const billing_plan_update_attributes = [
{
op: "replace",
path: "/",
value: {
state: "ACTIVE", // Activate the billing plan
},
},
];
await new Promise((resolve, reject) => {
paypal.billingPlan.update(
payment.id,
billing_plan_update_attributes,
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
}
);
});
// Calculate the start date for recurring payments (1 day after the initial payment)
const now = new Date();
const oneDayInMillis = 24 * 60 * 60 * 1000;
let startDate = new Date(now.getTime() + oneDayInMillis).toISOString();
if (body.start_date) {
startDate = body.start_date;
}
// Create the billing agreement
const billing_agreement_attributes = {
name: body.plan_name + " Agreement",
description:
body.trial_period_days > 0
? "Agreement for trial and recurring payments"
: "Agreement with initial payment and recurring payments",
start_date: startDate, // Start the recurring payments 1 day after the initial payment
plan: {
id: payment.id,
},
payer: {
payment_method: "paypal",
},
};
const billingAgreement = await new Promise((resolve, reject) => {
paypal.billingAgreement.create(
billing_agreement_attributes,
(error, billingAgreement) => {
if (error) {
reject(
error.response
? error.response.details
: "Could not create billing agreement"
);
} else {
resolve(billingAgreement);
}
}
);
});
let link = "";
for (let i = 0; i < billingAgreement.links.length; i++) {
if (billingAgreement.links[i].rel == "approval_url") {
link = billingAgreement.links[i];
}
}
resolve({
error: false,
message: "Sucessfully created billing agreement",
response: { billingAgreement, link },
}); // Return the billing agreement
} catch (error) {
resolve({ error: true, message: error.message, response: null });
}
});
};
/**************************************************************************************************************/
// Execute the payment
let executePayment = async (body, paypal) => {
//validate the body with joi
let { error, message } = validateExecutePayment(body);
if (error) {
return { error: true, message: message, response: null };
}
try {
let execute_payment_json = {
payer_id: body.payer_id,
};
const payment = await new Promise((resolve, reject) => {
paypal.payment.execute(
body.payment_id,
execute_payment_json,
(error, payment) => {
if (error) {
console.log(
error,
"error in one-time PayPal payment.............."
);
reject(error);
} else {
resolve(payment);
}
}
);
});
return { error: false, message: "", response: payment };
} catch (error) {
return { error: true, message: error.message, response: null };
}
};
let billingAgreementExecute = async (body, paypal) => {
//validate the body with joi
let { error, message } = validateBillingAgreementExecute(body);
if (error) {
return { error: true, message: message, response: null };
}
try {
const billing_agreement_execute_res = await new Promise(
(resolve, reject) => {
paypal.billingAgreement.execute(
body.token,
(error, billing_agreement_execute_res) => {
if (error) {
console.log(
error,
"error in executing recurring payment........"
);
reject(error);
} else {
if (body.trial_period_days > 0) {
// Handle trial period logic here if needed
}
resolve(billing_agreement_execute_res);
}
}
);
}
);
return {
error: false,
message: "",
response: billing_agreement_execute_res,
};
} catch (error) {
return { error: true, message: error.message, response: null };
}
};
/**************************************************************************************************************/
// Cancel the subscription
const cancelPaypalSubscription = async (subscriptionId, paypal) => {
return new Promise((resolve, reject) => {
paypal.billingAgreement.cancel(
subscriptionId,
{ note: "User requested cancellation" },
(error, response) => {
if (error) {
console.log("Error canceling subscription:", error);
reject(error);
} else {
resolve({
error: false,
message: "Subscription canceled successfully",
response,
});
}
}
);
});
};
const getLatestSaleId = async (subscriptionId, paypal) => {
const startDate = "2020-01-01"; // Adjust date range if needed
const endDate = "2099-12-31";
return new Promise((resolve, reject) => {
paypal.billingAgreement.searchTransactions(
subscriptionId,
startDate,
endDate,
(error, response) => {
if (error) {
console.error(
"❌ Error retrieving transactions:",
error.response || error
);
reject({
error: true,
message: "Could not retrieve transactions",
details: error.response || error.message,
});
} else {
const transactions = response?.agreement_transaction_list || [];
if (transactions.length === 0) {
return resolve({
error: true,
message: "No transactions found for this subscription.",
});
}
const latestTransaction = transactions[0]; // First transaction in the list is the latest
resolve({
error: false,
saleId: latestTransaction.transaction_id,
});
}
}
);
});
};
const refundPaypalPayment = async (body, paypal) => {
let saleId = body.saleId;
let refundAmount = body.refundAmount;
let currency = body.currency;
console.log("body", body);
const refundData = {
amount: {
total: refundAmount.toFixed(2), // Convert to 2 decimal places
currency: currency.toUpperCase(),
},
};
return new Promise((resolve, reject) => {
paypal.sale.refund(saleId, refundData, (error, refund) => {
if (error) {
console.log("Error processing refund:", error);
reject(error);
} else {
resolve({
error: false,
message: "Refund processed successfully",
response: refund,
});
}
});
});
};
module.exports = {
configurePaypal,
createPaymentPlanOneTime,
createPaymentPlanRecurring,
createPaymentFixedRecurring,
createPaymentInstallments,
executePayment,
billingAgreementExecute,
cancelPaypalSubscription,
getLatestSaleId,
refundPaypalPayment,
};