diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 492df3873b..abf576656e 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.21.0", + "version": "5.22.0", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/core/server/data/importer/importers/data/products.js b/ghost/core/core/server/data/importer/importers/data/products.js index 2294aaf4ca..b0466f180c 100644 --- a/ghost/core/core/server/data/importer/importers/data/products.js +++ b/ghost/core/core/server/data/importer/importers/data/products.js @@ -29,6 +29,48 @@ class ProductsImporter extends BaseImporter { }; } + populatePriceData() { + const invalidRows = []; + _.each(this.dataToImport, (row) => { + if (row.slug === 'free') { + return; + } + if (row.currency && row.monthly_price && row.yearly_price) { + return; + } + if (!row.monthly_price || !row.currency) { + const monthlyStripePrice = _.find( + this.requiredFromFile.stripe_prices, + {id: row.monthly_price_id} + ) || _.find( + this.requiredExistingData.stripe_prices, + {id: row.monthly_price_id} + ); + if (!monthlyStripePrice) { + invalidRows.push(row.id); + return; + } + row.monthly_price = row.monthly_price || monthlyStripePrice.amount; + row.currency = monthlyStripePrice.currency; + } + if (!row.yearly_price) { + const yearlyStripePrice = _.find( + this.requiredFromFile.stripe_prices, + {id: row.yearly_price_id} + ) || _.find( + this.requiredExistingData.stripe_prices, + {id: row.yearly_price_id} + ); + if (!yearlyStripePrice) { + invalidRows.push(row.id); + return; + } + row.yearly_price = row.yearly_price || yearlyStripePrice.amount; + } + }); + this.dataToImport = this.dataToImport.filter(item => !invalidRows.includes(item.id)); + } + validateStripePrice() { // the stripe price either needs to exist in the current db, // or be imported as part of the same import @@ -84,6 +126,11 @@ class ProductsImporter extends BaseImporter { this.dataToImport = this.dataToImport.filter(item => !duplicateProducts.includes(item.id)); } + beforeImport() { + this.populatePriceData(); + return super.beforeImport(); + } + replaceIdentifiers() { // this has to be in replaceIdentifiers because it's after required* fields are set this.preventDuplicates(); diff --git a/ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js b/ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js new file mode 100644 index 0000000000..a498d3150c --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js @@ -0,0 +1,35 @@ +const logging = require('@tryghost/logging'); + +const {createTransactionalMigration} = require('../../utils'); + +module.exports = createTransactionalMigration( + async function up(knex) { + const rows = await knex('products as t') // eslint-disable-line no-restricted-syntax + .select( + 't.id as id', + 'mp.amount as monthly_price', + 'yp.amount as yearly_price', + knex.raw('coalesce(yp.currency, mp.currency) as currency') + ) + .leftJoin('stripe_prices AS mp', 't.monthly_price_id', 'mp.id') + .leftJoin('stripe_prices AS yp', 't.yearly_price_id', 'yp.id') + .where({ + 't.type': 'paid', + 't.currency': null + }); + + if (!rows.length) { + logging.info('Did not find any active paid Tiers'); + return; + } else { + logging.info(`Updating ${rows.length} Tiers with price and currency information`); + } + + for (const row of rows) { // eslint-disable-line no-restricted-syntax + await knex('products').update(row).where('id', row.id); + } + }, + async function down() { + // no-op: we don't want to reintroduce the missing data + } +); diff --git a/ghost/core/package.json b/ghost/core/package.json index 72f979eea3..bc92fe9f6a 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.21.0", + "version": "5.22.0", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", diff --git a/ghost/core/test/unit/server/data/importer/importers/data/products.test.js b/ghost/core/test/unit/server/data/importer/importers/data/products.test.js new file mode 100644 index 0000000000..028c412bc4 --- /dev/null +++ b/ghost/core/test/unit/server/data/importer/importers/data/products.test.js @@ -0,0 +1,64 @@ +const assert = require('assert'); +const ProductsImporter = require('../../../../../../../core/server/data/importer/importers/data/products'); + +const fakeProducts = [{ + id: 'product_1', + name: 'New One', + slug: 'new-one', + active: 1, + welcome_page_url: null, + visibility: 'public', + trial_days: 0, + description: null, + type: 'paid', + created_at: '2022-10-20T11:11:32.000Z', + updated_at: '2022-10-21T04:47:42.000Z', + monthly_price_id: 'price_1', + yearly_price_id: 'price_2' +}]; + +const fakePrices = [{ + id: 'price_1', + stripe_price_id: 'price_YYYYYYYYYYYYYYYYYYYYYYYY', + stripe_product_id: 'prod_YYYYYYYYYYYYYY', + active: 1, + nickname: 'Monthly', + currency: 'usd', + amount: 500, + type: 'recurring', + interval: 'month', + description: null, + created_at: '2022-10-21T04:57:17.000Z', + updated_at: '2022-10-21T04:57:17.000Z' +}, +{ + id: 'price_2', + stripe_price_id: 'price_XXXXXXXXXXXXXXXXXXXXXXXX', + stripe_product_id: 'prod_XXXXXXXXXXXXXX', + active: 1, + nickname: 'Yearly', + currency: 'usd', + amount: 5000, + type: 'recurring', + interval: 'year', + description: null, + created_at: '2022-10-27T02:51:28.000Z', + updated_at: '2022-10-27T02:51:28.000Z' +}]; + +describe('ProductsImporter', function () { + describe('#beforeImport', function () { + it('Removes the sender_email column', function () { + const importer = new ProductsImporter({products: fakeProducts, stripe_prices: fakePrices}); + + importer.beforeImport(); + assert(importer.dataToImport.length === 1); + + const product = importer.dataToImport[0]; + + assert(product.currency === 'usd'); + assert(product.monthly_price === 500); + assert(product.yearly_price === 5000); + }); + }); +});