mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Updated Member model to cascade on destroy (#12077)
no-issue Up until now we have left orphaned rows in members_stripe_* tables when a member is deleted, this updates the destroy method so that we cascade and remove any MemberStripeCustomer and StripeCustomerSubscription models related to the Member. This also adds regression tests for the new functionality as well as to confirm the existing functionality of cascading to the members_labels join table This adds the relations of Subscription->Customer & Customer->Member
This commit is contained in:
parent
2ac69e637e
commit
5144a0e09c
6 changed files with 418 additions and 4 deletions
|
@ -1,7 +1,21 @@
|
|||
const ghostBookshelf = require('./base');
|
||||
|
||||
const MemberStripeCustomer = ghostBookshelf.Model.extend({
|
||||
tableName: 'members_stripe_customers'
|
||||
tableName: 'members_stripe_customers',
|
||||
|
||||
relationships: ['subscriptions'],
|
||||
|
||||
relationshipBelongsTo: {
|
||||
subscriptions: 'members_stripe_customers_subscriptions'
|
||||
},
|
||||
|
||||
subscriptions() {
|
||||
return this.hasMany('StripeCustomerSubscription', 'customer_id', 'customer_id');
|
||||
},
|
||||
|
||||
member() {
|
||||
return this.belongsTo('Member', 'member_id', 'id');
|
||||
}
|
||||
}, {
|
||||
async upsert(data, unfilteredOptions) {
|
||||
const customerId = data.customer_id;
|
||||
|
@ -12,6 +26,33 @@ const MemberStripeCustomer = ghostBookshelf.Model.extend({
|
|||
}));
|
||||
}
|
||||
return this.add(data, unfilteredOptions);
|
||||
},
|
||||
|
||||
add(data, unfilteredOptions) {
|
||||
if (!unfilteredOptions.transacting) {
|
||||
return ghostBookshelf.transaction((transacting) => {
|
||||
return this.add(data, Object.assign({transacting}, unfilteredOptions));
|
||||
});
|
||||
}
|
||||
return ghostBookshelf.Model.add.call(this, data, unfilteredOptions);
|
||||
},
|
||||
|
||||
edit(data, unfilteredOptions) {
|
||||
if (!unfilteredOptions.transacting) {
|
||||
return ghostBookshelf.transaction((transacting) => {
|
||||
return this.edit(data, Object.assign({transacting}, unfilteredOptions));
|
||||
});
|
||||
}
|
||||
return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions);
|
||||
},
|
||||
|
||||
destroy(unfilteredOptions) {
|
||||
if (!unfilteredOptions.transacting) {
|
||||
return ghostBookshelf.transaction((transacting) => {
|
||||
return this.destroy(Object.assign({transacting}, unfilteredOptions));
|
||||
});
|
||||
}
|
||||
return ghostBookshelf.Model.destroy.call(this, unfilteredOptions);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -15,10 +15,11 @@ const Member = ghostBookshelf.Model.extend({
|
|||
};
|
||||
},
|
||||
|
||||
relationships: ['labels'],
|
||||
relationships: ['labels', 'stripeCustomers'],
|
||||
|
||||
relationshipBelongsTo: {
|
||||
labels: 'labels'
|
||||
labels: 'labels',
|
||||
stripeCustomers: 'members_stripe_customers'
|
||||
},
|
||||
|
||||
labels: function labels() {
|
||||
|
@ -27,6 +28,10 @@ const Member = ghostBookshelf.Model.extend({
|
|||
.query('orderBy', 'sort_order', 'ASC');
|
||||
},
|
||||
|
||||
stripeCustomers() {
|
||||
return this.hasMany('MemberStripeCustomer', 'member_id', 'id');
|
||||
},
|
||||
|
||||
emitChange: function emitChange(event, options) {
|
||||
const eventToTrigger = 'member' + '.' + event;
|
||||
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
||||
|
@ -223,6 +228,33 @@ const Member = ghostBookshelf.Model.extend({
|
|||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
add(data, unfilteredOptions) {
|
||||
if (!unfilteredOptions.transacting) {
|
||||
return ghostBookshelf.transaction((transacting) => {
|
||||
return this.add(data, Object.assign({transacting}, unfilteredOptions));
|
||||
});
|
||||
}
|
||||
return ghostBookshelf.Model.add.call(this, data, unfilteredOptions);
|
||||
},
|
||||
|
||||
edit(data, unfilteredOptions) {
|
||||
if (!unfilteredOptions.transacting) {
|
||||
return ghostBookshelf.transaction((transacting) => {
|
||||
return this.edit(data, Object.assign({transacting}, unfilteredOptions));
|
||||
});
|
||||
}
|
||||
return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions);
|
||||
},
|
||||
|
||||
destroy(unfilteredOptions) {
|
||||
if (!unfilteredOptions.transacting) {
|
||||
return ghostBookshelf.transaction((transacting) => {
|
||||
return this.destroy(Object.assign({transacting}, unfilteredOptions));
|
||||
});
|
||||
}
|
||||
return ghostBookshelf.Model.destroy.call(this, unfilteredOptions);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
const ghostBookshelf = require('./base');
|
||||
|
||||
const StripeCustomerSubscription = ghostBookshelf.Model.extend({
|
||||
tableName: 'members_stripe_customers_subscriptions'
|
||||
tableName: 'members_stripe_customers_subscriptions',
|
||||
|
||||
customer() {
|
||||
return this.belongsTo('MemberStripeCustomer', 'customer_id', 'customer_id');
|
||||
}
|
||||
}, {
|
||||
async upsert(data, unfilteredOptions) {
|
||||
const subscriptionId = unfilteredOptions.subscription_id;
|
||||
|
|
150
test/regression/models/model_member_stripe_customer_spec.js
Normal file
150
test/regression/models/model_member_stripe_customer_spec.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
const should = require('should');
|
||||
const BaseModel = require('../../../core/server/models/base');
|
||||
const {Member} = require('../../../core/server/models/member');
|
||||
const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer');
|
||||
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
|
||||
|
||||
const testUtils = require('../../utils');
|
||||
|
||||
describe('MemberStripeCustomer Model', function run() {
|
||||
before(testUtils.teardownDb);
|
||||
beforeEach(testUtils.setup('roles'));
|
||||
afterEach(testUtils.teardownDb);
|
||||
|
||||
describe('subscriptions', function () {
|
||||
// For some reason the initial .add of MemberStripeCustomer is **not** adding a StripeCustomerSubscription :(
|
||||
it.skip('Is correctly mapped to the stripe subscriptions', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
await MemberStripeCustomer.add({
|
||||
member_id: 'fake_member_id',
|
||||
customer_id: 'fake_customer_id',
|
||||
subscriptions: [{
|
||||
subscription_id: 'fake_subscription_id1',
|
||||
plan_id: 'fake_plan_id',
|
||||
plan_amount: 1337,
|
||||
plan_nickname: 'e-LEET',
|
||||
plan_interval: 'year',
|
||||
plan_currency: 'btc',
|
||||
status: 'active',
|
||||
start_date: new Date(),
|
||||
current_period_end: new Date(),
|
||||
cancel_at_period_end: false
|
||||
}]
|
||||
}, context);
|
||||
|
||||
const subscription1 = await StripeCustomerSubscription.findOne({
|
||||
subscription_id: 'fake_subscription_id1'
|
||||
}, context);
|
||||
|
||||
should.exist(subscription1, 'StripeCustomerSubscription should have been created');
|
||||
|
||||
await StripeCustomerSubscription.add({
|
||||
customer_id: 'fake_customer_id',
|
||||
subscription_id: 'fake_subscription_id2',
|
||||
plan_id: 'fake_plan_id',
|
||||
plan_amount: 1337,
|
||||
plan_nickname: 'e-LEET',
|
||||
plan_interval: 'year',
|
||||
plan_currency: 'btc',
|
||||
status: 'active',
|
||||
start_date: new Date(),
|
||||
current_period_end: new Date(),
|
||||
cancel_at_period_end: false
|
||||
}, context);
|
||||
|
||||
const customer = await MemberStripeCustomer.findOne({
|
||||
customer_id: 'fake_customer_id'
|
||||
}, Object.assign({}, context, {
|
||||
withRelated: ['subscriptions']
|
||||
}));
|
||||
|
||||
should.exist(customer.related('subscriptions'), 'MemberStripeCustomer should have been fetched with subscriptions');
|
||||
|
||||
const subscriptions = customer.related('subscriptions');
|
||||
|
||||
should.equal(subscriptions.length, 2, 'Should be two subscriptions');
|
||||
|
||||
should.equal(subscriptions.models[0].get('subscription_id'), 'fake_subscription_id1');
|
||||
should.equal(subscriptions.models[1].get('subscription_id'), 'fake_subscription_id2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('member', function () {
|
||||
it('Is correctly mapped to the member', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
await Member.add({
|
||||
id: 'fake_member_id',
|
||||
email: 'test@test.member'
|
||||
}, context);
|
||||
|
||||
await MemberStripeCustomer.add({
|
||||
member_id: 'fake_member_id',
|
||||
customer_id: 'fake_customer_id'
|
||||
}, context);
|
||||
|
||||
const customer = await MemberStripeCustomer.findOne({
|
||||
customer_id: 'fake_customer_id'
|
||||
}, Object.assign({}, context, {
|
||||
withRelated: ['member']
|
||||
}));
|
||||
|
||||
const member = customer.related('member');
|
||||
|
||||
should.exist(member, 'MemberStripeCustomer should have been fetched with member');
|
||||
|
||||
should.equal(member.get('id'), 'fake_member_id');
|
||||
should.equal(member.get('email'), 'test@test.member');
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
it('Cascades to members_stripe_customers_subscriptions', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
await MemberStripeCustomer.add({
|
||||
member_id: 'fake_member_id',
|
||||
customer_id: 'fake_customer_id'
|
||||
}, context);
|
||||
|
||||
const customer = await MemberStripeCustomer.findOne({
|
||||
member_id: 'fake_member_id'
|
||||
}, context);
|
||||
|
||||
should.exist(customer, 'Customer should have been created');
|
||||
|
||||
await StripeCustomerSubscription.add({
|
||||
customer_id: 'fake_customer_id',
|
||||
subscription_id: 'fake_subscription_id',
|
||||
plan_id: 'fake_plan_id',
|
||||
plan_amount: 1337,
|
||||
plan_nickname: 'e-LEET',
|
||||
plan_interval: 'year',
|
||||
plan_currency: 'btc',
|
||||
status: 'active',
|
||||
start_date: new Date(),
|
||||
current_period_end: new Date(),
|
||||
cancel_at_period_end: false
|
||||
}, context);
|
||||
|
||||
const subscription = await StripeCustomerSubscription.findOne({
|
||||
customer_id: customer.get('customer_id')
|
||||
}, context);
|
||||
|
||||
should.exist(subscription, 'Subscription should have been created');
|
||||
|
||||
await MemberStripeCustomer.destroy(Object.assign({
|
||||
id: customer.get('id')
|
||||
}, context));
|
||||
|
||||
const customerAfterDestroy = await MemberStripeCustomer.findOne({
|
||||
member_id: 'fake_member_id'
|
||||
});
|
||||
should.not.exist(customerAfterDestroy, 'MemberStripeCustomer should have been destroyed');
|
||||
|
||||
const subscriptionAfterDestroy = await StripeCustomerSubscription.findOne({
|
||||
customer_id: customer.get('customer_id')
|
||||
});
|
||||
should.not.exist(subscriptionAfterDestroy, 'StripeCustomerSubscription should have been destroyed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
140
test/regression/models/model_members_spec.js
Normal file
140
test/regression/models/model_members_spec.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
const should = require('should');
|
||||
const BaseModel = require('../../../core/server/models/base');
|
||||
const {Label} = require('../../../core/server/models/label');
|
||||
const {Member} = require('../../../core/server/models/member');
|
||||
const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer');
|
||||
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
|
||||
|
||||
const testUtils = require('../../utils');
|
||||
|
||||
describe('Member Model', function run() {
|
||||
before(testUtils.teardownDb);
|
||||
beforeEach(testUtils.setup('roles'));
|
||||
afterEach(testUtils.teardownDb);
|
||||
|
||||
describe('stripeCustomers', function () {
|
||||
it('Is correctly mapped to the stripe customers', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
await Member.add({
|
||||
email: 'test@test.member',
|
||||
stripeCustomers: [{
|
||||
customer_id: 'fake_customer_id1'
|
||||
}]
|
||||
}, context);
|
||||
|
||||
const customer1 = await MemberStripeCustomer.findOne({
|
||||
customer_id: 'fake_customer_id1'
|
||||
}, context);
|
||||
|
||||
should.exist(customer1, 'MemberStripeCustomer should have been created');
|
||||
|
||||
await MemberStripeCustomer.add({
|
||||
member_id: customer1.get('member_id'),
|
||||
customer_id: 'fake_customer_id2'
|
||||
}, context);
|
||||
|
||||
const member = await Member.findOne({
|
||||
email: 'test@test.member'
|
||||
}, Object.assign({}, context, {
|
||||
withRelated: ['stripeCustomers']
|
||||
}));
|
||||
|
||||
should.exist(member.related('stripeCustomers'), 'Member should have been fetched with stripeCustomers');
|
||||
|
||||
const stripeCustomers = member.related('stripeCustomers');
|
||||
|
||||
should.equal(stripeCustomers.length, 2, 'Should be two stripeCustomers');
|
||||
|
||||
should.equal(stripeCustomers.models[0].get('customer_id'), 'fake_customer_id1');
|
||||
should.equal(stripeCustomers.models[1].get('customer_id'), 'fake_customer_id2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
it('Cascades to members_labels, members_stripe_customers & members_stripe_customers_subscriptions', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
await Member.add({
|
||||
email: 'test@test.member',
|
||||
labels: [{
|
||||
name: 'A label',
|
||||
slug: 'a-unique-slug-for-testing-members-model'
|
||||
}]
|
||||
}, context);
|
||||
const member = await Member.findOne({
|
||||
email: 'test@test.member'
|
||||
}, context);
|
||||
|
||||
should.exist(member, 'Member should have been created');
|
||||
|
||||
const label = await Label.findOne({
|
||||
slug: 'a-unique-slug-for-testing-members-model'
|
||||
}, context);
|
||||
|
||||
should.exist(label, 'Label should have been created');
|
||||
|
||||
const memberLabel = await BaseModel.knex('members_labels').where({
|
||||
label_id: label.get('id'),
|
||||
member_id: member.get('id')
|
||||
}).select().first();
|
||||
|
||||
should.exist(memberLabel, 'Label should have been attached to member');
|
||||
|
||||
await MemberStripeCustomer.add({
|
||||
member_id: member.get('id'),
|
||||
customer_id: 'fake_customer_id'
|
||||
}, context);
|
||||
|
||||
const customer = await MemberStripeCustomer.findOne({
|
||||
member_id: member.get('id')
|
||||
}, context);
|
||||
|
||||
should.exist(customer, 'Customer should have been created');
|
||||
|
||||
await StripeCustomerSubscription.add({
|
||||
customer_id: 'fake_customer_id',
|
||||
subscription_id: 'fake_subscription_id',
|
||||
plan_id: 'fake_plan_id',
|
||||
plan_amount: 1337,
|
||||
plan_nickname: 'e-LEET',
|
||||
plan_interval: 'year',
|
||||
plan_currency: 'btc',
|
||||
status: 'active',
|
||||
start_date: new Date(),
|
||||
current_period_end: new Date(),
|
||||
cancel_at_period_end: false
|
||||
}, context);
|
||||
|
||||
const subscription = await StripeCustomerSubscription.findOne({
|
||||
customer_id: customer.get('customer_id')
|
||||
}, context);
|
||||
|
||||
should.exist(subscription, 'Subscription should have been created');
|
||||
|
||||
await Member.destroy(Object.assign({
|
||||
id: member.get('id')
|
||||
}, context));
|
||||
|
||||
const memberAfterDestroy = await Member.findOne({
|
||||
email: 'test@test.member'
|
||||
});
|
||||
should.not.exist(memberAfterDestroy, 'Member should have been destroyed');
|
||||
|
||||
const memberLabelAfterDestroy = await BaseModel.knex('members_labels').where({
|
||||
label_id: label.get('id'),
|
||||
member_id: member.get('id')
|
||||
}).select().first();
|
||||
should.not.exist(memberLabelAfterDestroy, 'Label should have been removed from member');
|
||||
|
||||
const customerAfterDestroy = await MemberStripeCustomer.findOne({
|
||||
member_id: member.get('id')
|
||||
});
|
||||
should.not.exist(customerAfterDestroy, 'MemberStripeCustomer should have been destroyed');
|
||||
|
||||
const subscriptionAfterDestroy = await StripeCustomerSubscription.findOne({
|
||||
customer_id: customer.get('customer_id')
|
||||
});
|
||||
should.not.exist(subscriptionAfterDestroy, 'StripeCustomerSubscription should have been destroyed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
const should = require('should');
|
||||
const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer');
|
||||
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
|
||||
|
||||
const testUtils = require('../../utils');
|
||||
|
||||
describe('StripeCustomerSubscription Model', function run() {
|
||||
before(testUtils.teardownDb);
|
||||
beforeEach(testUtils.setup('roles'));
|
||||
afterEach(testUtils.teardownDb);
|
||||
|
||||
describe('customer', function () {
|
||||
it('Is correctly mapped to the stripe customer', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
await MemberStripeCustomer.add({
|
||||
member_id: 'fake_member_id',
|
||||
customer_id: 'fake_customer_id'
|
||||
}, context);
|
||||
|
||||
await StripeCustomerSubscription.add({
|
||||
customer_id: 'fake_customer_id',
|
||||
subscription_id: 'fake_subscription_id',
|
||||
plan_id: 'fake_plan_id',
|
||||
plan_amount: 1337,
|
||||
plan_nickname: 'e-LEET',
|
||||
plan_interval: 'year',
|
||||
plan_currency: 'btc',
|
||||
status: 'active',
|
||||
start_date: new Date(),
|
||||
current_period_end: new Date(),
|
||||
cancel_at_period_end: false
|
||||
}, context);
|
||||
|
||||
const subscription = await StripeCustomerSubscription.findOne({
|
||||
subscription_id: 'fake_subscription_id'
|
||||
}, Object.assign({}, context, {
|
||||
withRelated: ['customer']
|
||||
}));
|
||||
|
||||
const customer = subscription.related('customer');
|
||||
|
||||
should.exist(customer, 'StripeCustomerSubscription should have been fetched with customer');
|
||||
|
||||
should.equal(customer.get('customer_id'), 'fake_customer_id');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue