Added stripe_price_id column to subscriptions table (#12881)

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

- Adds new `stripe_price_id` column to subscriptions table to store stripe price ids with `index`
- Populates `stripe_price_id` column value to current `plan_id` making the `plan_*` values redundant
- Updates tests
This commit is contained in:
Rishabh Garg 2021-04-20 16:37:59 +05:30 committed by GitHub
parent 5da4ae90b2
commit 48a2d24497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 249 additions and 6 deletions

View File

@ -0,0 +1,10 @@
const {createAddColumnMigration} = require('../../utils');
module.exports = createAddColumnMigration('members_stripe_customers_subscriptions', 'stripe_price_id', {
type: 'string',
maxlength: 255,
nullable: false,
unique: false,
defaultTo: '',
index: true
});

View File

@ -0,0 +1,20 @@
const logging = require('../../../../../shared/logging');
const {createTransactionalMigration} = require('../../utils');
module.exports = createTransactionalMigration(
async function up(knex) {
logging.info('Populating stripe_price_id from plan id in stripe customer subscriptions table');
await knex('members_stripe_customers_subscriptions')
.update({
stripe_price_id: knex.ref('plan_id')
});
},
async function down(knex) {
logging.info('Resetting stripe_price_id column to empty in stripe customer subscriptions table');
await knex('members_stripe_customers_subscriptions')
.update({
stripe_price_id: ''
});
}
);

View File

@ -454,7 +454,7 @@ module.exports = {
id: {type: 'string', maxlength: 24, nullable: false, primary: true}, id: {type: 'string', maxlength: 24, nullable: false, primary: true},
customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'members_stripe_customers.customer_id', cascadeDelete: true}, customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'members_stripe_customers.customer_id', cascadeDelete: true},
subscription_id: {type: 'string', maxlength: 255, nullable: false, unique: true}, subscription_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
plan_id: {type: 'string', maxlength: 255, nullable: false, unique: false}, stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: false, index: true, defaultTo: ''},
status: {type: 'string', maxlength: 50, nullable: false}, status: {type: 'string', maxlength: 50, nullable: false},
cancel_at_period_end: {type: 'bool', nullable: false, defaultTo: false}, cancel_at_period_end: {type: 'bool', nullable: false, defaultTo: false},
cancellation_reason: {type: 'string', maxlength: 500, nullable: true}, cancellation_reason: {type: 'string', maxlength: 500, nullable: true},
@ -465,7 +465,8 @@ module.exports = {
created_by: {type: 'string', maxlength: 24, nullable: false}, created_by: {type: 'string', maxlength: 24, nullable: false},
updated_at: {type: 'dateTime', nullable: true}, updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'string', maxlength: 24, nullable: true}, updated_by: {type: 'string', maxlength: 24, nullable: true},
/* Below fields eventually should be normalised e.g. stripe_plans table, link to here on plan_id */ /* Below fields are now redundant as we link prie_id to stripe_prices table */
plan_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
plan_nickname: {type: 'string', maxlength: 50, nullable: false}, plan_nickname: {type: 'string', maxlength: 50, nullable: false},
plan_interval: {type: 'string', maxlength: 50, nullable: false}, plan_interval: {type: 'string', maxlength: 50, nullable: false},
plan_amount: {type: 'integer', nullable: false}, plan_amount: {type: 'integer', nullable: false},
@ -480,7 +481,7 @@ module.exports = {
}, },
stripe_products: { stripe_products: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true}, id: {type: 'string', maxlength: 24, nullable: false, primary: true},
product_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'products.id', cascadeDelete: true}, product_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'products.id'},
stripe_product_id: {type: 'string', maxlength: 255, nullable: false, unique: true}, stripe_product_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
created_at: {type: 'dateTime', nullable: false}, created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true} updated_at: {type: 'dateTime', nullable: true}
@ -488,7 +489,7 @@ module.exports = {
stripe_prices: { stripe_prices: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true}, id: {type: 'string', maxlength: 24, nullable: false, primary: true},
stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: true}, stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
stripe_product_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'stripe_products.stripe_product_id', cascadeDelete: true}, stripe_product_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'stripe_products.stripe_product_id'},
active: {type: 'boolean', nullable: false}, active: {type: 'boolean', nullable: false},
nickname: {type: 'string', maxlength: 50, nullable: true}, nickname: {type: 'string', maxlength: 50, nullable: true},
currency: {type: 'string', maxLength: 3, nullable: false}, currency: {type: 'string', maxLength: 3, nullable: false},

View File

