From 65d2eac4c3a20529d633b8d7f511a9e365442336 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Fri, 10 Jun 2022 21:07:36 +0200 Subject: [PATCH] Correctly handle google accounts with multiple subscriptions (#13982) --- test/api/unit/libs/payments/google.test.js | 53 ++++++++++++++++++++++ website/server/libs/payments/google.js | 29 +++++++----- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/test/api/unit/libs/payments/google.test.js b/test/api/unit/libs/payments/google.test.js index 3cf40b0a4e..7df7f273ef 100644 --- a/test/api/unit/libs/payments/google.test.js +++ b/test/api/unit/libs/payments/google.test.js @@ -326,6 +326,36 @@ describe('Google Payments', () => { }); }); + it('should cancel a user subscription with multiple inactive subscriptions', async () => { + const laterDate = moment.utc().add(7, 'days'); + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ expirationDate, autoRenewing: false }, + { expirationDate: laterDate, autoRenewing: false }, + ]); + await googlePayments.cancelSubscribe(user, headers); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, { + data: receipt, + signature, + }); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({ + expirationDate, + }); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE, + nextBill: laterDate.toDate(), + headers, + }); + }); + it('should not cancel a user subscription with autorenew', async () => { iap.getPurchaseData.restore(); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') @@ -346,5 +376,28 @@ describe('Google Payments', () => { expect(paymentCancelSubscriptionSpy).to.not.be.called; }); + + it('should not cancel a user subscription with multiple subscriptions with one autorenew', async () => { + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ expirationDate, autoRenewing: false }, + { autoRenewing: true }, + { expirationDate, autoRenewing: false }]); + await googlePayments.cancelSubscribe(user, headers); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, { + data: receipt, + signature, + }); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({ + expirationDate, + }); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentCancelSubscriptionSpy).to.not.be.called; + }); }); }); diff --git a/website/server/libs/payments/google.js b/website/server/libs/payments/google.js index 86f0967213..9beac9b2a4 100644 --- a/website/server/libs/payments/google.js +++ b/website/server/libs/payments/google.js @@ -244,11 +244,15 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) { const purchases = iap.getPurchaseData(googleRes); if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT); - const subscriptionData = purchases[0]; - // Check to make sure the sub isn't active anymore. - if (subscriptionData.autoRenewing !== false) return; - - dateTerminated = new Date(Number(subscriptionData.expirationDate)); + for (const i in purchases) { + if (Object.prototype.hasOwnProperty.call(purchases, i)) { + const purchase = purchases[i]; + if (purchase.autoRenewing !== false) return; + if (!dateTerminated || Number(purchase.expirationDate) > Number(dateTerminated)) { + dateTerminated = new Date(Number(purchase.expirationDate)); + } + } + } } catch (err) { // Status:410 means that the subsctiption isn't active anymore and we can safely delete it if (err && err.message === 'Status:410') { @@ -257,13 +261,14 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) { throw err; } } - - await payments.cancelSubscription({ - user, - nextBill: dateTerminated, - paymentMethod: this.constants.PAYMENT_METHOD_GOOGLE, - headers, - }); + if (dateTerminated) { + await payments.cancelSubscription({ + user, + nextBill: dateTerminated, + paymentMethod: this.constants.PAYMENT_METHOD_GOOGLE, + headers, + }); + } }; export default api;