diff --git a/ghost/members-api/index.js b/ghost/members-api/index.js index 0259173db8..766951b81a 100644 --- a/ghost/members-api/index.js +++ b/ghost/members-api/index.js @@ -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, diff --git a/ghost/members-api/lib/migrations/index.js b/ghost/members-api/lib/migrations/index.js index 7db8d11abd..b0cdccffa7 100644 --- a/ghost/members-api/lib/migrations/index.js +++ b/ghost/members-api/lib/migrations/index.js @@ -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 + }); + } };