@ -2,7 +2,10 @@ const should = require('should');
const BaseModel = require('../../../core/server/models/base'); const BaseModel = require('../../../core/server/models/base');
const {Member} = require('../../../core/server/models/member'); const {Member} = require('../../../core/server/models/member');
const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer'); const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer');
const {Product} = require('../../../core/server/models/product');
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription'); const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
const {StripePrice} = require('../../../core/server/models/stripe-price');
const {StripeProduct} = require('../../../core/server/models/stripe-product');
const testUtils = require('../../utils'); const testUtils = require('../../utils');
@ -20,12 +23,34 @@ describe('MemberStripeCustomer Model', function run() {
email: 'test@test.test' email: 'test@test.test'
}); });
const product = await Product.add({
name: 'Ghost Product',
slug: 'ghost-product'
}, context);
await StripeProduct.add({
product_id: product.get('id'),
stripe_product_id: 'fake_product_id'
}, context);
await StripePrice.add({
stripe_price_id: 'fake_plan_id',
stripe_product_id: 'fake_product_id',
amount: 5000,
interval: 'monthly',
currency: 'USD',
active: 1,
nickname: 'Monthly',
type: 'recurring'
}, context);
await MemberStripeCustomer.add({ await MemberStripeCustomer.add({
member_id: member.get('id'), member_id: member.get('id'),
customer_id: 'fake_customer_id', customer_id: 'fake_customer_id',
subscriptions: [{ subscriptions: [{
subscription_id: 'fake_subscription_id1', subscription_id: 'fake_subscription_id1',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',
@ -47,6 +72,7 @@ describe('MemberStripeCustomer Model', function run() {
customer_id: 'fake_customer_id', customer_id: 'fake_customer_id',
subscription_id: 'fake_subscription_id2', subscription_id: 'fake_subscription_id2',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',
@ -119,10 +145,32 @@ describe('MemberStripeCustomer Model', function run() {
should.exist(customer, 'Customer should have been created'); should.exist(customer, 'Customer should have been created');
const product = await Product.add({
name: 'Ghost Product',
slug: 'ghost-product'
}, context);
await StripeProduct.add({
product_id: product.get('id'),
stripe_product_id: 'fake_product_id'
}, context);
await StripePrice.add({
stripe_price_id: 'fake_plan_id',
stripe_product_id: 'fake_product_id',
amount: 5000,
interval: 'monthly',
active: 1,
nickname: 'Monthly',
currency: 'USD',
type: 'recurring'
}, context);
await StripeCustomerSubscription.add({ await StripeCustomerSubscription.add({
customer_id: 'fake_customer_id', customer_id: 'fake_customer_id',
subscription_id: 'fake_subscription_id', subscription_id: 'fake_subscription_id',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',

View File

@ -7,6 +7,8 @@ const {MemberStripeCustomer} = require('../../../core/server/models/member-strip
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription'); const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
const testUtils = require('../../utils'); const testUtils = require('../../utils');
const {StripeProduct} = require('../../../core/server/models/stripe-product');
const {StripePrice} = require('../../../core/server/models/stripe-price');
describe('Member Model', function run() { describe('Member Model', function run() {
before(testUtils.teardownDb); before(testUtils.teardownDb);
@ -26,6 +28,27 @@ describe('Member Model', function run() {
should.exist(member, 'Member should have been created'); should.exist(member, 'Member should have been created');
const product = await Product.add({
name: 'Ghost Product',
slug: 'ghost-product'
}, context);
await StripeProduct.add({
product_id: product.get('id'),
stripe_product_id: 'fake_product_id'
}, context);
await StripePrice.add({
stripe_price_id: 'fake_plan_id',
stripe_product_id: 'fake_product_id',
amount: 5000,
interval: 'monthly',
active: 1,
nickname: 'Monthly',
currency: 'USD',
type: 'recurring'
}, context);
await MemberStripeCustomer.add({ await MemberStripeCustomer.add({
member_id: member.get('id'), member_id: member.get('id'),
customer_id: 'fake_customer_id1' customer_id: 'fake_customer_id1'
@ -40,6 +63,7 @@ describe('Member Model', function run() {
customer_id: 'fake_customer_id1', customer_id: 'fake_customer_id1',
subscription_id: 'fake_subscription_id1', subscription_id: 'fake_subscription_id1',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',
@ -54,6 +78,7 @@ describe('Member Model', function run() {
customer_id: 'fake_customer_id2', customer_id: 'fake_customer_id2',
subscription_id: 'fake_subscription_id2', subscription_id: 'fake_subscription_id2',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',
@ -161,10 +186,32 @@ describe('Member Model', function run() {
should.exist(customer, 'Customer should have been created'); should.exist(customer, 'Customer should have been created');
const product = await Product.add({
name: 'Ghost Product',
slug: 'ghost-product'
}, context);
await StripeProduct.add({
product_id: product.get('id'),
stripe_product_id: 'fake_product_id'
}, context);
await StripePrice.add({
stripe_price_id: 'fake_plan_id',
stripe_product_id: 'fake_product_id',
amount: 5000,
interval: 'monthly',
active: 1,
nickname: 'Monthly',
currency: 'USD',
type: 'recurring'
}, context);
await StripeCustomerSubscription.add({ await StripeCustomerSubscription.add({
customer_id: 'fake_customer_id', customer_id: 'fake_customer_id',
subscription_id: 'fake_subscription_id', subscription_id: 'fake_subscription_id',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',

View File

@ -1,7 +1,10 @@
const should = require('should'); const should = require('should');
const {Member} = require('../../../core/server/models/member'); const {Member} = require('../../../core/server/models/member');
const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer'); const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer');
const {Product} = require('../../../core/server/models/product');
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription'); const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
const {StripePrice} = require('../../../core/server/models/stripe-price');
const {StripeProduct} = require('../../../core/server/models/stripe-product');
const testUtils = require('../../utils'); const testUtils = require('../../utils');
@ -21,10 +24,32 @@ describe('StripeCustomerSubscription Model', function run() {
customer_id: 'fake_customer_id' customer_id: 'fake_customer_id'
}, context); }, context);
const product = await Product.add({
name: 'Ghost Product',
slug: 'ghost-product'
}, context);
await StripeProduct.add({
product_id: product.get('id'),
stripe_product_id: 'fake_product_id'
}, context);
await StripePrice.add({
stripe_price_id: 'fake_plan_id',
stripe_product_id: 'fake_product_id',
amount: 5000,
interval: 'monthly',
active: 1,
nickname: 'Monthly',
currency: 'USD',
type: 'recurring'
}, context);
await StripeCustomerSubscription.add({ await StripeCustomerSubscription.add({
customer_id: 'fake_customer_id', customer_id: 'fake_customer_id',
subscription_id: 'fake_subscription_id', subscription_id: 'fake_subscription_id',
plan_id: 'fake_plan_id', plan_id: 'fake_plan_id',
stripe_price_id: 'fake_plan_id',
plan_amount: 1337, plan_amount: 1337,
plan_nickname: 'e-LEET', plan_nickname: 'e-LEET',
plan_interval: 'year', plan_interval: 'year',

View File

@ -32,7 +32,7 @@ const defaultSettings = require('../../../../core/server/data/schema/default-set
*/ */
describe('DB version integrity', function () { describe('DB version integrity', function () {
// Only these variables should need updating // Only these variables should need updating
const currentSchemaHash = 'b94aa62d6e50eb42280837605e3f4a66'; const currentSchemaHash = 'c31e5e88461bbc015a9e50561d07f6f7';
const currentFixturesHash = '3dc9747eadecec34958dfba14c5332db'; const currentFixturesHash = '3dc9747eadecec34958dfba14c5332db';
const currentSettingsHash = 'b943cc3956eee3dd042f8394b2701d21'; const currentSettingsHash = 'b943cc3956eee3dd042f8394b2701d21';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';

View File

@ -477,6 +477,18 @@ const fixtures = {
return Promise.each(_.cloneDeep(DataGenerator.forKnex.members_stripe_customers), function (customer) { return Promise.each(_.cloneDeep(DataGenerator.forKnex.members_stripe_customers), function (customer) {
return models.MemberStripeCustomer.add(customer, context.internal); return models.MemberStripeCustomer.add(customer, context.internal);
}); });
}).then(function () {
return Promise.each(_.cloneDeep(DataGenerator.forKnex.products), function (product) {
return models.Product.add(product, context.internal);
});
}).then(function () {
return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_products), function (stripeProduct) {
return models.StripeProduct.add(stripeProduct, context.internal);
});
}).then(function () {
return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_prices), function (stripePrice) {
return models.StripePrice.add(stripePrice, context.internal);
});
}).then(function () { }).then(function () {
return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_customer_subscriptions), function (subscription) { return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_customer_subscriptions), function (subscription) {
return models.StripeCustomerSubscription.add(subscription, context.internal); return models.StripeCustomerSubscription.add(subscription, context.internal);

View File

@ -344,6 +344,14 @@ DataGenerator.Content = {
} }
], ],
products: [
{
id: ObjectId.generate(),
name: 'Ghost Product',
slug: 'ghost-product'
}
],
labels: [ labels: [
{ {
id: ObjectId.generate(), id: ObjectId.generate(),
@ -386,6 +394,7 @@ DataGenerator.Content = {
customer_id: 'cus_HR3tBmNhx4QsZY', customer_id: 'cus_HR3tBmNhx4QsZY',
subscription_id: 'sub_HR3tLNgGAHsa7b', subscription_id: 'sub_HR3tLNgGAHsa7b',
plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8', plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8',
stripe_price_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8',
status: 'active', status: 'active',
cancel_at_period_end: false, cancel_at_period_end: false,
current_period_end: '2020-07-09 19:01:20', current_period_end: '2020-07-09 19:01:20',
@ -401,6 +410,7 @@ DataGenerator.Content = {
customer_id: 'cus_HR3tBmNhx4QsZZ', customer_id: 'cus_HR3tBmNhx4QsZZ',
subscription_id: 'sub_HR3tLNgGAHsa7c', subscription_id: 'sub_HR3tLNgGAHsa7c',
plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb9', plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb9',
stripe_price_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb9',
status: 'trialing', status: 'trialing',
cancel_at_period_end: true, cancel_at_period_end: true,
current_period_end: '2025-07-09 19:01:20', current_period_end: '2025-07-09 19:01:20',
@ -416,6 +426,7 @@ DataGenerator.Content = {
customer_id: 'cus_HR3tBmNhx4QsZ0', customer_id: 'cus_HR3tBmNhx4QsZ0',
subscription_id: 'sub_HR3tLNgGAHsa7d', subscription_id: 'sub_HR3tLNgGAHsa7d',
plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730ba0', plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730ba0',
stripe_price_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730ba0',
status: 'active', status: 'active',
cancel_at_period_end: true, cancel_at_period_end: true,
current_period_end: '2025-07-09 19:01:20', current_period_end: '2025-07-09 19:01:20',
@ -427,7 +438,48 @@ DataGenerator.Content = {
plan_currency: 'usd' plan_currency: 'usd'
} }
], ],
stripe_prices: [
{
id: ObjectId.generate(),
stripe_price_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8',
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d',
active: 1,
nickname: 'Monthly',
currency: 'USD',
amount: 500,
type: 'recurring',
interval: 'month'
},
{
id: ObjectId.generate(),
stripe_price_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb9',
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d',
active: 1,
nickname: 'Yearly',
currency: 'USD',
amount: 1500,
type: 'recurring',
interval: 'year'
},
{
id: ObjectId.generate(),
stripe_price_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730ba0',
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d',
active: 1,
nickname: 'Yearly',
currency: 'USD',
amount: 2400,
type: 'recurring',
interval: 'year'
}
],
stripe_products: [
{
id: ObjectId.generate(),
product_id: '',
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d'
}
],
webhooks: [ webhooks: [
{ {
id: ObjectId.generate(), id: ObjectId.generate(),
@ -790,6 +842,14 @@ DataGenerator.forKnex = (function () {
}; };
} }
function createStripeProduct(product_id, stripe_product_id) {
return {
id: ObjectId.generate(),
product_id,
stripe_product_id
};
}
function createSetting(overrides) { function createSetting(overrides) {
const newObj = _.cloneDeep(overrides); const newObj = _.cloneDeep(overrides);
@ -1120,12 +1180,29 @@ DataGenerator.forKnex = (function () {
) )
]; ];
const products = [
createBasic(DataGenerator.Content.products[0])
];
const members_stripe_customers = [ const members_stripe_customers = [
createBasic(DataGenerator.Content.members_stripe_customers[0]), createBasic(DataGenerator.Content.members_stripe_customers[0]),
createBasic(DataGenerator.Content.members_stripe_customers[1]), createBasic(DataGenerator.Content.members_stripe_customers[1]),
createBasic(DataGenerator.Content.members_stripe_customers[2]) createBasic(DataGenerator.Content.members_stripe_customers[2])
]; ];
const stripe_products = [
createStripeProduct(
DataGenerator.Content.products[0].id,
DataGenerator.Content.stripe_products[0].stripe_product_id
)
];
const stripe_prices = [
createBasic(DataGenerator.Content.stripe_prices[0]),
createBasic(DataGenerator.Content.stripe_prices[1]),
createBasic(DataGenerator.Content.stripe_prices[2])
];
const stripe_customer_subscriptions = [ const stripe_customer_subscriptions = [
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[0]), createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[0]),
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[1]), createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[1]),
@ -1178,9 +1255,12 @@ DataGenerator.forKnex = (function () {
email_recipients, email_recipients,
labels, labels,
members, members,
products,
members_labels, members_labels,
members_stripe_customers, members_stripe_customers,
stripe_customer_subscriptions, stripe_customer_subscriptions,
stripe_prices,
stripe_products,
snippets snippets
}; };
}()); }());