Files
node_learning_module_1/day11/services/PaymentService.js
T
ryanwong a07577bffa day 11
2022-02-06 22:15:10 -05:00

681 lines
26 KiB
JavaScript
Executable File

require('dotenv').config(); //require .env for stripe configurations
const moment = require('moment');
const stripe = require('./StripeApi'); //this is Payment Service
const paypal = require('./PaypalApi');
const db = require('../models');
const userModel = db.user;
const stripeSubscriptionsModel = db.stripe_subscriptions;
const stripeSubscriptionsLogModel = db.stripe_subscriptions_log;
const paymentPlansModel = db.payment_plans;
const stripeProductsModel = db.stripe_products;
const paymentServicesModel = db.payment_services;
const stripeCardsModel = db.stripe_cards;
module.exports = new Service();
function Service() {
this.userId = 0;
this.roleId = 0;
this.currency = process.env.STRIPE_CURRENCY;
this.prorate = process.env.STRIPE_PRORATE;
this.forceCancel = process.env.STRIPE_FORCE_CANCEL;
this.setUserId = function (userId) {
this.userId = userId;
};
this.setRoleId = function (roleId) {
this.roleId = roleId;
};
this.setCurrency = function (currency) {
this.currency = currency;
};
this.setProrationType = function (prorate) {
this.prorate = prorate;
};
this.setCancelType = function (forceCancel) {
this.forceCancel = forceCancel;
};
let convertToCents = function (amount) {
return amount * 100;
};
let handleDBError = function (error) {
console.error(error);
throw new Error('Internal error: Database error');
};
let handleRequestError = function (error) {
console.error(error);
throw new Error('Internal error: Request error');
};
this.subscribe = async function (subscriptionParams, plan) {
var subscription = await stripe.createSubscription(subscriptionParams);
//create model entry
let subscriptionModelEntry = {
stripe_id: subscription.id ?? '',
cancel_at_period_end: subscription.cancel_at_period_end ?? null,
current_period_start: moment(new Date(subscription.current_period_start * 1000)).format('YYYY-MM-DD') ?? null,
current_period_end: moment(new Date(subscription.current_period_end * 1000)).format('YYYY-MM-DD') ?? null,
user_id: this.userId ?? null,
role_id: this.roleId ?? null,
plan_id: plan?.id ?? null,
coupon_stripe_id: subscription.discount?.coupon?.id ?? '',
customer_stripe_id: subscription.customer ?? '',
collection_method: subscription.collection_method ?? '',
interval: stripeSubscriptionsModel.inverse_interval_mapping(subscription.plan?.interval) ?? 0,
interval_count: subscription.plan?.interval_count ?? '',
trial_period_days: subscription.plan?.trial_period_days ?? 0,
trial_end: subscription.trial_end ? moment(new Date(subscription.trial_end * 1000)).format('YYYY-MM-DD') : null,
trial_start: subscription.trial_start ? moment(new Date(subscription.subscriptiontrial_start * 1000)).format('YYYY-MM-DD') : null,
status: stripeSubscriptionsModel.inverse_status_mapping(subscription.status) ?? null,
};
let subscriptionCreatedId = await stripeSubscriptionsModel.insert(subscriptionModelEntry);
if (!subscriptionCreatedId) {
throw new Error('Subscription is not found.');
}
let subscriptionLogUpdated = await this.updateSubscriptionLog(subscriptionCreatedId, subscription.status, plan);
if (!subscriptionLogUpdated) {
throw new Error('Error while editing subscription log');
}
return subscriptionCreatedId;
};
this.updateSubscriptionLog = async function (subscriptionCreatedId, subscriptionCreatedStatus, plan) {
let subscriptionLog = await stripeSubscriptionsLogModel.getLast({ user_id: this.userId, role_id: this.roleId });
if (!subscriptionLog) {
let subscriptionLogModelEntry = {
user_id: this.userId,
role_id: this.roleId,
plan_id: plan?.id ?? null,
subscription_id: subscriptionCreatedId,
type: plan?.type ?? null,
status: subscriptionCreatedStatus == 'active' || subscriptionCreatedStatus == 'trialing' ? 1 : 0,
};
if (!(await stripeSubscriptionsLogModel.insert(subscriptionLogModelEntry))) {
throw new Error('Subscription log add not successfull');
}
} else {
let subscriptionLogModelEntry = {
plan_id: plan?.id ?? null,
subscription_id: subscriptionCreatedId,
type: plan?.type ?? null,
status: subscriptionCreatedStatus == 'active' || subscriptionCreatedStatus == 'trialing' ? 1 : 0,
};
if (!(await stripeSubscriptionsLogModel.edit(subscriptionLogModelEntry, subscriptionLog.id))) {
throw new Error('Subscription log edit not successfull');
}
}
return true;
};
this.createRegularSubscription = async function (user, plan, card, coupon) {
let subscriptionParams = {
customer: user.stripe_id,
items: [{ price: plan.stripe_id }],
trial_from_plan: true,
default_payment_method: card.stripe_card_id,
coupon: coupon?.stripe_id ?? null,
};
return await this.subscribe(subscriptionParams, plan);
};
this.createLifetimeSubscription = async function (user, plan, card, coupon) {
//create invoice, finalize and pay it.
let lifetimePlanStripeObject = await stripe.retrievePrice(plan.stripe_id);
let invoiceItemParams = {
customer: user.stripe_id,
price: plan.stripe_id,
discounts: [{ coupon: coupon?.stripe_id }],
};
let invoiceItemCreated = await stripe.createInvoiceItem(invoiceItemParams);
let invoiceParams = {
customer: user.stripe_id,
default_payment_method: card?.stripe_card_id ?? null,
};
let invoiceCreated = await stripe.createInvoice(invoiceParams);
let invoicePaid = await stripe.payInvoice(invoiceCreated.id);
let subscriptionModelEntry = {
stripe_id: invoiceItemCreated.id ?? '',
cancel_at_period_end: null,
current_period_start: moment().format('YYYY-MM-DD') ?? null,
current_period_end: null,
user_id: this.userId ?? null,
role_id: this.roleId ?? null,
plan_id: plan?.id ?? null,
coupon_stripe_id: invoiceItemCreated.discounts.length > 0 ? coupon?.stripe_id : '',
customer_stripe_id: invoicePaid.customer ?? '',
collection_method: invoicePaid.collection_method ?? '',
interval: 4,
interval_count: null,
trial_period_days: 0,
trial_end: null,
trial_start: null,
status: 4,
};
let subscriptionCreatedId = await stripeSubscriptionsModel.insert(subscriptionModelEntry);
if (!subscriptionCreatedId) {
throw new Error('Subscription is not found.');
}
let subscriptionLogUpdated = await this.updateSubscriptionLog(subscriptionCreatedId, 'active', plan);
if (!subscriptionLogUpdated) {
throw new Error('Error while editing subscription log');
}
return subscriptionCreatedId;
};
let calculateFullPeriod = function (interval, interval_count = 1) {
let totalNumberOfDays = paymentPlansModel.interval_days_mapping()[interval] * interval_count;
let cancelAtEpoch = moment().add(totalNumberOfDays, 'days').unix();
return cancelAtEpoch;
};
this.createTrialSubscription = async function (user, plan, card, coupon) {
let trialPlanFullPeriod = calculateFullPeriod(plan.interval, plan.interval_count);
let subscriptionParams = {
customer: user.stripe_id,
items: [{ price: plan.stripe_id }],
trial_from_plan: true,
default_payment_method: card.stripe_card_id,
coupon: coupon?.stripe_id ?? null,
cancel_at: trialPlanFullPeriod,
};
return await this.subscribe(subscriptionParams, plan);
};
this.cancelRegularSubscription = async function (subscription) {
if (this.forceCancel === 'true') {
var cancellationParams = {
subscriptionId: subscription.stripe_id,
params: {
invoice_now: true,
prorate: this.prorate,
},
};
let subscriptionCanceled = await stripe.cancelSubscription(cancellationParams);
let newStatus = stripeSubscriptionsModel.inverse_status_mapping()[subscriptionCanceled.status];
await stripeSubscriptionsModel.edit({ status: newStatus }, subscription.id);
} else {
let updateParams = {
subscriptionId: subscription.stripe_id,
params: {
cancel_at_period_end: true,
},
};
await stripe.updateSubscription(updateParams);
await stripeSubscriptionsModel.edit({ cancel_at_period_end: 1 }, subscription.id);
}
};
this.cancelLifetimeSubscription = async function (subscription) {
//TODO check if we refund on this or not
//we could make a fixed refund amount
//right now we just cancel it locally and refund is never as it is a onetime product thing.
let subscriptionEdited = await stripeSubscriptionsModel.edit({ status: 5 }, parseInt(subscription.id));
if (!subscriptionEdited) {
throw new Error("Couldn't edit subscription");
}
let subscriptionLog = await stripeSubscriptionsLogModel.getByFields({ subscription_id: parseInt(subscription.id), status: 1 });
if (!subscriptionLog) {
throw new Error("Couldn't find relative active subscription log");
}
let subscriptionLogEdited = await stripeSubscriptionsLogModel.edit({ status: 0 }, subscriptionLog.id);
if (subscriptionLogEdited) {
return true;
}
throw new Error("Couldn't Edit subscription log");
};
this.createCustomerWithoutCard = async function (params) {
let createdCustomer = await stripe.createCustomer(params);
let userModelEntry = {
stripe_id: createdCustomer.id,
};
let modelEditedUser = await this.userModel.edit(userModelEntry, this.userId);
if (!modelEditedUser) {
throw new Error('Internal error: Error editing user stripe id');
}
return modelEditedUser;
};
this.createCustomer = async function (params) {
let createdCustomer = await stripe.createCustomer(params);
let userModelEntry = {
stripe_id: createdCustomer.id,
};
let editedUserModel = await userModel.edit(userModelEntry, this.userId);
if (!editedUserModel) {
throw new Error('Error editing user stripe id');
}
let customerCardParams = {
customerId: createdCustomer.id,
cardId: createdCustomer.default_source,
};
let card = await stripe.retrieveCard(customerCardParams);
if (card) {
cardModelEntry = {
card_last: card.last4 ?? '',
card_brand: card.brand ?? '',
card_exp_month: card.exp_month ?? '',
exp_year: card.exp_year ?? '',
card_name: createdCustomer.metadata.card_name ?? '',
stripe_card_customer: card.customer,
stripe_card_id: card.id,
is_default: createdCustomer.metadata.is_default,
user_id: this.userId ?? null,
role_id: this.roleId ?? null,
};
var modelCreatedCard = await stripeCardsModel.insert(cardModelEntry);
if (!modelCreatedCard) {
throw new Error('Internal error: Error adding card');
}
}
return modelCreatedCard;
};
this.adminCreateProductPrice = async function (productStripeId, price, productLocalId) {
let priceArgs = {
unit_amount: convertToCents(price),
currency: this.currency,
product: productStripeId,
};
let priceCreated = await stripe.createPrice(priceArgs);
let productModelEditEntry = {
price,
price_stripe_id: priceCreated.id,
};
let modelEditedProduct = await stripeProductsModel.edit(productModelEditEntry, productLocalId);
if (!modelEditedProduct) {
throw new Error('Internal Error: error setting price');
}
return priceCreated;
};
this.adminCreateProduct = async function (stripeProductParam, paypalProductParam, localProductType) {
if (!stripeProductParam.name) {
throw new Error('Must provide a name for your product');
}
await paypal.setAccessToken();
let createdStripeProduct = await stripe.createProduct(stripeProductParam);
if (localProductType == 0) {
let productModelEntry = {
stripe_id: createdStripeProduct.id,
name: createdStripeProduct.name,
status: createdStripeProduct.active ? 1 : 0,
description: createdStripeProduct.description ?? '',
images: createdStripeProduct.images.join(';;;'),
shippable: createdStripeProduct.shippable ?? 0,
unit_label: createdStripeProduct.unit_label ?? '',
statement_descriptor: createdStripeProduct.statement_descriptor ?? '',
};
let modelCreatedStripeProduct = await stripeProductsModel.insert(productModelEntry);
if (!modelCreatedStripeProduct) {
throw new Error('Internal error: error adding a product');
}
return {
productStripeId: createdStripeProduct.id,
productLocalId: modelCreatedStripeProduct,
};
} else if (localProductType == 1) {
let createdPaypalProduct = await paypal.createProduct(paypalProductParam);
let paymentServiceModelEntry = {
name: createdStripeProduct.name,
stripe_id: createdStripeProduct.id,
paypal_id: createdPaypalProduct.data.id,
status: createdStripeProduct.active ? 1 : 0,
image: createdStripeProduct.images.length > 0 ? createdStripeProduct.images[0] : '',
url: createdStripeProduct.url ?? '',
category: createdStripeProduct.metadata.category,
description: createdStripeProduct.description ?? '',
};
let modelCreatedServiceId = await paymentServicesModel.insert(paymentServiceModelEntry);
let service = paymentServicesModel.getByPK(modelCreatedServiceId);
return {
serviceProductStripeId: service.stripe_id,
serviceProductPaypalId: service.paypal_id,
serviceProductLocalId: service.id,
};
}
return false;
};
/**
* [adminCreateSubscriptionPlan allows an admin to create subscription plan through admin portal]
* @param {object} planParams [object that contain plan parameters]
* @param {integer} planType [integer that represent plan type in relative to stripe plans table] @see /models/stripe_plans.js type_mapping()
* @param {integer} localProductId [integer that represent product id in relative to stripe products table]
* @return {integer} [the id of the created plan in database]
*/
this.adminCreateSubscriptionPlan = async function (params, planType, localServiceId) {
if (!params.nickname) {
throw new Error('Must have a display name');
}
if (!localServiceId) {
throw new Error('Must provide system product id');
}
if (!planType) {
throw new Error('Must provide plan type');
}
await paypal.setAccessToken();
let service = await paymentServicesModel.getByPK(localServiceId);
let stripeSubscriptionPlanParams = {
nickname: params.nickname,
currency: process.env.STRIPE_CURRENCY,
unit_amount: convertToCents(params.amount),
product: service.stripe_id,
recurring: {
interval: paymentPlansModel.interval_mapping()[params.interval],
interval_count: params.interval_count,
trial_period_days: params.trial_period_days,
},
active: parseInt(params.status) === 1 ? true : false,
};
let paypalSubscriptionPlanParams = {
product_id: service.paypal_id,
name: params.nickname,
billing_cycles: [
{
frequency: {
interval_unit: paymentPlansModel.interval_mapping()[params.interval].toUpperCase(),
interval_count: params.interval_count,
},
tenure_type: 'REGULAR',
sequence: 1,
total_cycles: 0,
pricing_scheme: {
fixed_price: {
value: params.amount,
currency_code: this.currency.toUpperCase(),
},
},
},
],
payment_preferences: {
auto_bill_outstanding: true,
setup_fee_failure_action: 'CANCEL',
payment_failure_threshold: 0,
},
};
if (params.trial_period_days > 0) {
paypalSubscriptionPlanParams.billing_cycles[0].sequence = 2;
paypalSubscriptionPlanParams.billing_cycles.unshift({
frequency: {
interval_unit: 'DAY',
interval_count: parseInt(params.trial_period_days),
},
tenure_type: 'TRIAL',
sequence: 1,
total_cycles: 1,
});
}
let stripePlanCreated = await stripe.createPrice(stripeSubscriptionPlanParams);
let paypalPlanCreated = await paypal.createSubscriptionPlan(paypalSubscriptionPlanParams);
let planModelEntry = {
stripe_product_id: stripePlanCreated.product,
paypal_product_id: paypalPlanCreated.data.product_id,
stripe_id: stripePlanCreated.id,
paypal_id: paypalPlanCreated.data.id,
amount: stripePlanCreated.unit_amount / 100,
interval: paymentPlansModel.inverse_interval_mapping(stripePlanCreated.recurring.interval),
interval_count: stripePlanCreated.recurring.interval_count,
trial_period_days: stripePlanCreated.recurring.trial_period_days ?? 0,
nickname: stripePlanCreated.nickname,
service_id: localServiceId,
status: stripePlanCreated.active ? 1 : 0,
type: parseInt(planType),
};
let modelCreatedPlan = await paymentPlansModel.insert(planModelEntry);
if (!modelCreatedPlan) {
throw new Error('Internal error: error creating plan');
}
return modelCreatedPlan;
};
this._createStripeLifetimePlan = async function (params, service) {
//stripe doesn't have lifetime plans
//how it works is a price is created with no recurring parameter as a single time product
//which acts and dealt with internally within our system as a lifetime plan.
let stripeSubscriptionPlanParams = {
nickname: params.nickname,
currency: this.currency,
unit_amount: convertToCents(params.amount),
product: service.stripe_id,
active: parseInt(params.status) === 1 ? true : false,
};
return await stripe.createPrice(stripeSubscriptionPlanParams);
};
this._createPaypalLifetimePlan = async function (params, service) {
//paypal doesn't allow lifetime plan
//how it works is i create a blan with a trial with 999 years and setup fee for subscription as the main amount for the lifetime plan
//paypal needs to have a regular plan so after the 999 years there is a regular plan with 0.01 amount money
//this works for both lifetime paid and free plans.
await paypal.setAccessToken();
let paypalSubscriptionPlanParams = {
product_id: service.paypal_id,
name: params.nickname,
billing_cycles: [
{
frequency: {
interval_unit: 'YEAR',
interval_count: 1,
},
tenure_type: 'TRIAL',
sequence: 1,
total_cycles: 999,
},
{
frequency: {
interval_unit: 'YEAR',
interval_count: 1,
},
tenure_type: 'REGULAR',
sequence: 2,
total_cycles: 0,
pricing_scheme: {
fixed_price: {
value: '0.01',
currency_code: this.currency.toUpperCase(),
},
},
},
],
payment_preferences: {
auto_bill_outstanding: true,
setup_fee: {
value: params.amount,
currency_code: this.currency.toUpperCase(),
},
setup_fee_failure_action: 'CANCEL',
payment_failure_threshold: 0,
},
};
return await paypal.createPlan(paypalSubscriptionPlanParams);
};
this._destroyStripeLifetimePlan = async function (planStripeId) {
//stripe doesn't allow deleting a price. will be set to unactive instead
return await stripe.updatePrice({ priceId: planStripeId, params: { active: false } });
};
this._destroyPaypalLifetimePlan = async function (planPaypalId) {
//paypal doesn't allow deleting a price. will be set to unactive instead
return await paypal.deactivatePlan(planPaypalId);
};
this.adminCreateLifetimePlan = async function (params, planType, localServiceId) {
if (!params.nickname) {
throw new Error('Must have a display name');
}
if (!localServiceId) {
throw new Error('Must provide system product id');
}
if (!planType) {
throw new Error('Must provide plan type');
}
if (parseInt(params.interval) !== 4) {
throw new Error('Interval must be "forever".');
}
let service = await paymentServicesModel.getByPK(localServiceId);
let stripePlanCreated = await this._createStripeLifetimePlan(params, service);
let paypalPlanCreated = await this._createPaypalLifetimePlan(params, service);
if (!stripePlanCreated && paypalPlanCreated) {
await this._destroyPaypalLifetimePlan(paypalPlanCreated.data.id);
throw new Error('Internal Error: Something happended while creating the plans.');
}
if (!paypalPlanCreated && stripePlanCreated) {
await this._destroyStripeLifetimePlan(stripePlanCreated.id);
throw new Error('Internal Error: Something happended while creating the plans.');
}
let planModelEntry = {
stripe_product_id: stripePlanCreated?.product ?? '',
paypal_product_id: paypalPlanCreated?.data?.product_id ?? '',
stripe_id: stripePlanCreated?.id ?? '',
paypal_id: paypalPlanCreated?.data?.id ?? '',
amount: stripePlanCreated?.unit_amount ? stripePlanCreated.unit_amount / 100 : params.amount,
interval: 4,
interval_count: 1,
trial_period_days: 0,
nickname: stripePlanCreated?.nickname ?? paypalPlanCreated.data.name,
service_id: localServiceId,
status: stripePlanCreated?.active == true ? 1 : params.status,
type: parseInt(planType),
};
let modelCreatedPlan = await paymentPlansModel.insert(planModelEntry);
if (!modelCreatedPlan) {
throw new Error('Internal error: error creating plan');
}
return modelCreatedPlan;
};
this.adminCreateTrialPlan = async function (planParams, planType, localServiceId) {
if (Object.keys(planParams.recurring).length === 0 || planParams.recurring.constructor !== Object) {
throw new Error('Must have a recurring parameter (subscription interval)');
}
if (!planParams.nickname) {
throw new Error('Must have a display name');
}
if (!localServiceId) {
throw new Error('Must provide system product id');
}
let priceCreated = await stripe.createPrice(planParams);
let planModelEntry = {
stripe_product_id: priceCreated.product,
stripe_id: priceCreated.id,
amount: priceCreated.unit_amount / 100,
interval: paymentPlansModel.inverse_interval_mapping(priceCreated.recurring.interval),
interval_count: priceCreated.recurring.interval_count,
trial_period_days: priceCreated.recurring.trial_period_days ?? 0,
nickname: priceCreated.nickname,
service_id: localServiceId,
status: priceCreated.active ? 1 : 0,
type: parseInt(planType),
};
let modelCreatedPlan = await paymentPlansModel.insert(planModelEntry);
if (!modelCreatedPlan) {
throw new Error('Internal error: error creating plan');
}
return modelCreatedPlan;
};
this.adminCancelSubscription = async function (subscriptionId) {
let subscriptionToCancel = await stripeSubscriptionsModel.getByPK(subscriptionId);
let subscriptionPlan = await paymentPlansModel.getByPK(subscriptionToCancel.plan_id);
if (!subscriptionToCancel || !subscriptionPlan) {
throw new Error('No subscription or plan of that id');
}
// let subscirptionType = subscriptionPlan.type;
let subscirptionType = 3;
//if normal subscription
switch (subscirptionType) {
case 0:
case 3:
case 4:
let cancelSubscriptionParams = {
subscriptionId: subscriptionToCancel.stripe_id,
params: {
prorate: this.prorate,
invoice_now: true,
},
};
let subscriptionCanceled = await stripe.cancelSubscription(cancelSubscriptionParams);
let localCancellationParams = {
status: stripeSubscriptionsModel.inverse_status_mapping()[subscriptionCanceled.status],
};
await stripeSubscriptionsModel.edit(localCancellationParams, subscriptionId);
return subscriptionCanceled;
default:
throw new Error('Something wrong');
}
};
this.userAddCard = async function (cardParams) {
let createdCard = await stripe.createCard(cardParams);
if (createdCard) {
if (createdCard.metadata.is_default == '1') {
let customerUpdateParams = {
customerId: createdCard.customer,
params: { default_source: createdCard.id },
};
await stripe.updateCustomer(customerUpdateParams);
let defaultCards = await stripeCardsModel.getAll({ is_default: 1 });
if (defaultCards.length > 0) {
defaultCards.forEach(async (card) => {
await stripeCardsModel.edit({ is_default: 0 }, card.id);
});
}
}
cardModelEntry = {
card_last: createdCard.last4 ?? '',
card_brand: createdCard.brand ?? '',
card_exp_month: createdCard.exp_month ?? '',
exp_year: createdCard.exp_year ?? '',
card_name: createdCard.metadata.card_name ?? '',
stripe_card_customer: createdCard.customer,
stripe_card_id: createdCard.id,
is_default: createdCard.metadata.is_default,
user_id: this.userId ?? null,
role_id: this.roleId ?? null,
};
var modelCreatedCard = await stripeCardsModel.insert(cardModelEntry);
if (!modelCreatedCard) {
throw new Error('Internal error: Error adding card');
}
}
return modelCreatedCard;
};
this.changePlan = async function (currentSubscription, newPlan, user, card, couponId = '') {
//if current subscriptions is canceled
if (!currentSubscription || currentSubscription.status == 5) {
let subscriptionParams = {
customer: user.stripe_id,
items: { plan: newPlan.id },
coupon: couponId,
};
return await this.subscribe(subscriptionParams, newPlan);
}
//user has subscription (upgrade/ downgrade)
if (currentSubscription && currentSubscription.status != 5) {
if (currentSubscription.plan_id == newPlan.id) throw new Error('Same Plan');
}
};
}