diff --git a/test/api/unit/models/group.test.js b/test/api/unit/models/group.test.js index 91a80dad1a..19613448af 100644 --- a/test/api/unit/models/group.test.js +++ b/test/api/unit/models/group.test.js @@ -1932,28 +1932,54 @@ describe('Group Model', () => { context('hasNotCancelled', () => { it('returns false if group does not have customer id', () => { - expect(party.hasNotCancelled()).to.be.undefined; + expect(party.hasNotCancelled()).to.be.false; }); - it('returns true if party does not have plan.dateTerminated', () => { + it('returns true if group does not have plan.dateTerminated', () => { party.purchased.plan.customerId = 'test-id'; expect(party.hasNotCancelled()).to.be.true; }); - it('returns false if party if plan.dateTerminated is after today', () => { + it('returns false if group if plan.dateTerminated is after today', () => { party.purchased.plan.customerId = 'test-id'; party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate(); expect(party.hasNotCancelled()).to.be.false; }); - it('returns false if party if plan.dateTerminated is before today', () => { + it('returns false if group if plan.dateTerminated is before today', () => { party.purchased.plan.customerId = 'test-id'; party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate(); expect(party.hasNotCancelled()).to.be.false; }); }); + + context('hasCancelled', () => { + it('returns false if group does not have customer id', () => { + expect(party.hasCancelled()).to.be.false; + }); + + it('returns false if group does not have plan.dateTerminated', () => { + party.purchased.plan.customerId = 'test-id'; + + expect(party.hasCancelled()).to.be.false; + }); + + it('returns true if group if plan.dateTerminated is after today', () => { + party.purchased.plan.customerId = 'test-id'; + party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate(); + + expect(party.hasCancelled()).to.be.true; + }); + + it('returns false if group if plan.dateTerminated is before today', () => { + party.purchased.plan.customerId = 'test-id'; + party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate(); + + expect(party.hasCancelled()).to.be.false; + }); + }); }); }); diff --git a/test/api/unit/models/user.test.js b/test/api/unit/models/user.test.js index 825dee32c4..24652fa7b6 100644 --- a/test/api/unit/models/user.test.js +++ b/test/api/unit/models/user.test.js @@ -315,9 +315,8 @@ describe('User Model', () => { user = new User(); }); - it('returns false if user does not have customer id', () => { - expect(user.hasNotCancelled()).to.be.undefined; + expect(user.hasNotCancelled()).to.be.false; }); it('returns true if user does not have plan.dateTerminated', () => { @@ -341,6 +340,38 @@ describe('User Model', () => { }); }); + + context('hasCancelled', () => { + let user; + beforeEach(() => { + user = new User(); + }); + + it('returns false if user does not have customer id', () => { + expect(user.hasCancelled()).to.be.false; + }); + + it('returns false if user does not have plan.dateTerminated', () => { + user.purchased.plan.customerId = 'test-id'; + + expect(user.hasCancelled()).to.be.false; + }); + + it('returns true if user if plan.dateTerminated is after today', () => { + user.purchased.plan.customerId = 'test-id'; + user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate(); + + expect(user.hasCancelled()).to.be.true; + }); + + it('returns false if user if plan.dateTerminated is before today', () => { + user.purchased.plan.customerId = 'test-id'; + user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate(); + + expect(user.hasCancelled()).to.be.false; + }); + }); + context('pre-save hook', () => { it('does not try to award achievements when achievements or items not selected in query', async () => { let user = new User(); diff --git a/website/server/libs/payments/paypal.js b/website/server/libs/payments/paypal.js index 06e553df42..350af84094 100644 --- a/website/server/libs/payments/paypal.js +++ b/website/server/libs/payments/paypal.js @@ -257,12 +257,18 @@ api.ipn = async function ipnApi (options = {}) { 'recurring_payment_failed', 'recurring_payment_expired', 'subscr_cancel', - 'subscr_failed']; + 'subscr_failed', + ]; if (ipnAcceptableTypes.indexOf(txn_type) === -1) return; + // @TODO: Should this request billing date? let user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec(); if (user) { + // If the user has already cancelled the subscription, return + // Otherwise the subscription would be cancelled twice resulting in the loss of subscription credits + if (user.hasCancelled()) return; + await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD }); return; } @@ -274,6 +280,10 @@ api.ipn = async function ipnApi (options = {}) { .exec(); if (group) { + // If the group subscription has already been cancelled the subscription, return + // Otherwise the subscription would be cancelled twice resulting in the loss of subscription credits + if (group.hasCancelled()) return; + await payments.cancelSubscription({ groupId: group._id, paymentMethod: this.constants.PAYMENT_METHOD }); } }; diff --git a/website/server/models/group.js b/website/server/models/group.js index 15f880fd71..e2939278e3 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -1421,7 +1421,12 @@ schema.methods.isSubscribed = function isSubscribed () { schema.methods.hasNotCancelled = function hasNotCancelled () { let plan = this.purchased.plan; - return this.isSubscribed() && !plan.dateTerminated; + return Boolean(this.isSubscribed() && !plan.dateTerminated); +}; + +schema.methods.hasCancelled = function hasNotCancelled () { + let plan = this.purchased.plan; + return Boolean(this.isSubscribed() && plan.dateTerminated); }; schema.methods.updateGroupPlan = async function updateGroupPlan (removingMember) { diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index c80cec0159..28a51fb158 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -31,7 +31,12 @@ schema.methods.isSubscribed = function isSubscribed () { schema.methods.hasNotCancelled = function hasNotCancelled () { let plan = this.purchased.plan; - return this.isSubscribed() && !plan.dateTerminated; + return Boolean(this.isSubscribed() && !plan.dateTerminated); +}; + +schema.methods.hasCancelled = function hasCancelled () { + let plan = this.purchased.plan; + return Boolean(this.isSubscribed() && plan.dateTerminated); }; // Get an array of groups ids the user is member of