Added migration for stripe plans to custom prices

refs https://github.com/TryGhost/Team/issues/637

- Adds one-off migration that reads from current `stripe_plans data` for a price, and ensures that the corresponding price is present in `stripe_prices` table at start.
- Currently, the portal_plans setting is used to determine the prices available to Portal for showing on Signup or Subscription change screen. The values allowed in portal_plans currently only allow [free, monthly, yearly] , which needs to be updated now to store price ids of available prices instead. Uses above migration to populate `portal_plans` with ids instead of names.
This commit is contained in:
Rishabh 2021-05-04 21:33:06 +05:30 committed by Rishabh Garg
parent 460dd09f8b
commit aa640ada5e
2 changed files with 145 additions and 4 deletions

View File

@ -166,7 +166,11 @@ module.exports = function MembersApi({
plans: stripeConfig.plans,
mode: process.env.NODE_ENV || 'development'
}),
stripeMigrations.populateProductsAndPrices(),
stripeMigrations.populateProductsAndPrices().then(() => {
return stripeMigrations.populateStripePricesFromStripePlansSetting(stripeConfig.plans);
}).then(() => {
return stripeMigrations.updatePortalPlansSetting(stripeConfig.plans);
}),
stripeWebhookService.configure({
webhookSecret: process.env.WEBHOOK_SECRET,
webhookHandlerUrl: stripeConfig.webhookHandlerUrl,

View File

@ -17,13 +17,15 @@ module.exports = class StripeMigrations {
* @param {any} params.StripeProduct
* @param {any} params.StripePrice
* @param {any} params.Product
* @param {any} params.stripeAPIService
* @param {any} params.Settings
* @param {import('../services/stripe-api')} params.stripeAPIService
*/
constructor({
StripeCustomerSubscription,
StripeProduct,
StripePrice,
Product,
Settings,
stripeAPIService,
logger
}) {
@ -32,7 +34,8 @@ module.exports = class StripeMigrations {
this._StripeProduct = StripeProduct;
this._StripePrice = StripePrice;
this._Product = Product;
this._StripeAPIService = stripeAPIService;
this._Settings = Settings;
this._stripeAPIService = stripeAPIService;
}
async populateProductsAndPrices() {
@ -60,7 +63,7 @@ module.exports = class StripeMigrations {
let stripePlans = [];
for (const plan of uniquePlans) {
try {
const stripePlan = await this._StripeAPIService.getPlan(plan, {
const stripePlan = await this._stripeAPIService.getPlan(plan, {
expand: ['product']
});
stripePlans.push(stripePlan);
@ -98,4 +101,138 @@ module.exports = class StripeMigrations {
}
}
}
async findPriceByPlan(plan) {
const currency = plan.currency ? plan.currency.toLowerCase() : 'usd';
const amount = Number.isInteger(plan.amount) ? plan.amount : parseInt(plan.amount);
const interval = plan.interval;
const price = await this._StripePrice.findOne({
currency,
amount,
interval
});
return price;
}
async populateStripePricesFromStripePlansSetting(plans) {
if (!plans) {
this._logging.info('Skipping stripe_plans -> stripe_prices migration');
return;
}
let defaultStripeProduct;
const stripeProductsPage = await this._StripeProduct.findPage({limit: 1});
defaultStripeProduct = stripeProductsPage.data[0];
if (!defaultStripeProduct) {
this._logging.info('Could not find Stripe Product - creating one');
const stripeProduct = await this._stripeAPIService.createProduct({
name: 'Ghost Product'
});
const productsPage = await this._Product.findPage({limit: 1});
const defaultProduct = productsPage.data[0];
if (!defaultProduct) {
this._logging.error('Could not find Product - skipping stripe_plans -> stripe_prices migration');
return;
}
defaultStripeProduct = await this._StripeProduct.add({
product_id: defaultProduct.id,
stripe_product_id: stripeProduct.id
});
}
for (const plan of plans) {
const price = await this.findPriceByPlan(plan);
if (!price) {
this._logging.info(`Could not find Stripe Price ${JSON.stringify(plan)}`);
try {
this._logging.info(`Creating Stripe Price ${JSON.stringify(plan)}`);
const price = await this._stripeAPIService.createPrice({
currency: plan.currency,
amount: plan.amount,
nickname: plan.name,
interval: plan.interval,
active: true,
type: 'recurring',
product: defaultStripeProduct.get('stripe_product_id')
});
await this._StripePrice.add({
stripe_price_id: price.id,
stripe_product_id: defaultStripeProduct.get('stripe_product_id'),
active: price.active,
nickname: price.nickname,
currency: price.currency,
amount: price.unit_amount,
type: 'recurring',
interval: price.recurring.interval
});
} catch (err) {
this._logging.error({err, message: 'Adding price failed'});
}
}
}
}
async updatePortalPlansSetting(plans) {
this._logging.info('Migrating portal_plans setting from names to ids');
const portalPlansSetting = await this._Settings.findOne({key: 'portal_plans'});
let portalPlans;
try {
portalPlans = JSON.parse(portalPlansSetting.get('value'));
} catch (err) {
this._logging.error({
message: 'Could not parse portal_plans setting, skipping migration',
err
});
return;
}
const containsOldValues = !!portalPlans.find((plan) => {
return ['monthly', 'yearly'].includes(plan);
});
if (!containsOldValues) {
this._logging.info('Could not find names in portal_plans setting, skipping migration');
return;
}
const newPortalPlans = await portalPlans.reduce(async (newPortalPlansPromise, plan) => {
let newPlan = plan;
if (plan === 'monthly') {
const monthlyPlan = plans.find((plan) => {
return plan.name === 'Monthly';
});
if (!monthlyPlan) {
return newPortalPlansPromise;
}
const price = await this.findPriceByPlan(monthlyPlan);
newPlan = price.id;
}
if (plan === 'yearly') {
const yearlyPlan = plans.find((plan) => {
return plan.name === 'Yearly';
});
if (!yearlyPlan) {
return newPortalPlansPromise;
}
const price = await this.findPriceByPlan(yearlyPlan);
newPlan = price.id;
}
const newPortalPlans = await newPortalPlansPromise;
return newPortalPlans.concat(newPlan);
}, []);
this._logging.info(`Updating portal_plans setting to ${JSON.stringify(newPortalPlans)}`);
await this._Settings.edit({
key: 'portal_plans',
value: JSON.stringify(newPortalPlans)
}, {
id: portalPlansSetting.id
});
}
};