'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: /.*(? 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 doesn’t 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(); await createSubscription( Object.assign({}, subOps, { metadata: { type: SubType.PACKAGE } }, { items: [{ price: getStripePriceId(mCust.package) }] }) ); 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-local.json'); dbConn.once('open', async () => { try { await migrateToSM(custList); // await updateStripeCustomerInfoFromDB(); } catch (error) { console.error(error); } finally { process.exit(); } }); })();