agmission/Development/server/scripts/migrateToSM.js

238 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const debug = require('debug')('agm:migrateToSM_util'),
env = require('../helpers/env.js'),
isProd = env.PRODUCTION,
dbConn = require('../helpers/db/connect.js')(),
// dbConn = require('../helpers/db/connect-remote.js')(),
{ SubType } = require('../model/subscription.js'),
models = require('../model/index.js'),
utils = require('../helpers/utils.js'),
moment = require('moment'),
stripe = require('stripe')(env.STRIPE_SEC_KEY, { apiVersion: env.STRIPE_API_VERSION });
const TEST_MODE = false;
const VERIFY_ONLY = false;
process
.on('uncaughtException', function (err) {
debug(err);
process.exit(1);
})
.on('unhandledRejection', (reason, p) => {
debug(reason, 'Unhandled Rejection at Promise', p);
});
function getStripePriceId(pkgName) {
const priceKey = pkgName && pkgName.toLowerCase().replace(/-/g, '_');
return env.PRICES[priceKey];
}
// Quick convert from date string (MM-DD-YYYY or MM/DD/YYYY) to moment object
function dateStrToMoment(str) {
// Check if the date string is in the format of MM-DD-YYYY or MM/DD/YYYY
if (!str || !str.match(/(\d{2}[-/]){2}\d{4}/)) throw new Error(`Invalid date string format: ${str}`);
// Convert the date string to the format of YYYY-MM-DD
const _isoDateStr = str && (str.split(/[-/]/).reverse().join('-'));
return moment.utc(_isoDateStr);
}
async function createSubscription(params) {
return stripe && (await stripe.subscriptions.create(params));
}
async function createStripeCustomer(params) {
const custsRS = await stripe.customers.search({
query: `email:"${params.username}"`,
});
if (!utils.isEmptyArray(custsRS.data)) {
const subsRS = await stripe.subscriptions.list({
customer: custsRS.data[0].id,
});
// if (subsRS.data && subsRS.data.length) throw new Error(`Customer ${params.username} already has subscriptions in Stripe !`);
// else
return subsRS.data && subsRS.data.length ? [custsRS.data[0], subsRS.data] : [custsRS.data[0]];
} else {
return [await stripe.customers.create({
name: params.name, // The customer's (appplicator) business name.
email: params.username,
address: {
...((params.country == 'CA' && params.username.endsWith('@agnav.com')) && {
line1: '30 Churchill Drive',
city: 'Barrie',
state: 'ON',
postal_code: 'L4N 9P8'
}),
country: params.country
}
})];
}
}
async function updateStripeCustomerInfoFromDB() {
const customers = await models.Customer.find({ active: true, migratedDate: null, "membership.custId": { $ne: null } }).lean();
for (const cust of customers) {
const stripeCusts = await stripe.customers.search({ query: `email:"${cust.username.toLowerCase()}"` });
if (stripeCusts && stripeCusts.data && stripeCusts.data.length == 1) {
await stripe.customers.update(stripeCusts.data[0].id, { name: cust.name.trim() });
}
}
}
async function migrateToSM(custList) {
if (utils.isEmptyArray(custList)) return;
// Check and only proceed when is idle and the db connection is connected
if (!dbConn || dbConn.readyState !== 1) return;
const filterOps = { markedDelete: { $ne: true }, kind: '1', /*active: true, membership: { $ne: null }, "membership.custId": { $ne: null },migratedDate: null, /*username: /.*(?<!@agnav\.com)$/i */ };
const customers = await models.Customer.find(filterOps).lean();
const custSet = utils.arrayToObject(customers, "username", true) || {};
const notOKUsers = [], okCusts = [];
for (const cust of custList) {
// Set username to lowercase
cust.username = cust.username.toLowerCase();
if (!custSet[cust.username]) notOKUsers.push(cust.username);
else okCusts.push(cust);
}
if (!utils.isEmptyArray(notOKUsers)) {
debug(`Skipped ${notOKUsers.length} customers because the master account were NOT FOUND or are already in DB or were already migrated !.`, notOKUsers.join(','));
}
if (!VERIFY_ONLY) {
const noSubCusts = [];
const errorCusts = [];
let stripeCust;
for (const mCust of okCusts) {
const dbAppl = custSet[mCust.username];
try {
// Create subscriptions for the customer
// 1.1 Create a customer in Stripe if not exists
const stripeCustRS = await createStripeCustomer({ name: dbAppl.name.trim(), username: mCust.username, country: dbAppl.country });
stripeCust = stripeCustRS && (stripeCust = stripeCustRS[0]);
const startMoment = dateStrToMoment(mCust.startDate).startOf('day');
const endMoment = dateStrToMoment(mCust.endDate).endOf('day');
// 1.1.1 Update the customer's membership info in DB
await models.Customer.updateOne({ username: { $regex: new RegExp(`^${mCust.username}$`, 'i') } }, {
$set: {
membership: {
custId: stripeCust.id,
trials: {
type: 'byDate',
startDate: startMoment.toDate(),
byDate: endMoment.toDate(),
}
}
}
});
if (stripeCustRS.length > 1) {
// 1.1.2 Cancel all existing subscriptions for the customer
for (const sub of stripeCustRS[1]) {
await stripe.subscriptions.del(sub.id, { prorate: false, invoice_now: false });
}
}
if (endMoment.isAfter(moment.utc().endOf('day'))) {
// 1.2 Create subscriptions for the customer
const subOps = {
cancel_at_period_end: true,
automatic_tax: { enabled: utils.stringToBoolean(mCust.taxable) && dbAppl.country === 'CA' },
// Making the initial period up to the first full invoice date free. This action doesnt generate an invoice at all until the first billing cycle. Ref: https://docs.stripe.com/billing/subscriptions/billing-cycle#new-subscriptions
proration_behavior: 'none',
trial_end: endMoment.unix(), // TODO: convert from endofday of the UTC enddate to timestamp
trial_settings: { end_behavior: { missing_payment_method: "cancel" } },
customer: stripeCust.id,
};
if (startMoment.isBefore(moment.utc().startOf('day'))) {
// Ref: https://docs.stripe.com/billing/subscriptions/backdating?dashboard-or-api=api
subOps.backdate_start_date = startMoment.unix();
if (mCust?.package.trim().length) {
await createSubscription(
Object.assign({}, subOps, { metadata: { type: SubType.PACKAGE } }, { items: [{ price: getStripePriceId(mCust.package) }] })
);
}
if (mCust.trackingQty && mCust.trackingQty > 0) {
await createSubscription(
Object.assign({}, subOps, { metadata: { type: SubType.ADDON } }, { items: [{ price: getStripePriceId('addon_1'), quantity: +mCust.trackingQty || 1 }] })
);
}
} else {
// Only Log the customer as a future trial user if the start date is in the future
debug(`Customer ${mCust.username} is a future trial user. Start date: ${startMoment.format('YYYY-MM-DD')}, End date: ${endMoment.format('YYYY-MM-DD')}`);
}
}
// 2. Update membership info for the customer. This can be done by the webhooks handler which NEEDS TO BE ACTIVE BEFORE THE MIGRATION.
// 3. Marked the customer as as migrated when any steps failed
await models.Customer.updateOne({ username: { $regex: new RegExp(`^${mCust.username}$`, 'i') } }, {
$set: { migratedDate: new Date() }
});
} catch (error) {
if (error.type && error.type.startsWith('Stripe')) {
noSubCusts.push(mCust.username + ' (' + mCust.endDate + ')');
} else {
errorCusts.push(mCust.username + ' (' + mCust.endDate + ')');
}
}
}
!utils.isEmptyArray(noSubCusts) && (debug(`Can't create subscriptions for ${noSubCusts.length} customers !. Please inform them about the expiry issue.`, noSubCusts.join(',')));
!utils.isEmptyArray(errorCusts) && (debug(`Can't create subscriptions for ${errorCusts.length} customers !. Please check again.`, errorCusts.join(',')));
if (!utils.isEmptyArray(okCusts)) {
debug(`DONE. Migrated ${okCusts.length} customers to DB !.`);
}
} else {
debug(`DONE Verifying AGM Customer. There are ${okCusts.length} of ${custList.length} customers ready to be migrated to SM.`);
}
}
(async function main() {
const custList = TEST_MODE ? [
{ username: 'trungh1@agnav.com', package: 'ess-1', trackingQty: 1, startDate: '01-11-2024', endDate: '02-11-2025', taxable: false },
] :
// require('./custList-Mar14_25.json');
// require('./custList2.json');
// require('./custList-Mar24_25-2.json');
// require('./custList-Mar25_25.json');
// require('./custList-May_06_25.json');
// require('./custList-May07_25-up.json');
// require('./custList-May08_25-Crabbe.json');
// require('./custList-May08_25-Eastern.json');
// require('./custList-May12_25-Metro_NancyR.json');
// require('./custList-May12_25-Volusia.json');
// require('./custList-May14_25-FloridaKeys.json');
// require('./custList-May16_25-Osbone_Aviation.json');
// require('./custList-May20_25-VDCI.json');
// require('./custList-May21_25-reviewed.json');
// require('./custList-May26_25-AEROTREILE.json');
// require('./custList-May27_25-Rimin_Air-trial.json');
// require('./custList-May28_25-3SB.json');
// require('./custList-May29_25-Wyatt_Trost.json');
// require('./sub-migration/custList-Jun_03-Beaufort_2-3_corrected.json');
require('./sub-migration/custList-June11_25-Fazeda-renewed-manually.json');
// require('./custList-local.json');
dbConn.once('open', async () => {
try {
await migrateToSM(custList);
// await updateStripeCustomerInfoFromDB();
} catch (error) {
console.error(error);
}
finally {
process.exit();
}
});
})();