diff --git a/core/server/models/member-stripe-customer.js b/core/server/models/member-stripe-customer.js index 6a4807d0a6..a5cc05b21e 100644 --- a/core/server/models/member-stripe-customer.js +++ b/core/server/models/member-stripe-customer.js @@ -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); } }); diff --git a/core/server/models/member.js b/core/server/models/member.js index 7595933cc1..75cd491eef 100644 --- a/core/server/models/member.js +++ b/core/server/models/member.js @@ -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); } }); diff --git a/core/server/models/stripe-customer-subscription.js b/core/server/models/stripe-customer-subscription.js index 8b4d40d03b..6c2192d44f 100644 --- a/core/server/models/stripe-customer-subscription.js +++ b/core/server/models/stripe-customer-subscription.js @@ -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; diff --git a/test/regression/models/model_member_stripe_customer_spec.js b/test/regression/models/model_member_stripe_customer_spec.js new file mode 100644 index 0000000000..c2e9619130 --- /dev/null +++ b/test/regression/models/model_member_stripe_customer_spec.js @@ -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'); + }); + }); +}); + diff --git a/test/regression/models/model_members_spec.js b/test/regression/models/model_members_spec.js new file mode 100644 index 0000000000..2ffd641c94 --- /dev/null +++ b/test/regression/models/model_members_spec.js @@ -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'); + }); + }); +}); + diff --git a/test/regression/models/model_stripe_customer_subscription_spec.js b/test/regression/models/model_stripe_customer_subscription_spec.js new file mode 100644 index 0000000000..dd6ccdb6e5 --- /dev/null +++ b/test/regression/models/model_stripe_customer_subscription_spec.js @@ -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'); + }); + }); +});