diff --git a/test/api/unit/libs/cron.test.js b/test/api/unit/libs/cron.test.js index eb15119ee1..6757228033 100644 --- a/test/api/unit/libs/cron.test.js +++ b/test/api/unit/libs/cron.test.js @@ -231,13 +231,16 @@ describe('cron', async () => { }, }); // user1 has a 1-month recurring subscription starting today - user1.purchased.plan.customerId = 'subscribedId'; - user1.purchased.plan.dateUpdated = moment().toDate(); - user1.purchased.plan.planId = 'basic'; - user1.purchased.plan.consecutive.count = 0; - user1.purchased.plan.consecutive.offset = 0; - user1.purchased.plan.consecutive.trinkets = 0; - user1.purchased.plan.consecutive.gemCapExtra = 0; + beforeEach(async () => { + user1.purchased.plan.customerId = 'subscribedId'; + user1.purchased.plan.dateUpdated = moment().toDate(); + user1.purchased.plan.planId = 'basic'; + user1.purchased.plan.consecutive.count = 0; + user1.purchased.plan.perkMonthCount = 0; + user1.purchased.plan.consecutive.offset = 0; + user1.purchased.plan.consecutive.trinkets = 0; + user1.purchased.plan.consecutive.gemCapExtra = 0; + }); it('does not increment consecutive benefits after the first month', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') @@ -271,6 +274,24 @@ describe('cron', async () => { expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0); }); + it('increments consecutive benefits after the second month if they also received a 1 month gift subscription', async () => { + user1.purchased.plan.perkMonthCount = 1; + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') + .add(2, 'days') + .toDate()); + // Add 1 month to simulate what happens a month after the subscription was created. + // Add 2 days so that we're sure we're not affected by any start-of-month effects + // e.g., from time zone oddness. + await cron({ + user: user1, tasksByType, daysMissed, analytics, + }); + expect(user1.purchased.plan.perkMonthCount).to.equal(0); + expect(user1.purchased.plan.consecutive.count).to.equal(2); + expect(user1.purchased.plan.consecutive.offset).to.equal(0); + expect(user1.purchased.plan.consecutive.trinkets).to.equal(1); + expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5); + }); + it('increments consecutive benefits after the third month', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') @@ -315,6 +336,30 @@ describe('cron', async () => { expect(user1.purchased.plan.consecutive.trinkets).to.equal(3); expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15); }); + + it('initializes plan.perkMonthCount if necessary', async () => { + user.purchased.plan.perkMonthCount = undefined; + clock = sinon.useFakeTimers(moment(user.purchased.plan.dateUpdated) + .utcOffset(0) + .startOf('month') + .add(1, 'months') + .add(2, 'days') + .toDate()); + await cron({ + user, tasksByType, daysMissed, analytics, + }); + expect(user.purchased.plan.perkMonthCount).to.equal(1); + user.purchased.plan.perkMonthCount = undefined; + user.purchased.plan.consecutive.count = 8; + clock.restore(); + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') + .add(2, 'days') + .toDate()); + await cron({ + user, tasksByType, daysMissed, analytics, + }); + expect(user.purchased.plan.perkMonthCount).to.equal(2); + }); }); describe('for a 3-month recurring subscription', async () => { @@ -330,13 +375,16 @@ describe('cron', async () => { }, }); // user3 has a 3-month recurring subscription starting today - user3.purchased.plan.customerId = 'subscribedId'; - user3.purchased.plan.dateUpdated = moment().toDate(); - user3.purchased.plan.planId = 'basic_3mo'; - user3.purchased.plan.consecutive.count = 0; - user3.purchased.plan.consecutive.offset = 3; - user3.purchased.plan.consecutive.trinkets = 1; - user3.purchased.plan.consecutive.gemCapExtra = 5; + beforeEach(async () => { + user3.purchased.plan.customerId = 'subscribedId'; + user3.purchased.plan.dateUpdated = moment().toDate(); + user3.purchased.plan.planId = 'basic_3mo'; + user3.purchased.plan.perkMonthCount = 0; + user3.purchased.plan.consecutive.count = 0; + user3.purchased.plan.consecutive.offset = 3; + user3.purchased.plan.consecutive.trinkets = 1; + user3.purchased.plan.consecutive.gemCapExtra = 5; + }); it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') @@ -390,6 +438,21 @@ describe('cron', async () => { expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10); }); + it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => { + user3.purchased.plan.perkMonthCount = 2; + user3.purchased.plan.consecutive.trinkets = 1; + user3.purchased.plan.consecutive.gemCapExtra = 5; + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') + .add(2, 'days') + .toDate()); + await cron({ + user: user3, tasksByType, daysMissed, analytics, + }); + expect(user3.purchased.plan.perkMonthCount).to.equal(2); + expect(user3.purchased.plan.consecutive.trinkets).to.equal(2); + expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10); + }); + it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months') .add(2, 'days') @@ -456,13 +519,16 @@ describe('cron', async () => { }, }); // user6 has a 6-month recurring subscription starting today - user6.purchased.plan.customerId = 'subscribedId'; - user6.purchased.plan.dateUpdated = moment().toDate(); - user6.purchased.plan.planId = 'google_6mo'; - user6.purchased.plan.consecutive.count = 0; - user6.purchased.plan.consecutive.offset = 6; - user6.purchased.plan.consecutive.trinkets = 2; - user6.purchased.plan.consecutive.gemCapExtra = 10; + beforeEach(async () => { + user6.purchased.plan.customerId = 'subscribedId'; + user6.purchased.plan.dateUpdated = moment().toDate(); + user6.purchased.plan.planId = 'google_6mo'; + user6.purchased.plan.perkMonthCount = 0; + user6.purchased.plan.consecutive.count = 0; + user6.purchased.plan.consecutive.offset = 6; + user6.purchased.plan.consecutive.trinkets = 2; + user6.purchased.plan.consecutive.gemCapExtra = 10; + }); it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') @@ -503,6 +569,19 @@ describe('cron', async () => { expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20); }); + it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => { + user6.purchased.plan.perkMonthCount = 2; + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') + .add(2, 'days') + .toDate()); + await cron({ + user: user6, tasksByType, daysMissed, analytics, + }); + expect(user6.purchased.plan.perkMonthCount).to.equal(2); + expect(user6.purchased.plan.consecutive.trinkets).to.equal(4); + expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20); + }); + it('increments consecutive benefits the month after the third paid period has started', async () => { clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months') .add(2, 'days') diff --git a/test/api/unit/libs/payments/apple.test.js b/test/api/unit/libs/payments/apple.test.js index 217a307d0b..2f01fef793 100644 --- a/test/api/unit/libs/payments/apple.test.js +++ b/test/api/unit/libs/payments/apple.test.js @@ -29,8 +29,9 @@ describe('Apple Payments', () => { .resolves(); iapValidateStub = sinon.stub(iap, 'validate') .resolves({}); - iapIsValidatedStub = sinon.stub(iap, 'isValidated') - .returns(true); + iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true); + sinon.stub(iap, 'isExpired').returns(false); + sinon.stub(iap, 'isCanceled').returns(false); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ productId: 'com.habitrpg.ios.Habitica.21gems', @@ -44,6 +45,8 @@ describe('Apple Payments', () => { iap.setup.restore(); iap.validate.restore(); iap.isValidated.restore(); + iap.isExpired.restore(); + iap.isCanceled.restore(); iap.getPurchaseData.restore(); payments.buySkuItem.restore(); gems.validateGiftMessage.restore(); @@ -218,6 +221,7 @@ describe('Apple Payments', () => { headers = {}; receipt = `{"token": "${token}"}`; nextPaymentProcessing = moment.utc().add({ days: 2 }); + user = new User(); iapSetupStub = sinon.stub(iap, 'setup') .resolves(); @@ -228,14 +232,17 @@ describe('Apple Payments', () => { iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ expirationDate: moment.utc().subtract({ day: 1 }).toDate(), + purchaseDate: moment.utc().valueOf(), productId: sku, transactionId: token, }, { expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().valueOf(), productId: 'wrongsku', transactionId: token, }, { expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().valueOf(), productId: sku, transactionId: token, }]); @@ -250,21 +257,12 @@ describe('Apple Payments', () => { if (payments.createSubscription.restore) payments.createSubscription.restore(); }); - it('should throw an error if sku is empty', async () => { - await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing)) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 400, - name: 'BadRequest', - message: i18n.t('missingSubscriptionCode'), - }); - }); - it('should throw an error if receipt is invalid', async () => { iap.isValidated.restore(); iapIsValidatedStub = sinon.stub(iap, 'isValidated') .returns(false); - await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing)) + await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing)) .to.eventually.be.rejected.and.to.eql({ httpCode: 401, name: 'NotAuthorized', @@ -295,13 +293,15 @@ describe('Apple Payments', () => { iap.getPurchaseData.restore(); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ - expirationDate: moment.utc().add({ day: 1 }).toDate(), + expirationDate: moment.utc().add({ day: 2 }).toDate(), + purchaseDate: new Date(), productId: option.sku, transactionId: token, + originalTransactionId: token, }]); sub = common.content.subscriptionBlocks[option.subKey]; - await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing); + await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); expect(iapSetupStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce; @@ -321,21 +321,253 @@ describe('Apple Payments', () => { nextPaymentProcessing, }); }); + if (option !== subOptions[3]) { + const newOption = subOptions[3]; + it(`upgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => { + const oldSub = common.content.subscriptionBlocks[option.subKey]; + oldSub.logic = 'refundAndRepay'; + user.profile.name = 'sender'; + user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE; + user.purchased.plan.customerId = token; + user.purchased.plan.planId = option.subKey; + user.purchased.plan.additionalData = receipt; + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 2 }).toDate(), + purchaseDate: moment.utc().valueOf(), + productId: newOption.sku, + transactionId: `${token}new`, + originalTransactionId: token, + }]); + sub = common.content.subscriptionBlocks[newOption.subKey]; + + await applePayments.subscribe(user, + receipt, + headers, + nextPaymentProcessing); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({}); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + customerId: token, + paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, + sub, + headers, + additionalData: receipt, + nextPaymentProcessing, + updatedFrom: oldSub, + }); + }); + } + if (option !== subOptions[0]) { + const newOption = subOptions[0]; + it(`downgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => { + const oldSub = common.content.subscriptionBlocks[option.subKey]; + user.profile.name = 'sender'; + user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE; + user.purchased.plan.customerId = token; + user.purchased.plan.planId = option.subKey; + user.purchased.plan.additionalData = receipt; + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 2 }).toDate(), + purchaseDate: moment.utc().valueOf(), + productId: newOption.sku, + transactionId: `${token}new`, + originalTransactionId: token, + }]); + sub = common.content.subscriptionBlocks[newOption.subKey]; + + await applePayments.subscribe(user, + receipt, + headers, + nextPaymentProcessing); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({}); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + customerId: token, + paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, + sub, + headers, + additionalData: receipt, + nextPaymentProcessing, + updatedFrom: oldSub, + }); + }); + } }); - it('errors when a user is already subscribed', async () => { - payments.createSubscription.restore(); - user = new User(); - await user.save(); + it('uses the most recent subscription data', async () => { + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 4 }).toDate(), + purchaseDate: moment.utc().subtract({ day: 5 }).toDate(), + productId: 'com.habitrpg.ios.habitica.subscription.3month', + transactionId: `${token}oldest`, + originalTransactionId: `${token}evenOlder`, + }, { + expirationDate: moment.utc().add({ day: 2 }).toDate(), + purchaseDate: moment.utc().subtract({ day: 1 }).toDate(), + productId: 'com.habitrpg.ios.habitica.subscription.12month', + transactionId: `${token}newest`, + originalTransactionId: `${token}newest`, + }, { + expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().subtract({ day: 2 }).toDate(), + productId: 'com.habitrpg.ios.habitica.subscription.6month', + transactionId: token, + originalTransactionId: token, + }]); + sub = common.content.subscriptionBlocks.basic_12mo; - await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing); + await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); - await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing)) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - name: 'NotAuthorized', - message: applePayments.constants.RESPONSE_ALREADY_USED, - }); + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + customerId: `${token}newest`, + paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, + sub, + headers, + additionalData: receipt, + nextPaymentProcessing, + }); + }); + + describe('does not apply multiple times', async () => { + it('errors when a user is using the same subscription', async () => { + payments.createSubscription.restore(); + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().toDate(), + productId: sku, + transactionId: token, + originalTransactionId: token, + }]); + + await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); + + await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_ALREADY_USED, + }); + }); + + it('errors when a user is using a rebill of the same subscription', async () => { + user = new User(); + await user.save(); + payments.createSubscription.restore(); + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().toDate(), + productId: sku, + transactionId: `${token}renew`, + originalTransactionId: token, + }]); + + await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); + + await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_ALREADY_USED, + }); + }); + + it('errors when a different user is using the subscription', async () => { + user = new User(); + await user.save(); + payments.createSubscription.restore(); + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().toDate(), + productId: sku, + transactionId: token, + originalTransactionId: token, + }]); + + await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); + + const secondUser = new User(); + await secondUser.save(); + await expect(applePayments.subscribe( + secondUser, receipt, headers, nextPaymentProcessing, + )) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_ALREADY_USED, + }); + }); + + it('errors when a multiple users exist using the subscription', async () => { + user = new User(); + await user.save(); + payments.createSubscription.restore(); + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().toDate(), + productId: sku, + transactionId: token, + originalTransactionId: token, + }]); + + await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); + const secondUser = new User(); + secondUser.purchased.plan = user.purchased.plan; + secondUser.purchased.plan.dateTerminate = new Date(); + secondUser.save(); + + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({ day: 1 }).toDate(), + purchaseDate: moment.utc().toDate(), + productId: sku, + transactionId: `${token}new`, + originalTransactionId: token, + }]); + + const thirdUser = new User(); + await thirdUser.save(); + await expect(applePayments.subscribe( + thirdUser, receipt, headers, nextPaymentProcessing, + )) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_ALREADY_USED, + }); + }); }); }); @@ -360,9 +592,9 @@ describe('Apple Payments', () => { }); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ expirationDate: expirationDate.toDate() }]); - iapIsValidatedStub = sinon.stub(iap, 'isValidated') - .returns(true); - + iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true); + sinon.stub(iap, 'isCanceled').returns(false); + sinon.stub(iap, 'isExpired').returns(true); user = new User(); user.profile.name = 'sender'; user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE; @@ -377,6 +609,8 @@ describe('Apple Payments', () => { iap.setup.restore(); iap.validate.restore(); iap.isValidated.restore(); + iap.isExpired.restore(); + iap.isCanceled.restore(); iap.getPurchaseData.restore(); payments.cancelSubscription.restore(); }); @@ -396,6 +630,8 @@ describe('Apple Payments', () => { iap.getPurchaseData.restore(); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') .returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]); + iap.isExpired.restore(); + sinon.stub(iap, 'isExpired').returns(false); await expect(applePayments.cancelSubscribe(user, headers)) .to.eventually.be.rejected.and.to.eql({ @@ -418,7 +654,38 @@ describe('Apple Payments', () => { }); }); - it('should cancel a user subscription', async () => { + it('should cancel a cancelled subscription with termination date in the future', async () => { + const futureDate = expirationDate.add({ day: 1 }); + iap.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') + .returns([{ expirationDate: futureDate }]); + iap.isExpired.restore(); + sinon.stub(iap, 'isExpired').returns(false); + + iap.isCanceled.restore(); + sinon.stub(iap, 'isCanceled').returns(true); + + await applePayments.cancelSubscribe(user, headers); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({ + expirationDate: futureDate, + }); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, + nextBill: futureDate.toDate(), + headers, + }); + }); + + it('should cancel an expired subscription', async () => { await applePayments.cancelSubscribe(user, headers); expect(iapSetupStub).to.be.calledOnce; diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 65b42b42f3..fc069b6fa9 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -203,6 +203,28 @@ describe('payments/index', () => { expect(recipient.purchased.plan.dateCreated).to.exist; }); + it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => { + expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.exist; + + await api.createSubscription(data); + + expect(recipient.purchased.plan.dateCurrentTypeCreated).to.exist; + }); + + it('keeps plan.dateCreated when changing subscription type', async () => { + await api.createSubscription(data); + const initialDate = recipient.purchased.plan.dateCreated; + await api.createSubscription(data); + expect(recipient.purchased.plan.dateCreated).to.eql(initialDate); + }); + + it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => { + await api.createSubscription(data); + const initialDate = recipient.purchased.plan.dateCurrentTypeCreated; + await api.createSubscription(data); + expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate); + }); + it('does not change plan.customerId if it already exists', async () => { recipient.purchased.plan = plan; data.customerId = 'purchaserCustomerId'; @@ -213,6 +235,65 @@ describe('payments/index', () => { expect(recipient.purchased.plan.customerId).to.eql('customer-id'); }); + it('sets plan.perkMonthCount to zero if user is not subscribed', async () => { + recipient.purchased.plan = plan; + recipient.purchased.plan.perkMonthCount = 1; + recipient.purchased.plan.customerId = undefined; + data.sub.key = 'basic_earned'; + data.gift.subscription.key = 'basic_earned'; + data.gift.subscription.months = 1; + + expect(recipient.purchased.plan.perkMonthCount).to.eql(1); + await api.createSubscription(data); + + expect(recipient.purchased.plan.perkMonthCount).to.eql(0); + }); + + it('adds to plan.perkMonthCount if user is already subscribed', async () => { + recipient.purchased.plan = plan; + recipient.purchased.plan.perkMonthCount = 1; + data.sub.key = 'basic_earned'; + data.gift.subscription.key = 'basic_earned'; + data.gift.subscription.months = 1; + + expect(recipient.purchased.plan.perkMonthCount).to.eql(1); + await api.createSubscription(data); + + expect(recipient.purchased.plan.perkMonthCount).to.eql(2); + }); + + it('awards perks if plan.perkMonthCount reaches 3', async () => { + recipient.purchased.plan = plan; + recipient.purchased.plan.perkMonthCount = 2; + data.sub.key = 'basic_earned'; + data.gift.subscription.key = 'basic_earned'; + data.gift.subscription.months = 1; + + expect(recipient.purchased.plan.perkMonthCount).to.eql(2); + expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0); + expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0); + await api.createSubscription(data); + + expect(recipient.purchased.plan.perkMonthCount).to.eql(0); + expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1); + expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5); + }); + + it('awards perks if plan.perkMonthCount goes over 3', async () => { + recipient.purchased.plan = plan; + recipient.purchased.plan.perkMonthCount = 2; + data.sub.key = 'basic_earned'; + + expect(recipient.purchased.plan.perkMonthCount).to.eql(2); + expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0); + expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0); + await api.createSubscription(data); + + expect(recipient.purchased.plan.perkMonthCount).to.eql(2); + expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1); + expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5); + }); + it('sets plan.customerId to "Gift" if it does not already exist', async () => { expect(recipient.purchased.plan.customerId).to.not.exist; @@ -379,6 +460,7 @@ describe('payments/index', () => { expect(user.purchased.plan.customerId).to.eql('customer-id'); expect(user.purchased.plan.dateUpdated).to.exist; expect(user.purchased.plan.gemsBought).to.eql(0); + expect(user.purchased.plan.perkMonthCount).to.eql(0); expect(user.purchased.plan.paymentMethod).to.eql('Payment Method'); expect(user.purchased.plan.extraMonths).to.eql(0); expect(user.purchased.plan.dateTerminated).to.eql(null); @@ -386,6 +468,63 @@ describe('payments/index', () => { expect(user.purchased.plan.dateCreated).to.exist; }); + it('sets plan.dateCreated if it did not previously exist', async () => { + expect(user.purchased.plan.dateCreated).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.dateCreated).to.exist; + }); + + it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => { + expect(user.purchased.plan.dateCurrentTypeCreated).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.dateCurrentTypeCreated).to.exist; + }); + + it('keeps plan.dateCreated when changing subscription type', async () => { + await api.createSubscription(data); + const initialDate = user.purchased.plan.dateCreated; + await api.createSubscription(data); + expect(user.purchased.plan.dateCreated).to.eql(initialDate); + }); + + it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => { + await api.createSubscription(data); + const initialDate = user.purchased.plan.dateCurrentTypeCreated; + await api.createSubscription(data); + expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate); + }); + + it('keeps plan.perkMonthCount when changing subscription type', async () => { + await api.createSubscription(data); + user.purchased.plan.perkMonthCount = 2; + await api.createSubscription(data); + expect(user.purchased.plan.perkMonthCount).to.eql(2); + }); + + it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => { + user.purchased.plan.perkMonthCount = 2; + await api.createSubscription(data); + expect(user.purchased.plan.perkMonthCount).to.eql(0); + }); + + it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => { + user.purchased.plan.perkMonthCount = 2; + await api.createSubscription(data); + expect(user.purchased.plan.perkMonthCount).to.eql(0); + }); + + it('updates plan.consecutive.offset when changing subscription type', async () => { + await api.createSubscription(data); + expect(user.purchased.plan.consecutive.offset).to.eql(3); + data.sub.key = 'basic_6mo'; + await api.createSubscription(data); + expect(user.purchased.plan.consecutive.offset).to.eql(6); + }); + it('awards the Royal Purple Jackalope pet', async () => { await api.createSubscription(data); @@ -465,6 +604,89 @@ describe('payments/index', () => { }, }); }); + + context('Upgrades subscription', () => { + it('from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + const created = user.purchased.plan.dateCreated; + const updated = user.purchased.plan.dateUpdated; + + data.sub.key = 'basic_6mo'; + data.updatedFrom = { key: 'basic_earned' }; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.dateCreated).to.eql(created); + expect(user.purchased.plan.dateUpdated).to.not.eql(updated); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + }); + + it('from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + const created = user.purchased.plan.dateCreated; + const updated = user.purchased.plan.dateUpdated; + + data.sub.key = 'basic_12mo'; + data.updatedFrom = { key: 'basic_3mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.dateCreated).to.eql(created); + expect(user.purchased.plan.dateUpdated).to.not.eql(updated); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + }); + }); + + context('Downgrades subscription', () => { + it('from basic_6mo to basic_earned', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + const created = user.purchased.plan.dateCreated; + const updated = user.purchased.plan.dateUpdated; + + data.sub.key = 'basic_earned'; + data.updatedFrom = { key: 'basic_6mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.dateCreated).to.eql(created); + expect(user.purchased.plan.dateUpdated).to.not.eql(updated); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + }); + + it('from basic_12mo to basic_3mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + data.sub.key = 'basic_12mo'; + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + const created = user.purchased.plan.dateCreated; + const updated = user.purchased.plan.dateUpdated; + + data.sub.key = 'basic_3mo'; + data.updatedFrom = { key: 'basic_12mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.dateCreated).to.eql(created); + expect(user.purchased.plan.dateUpdated).to.not.eql(updated); + expect(user.purchased.plan.customerId).to.eql('customer-id'); + }); + }); }); context('Block subscription perks', () => { @@ -488,7 +710,6 @@ describe('payments/index', () => { it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => { data.sub.key = 'basic_6mo'; - await api.createSubscription(data); expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); @@ -496,7 +717,6 @@ describe('payments/index', () => { it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => { data.sub.key = 'basic_12mo'; - await api.createSubscription(data); expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); @@ -532,6 +752,532 @@ describe('payments/index', () => { expect(user.purchased.plan.consecutive.trinkets).to.eql(4); }); + + context('Upgrades subscription', () => { + context('Using payDifference logic', () => { + beforeEach(async () => { + data.updatedFrom = { logic: 'payDifference' }; + }); + it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); + }); + + it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); + }); + + it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_6mo'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + + it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + }); + + context('Using payFull logic', () => { + beforeEach(async () => { + data.updatedFrom = { logic: 'payFull' }; + }); + it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); + }); + + it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25); + }); + + it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_6mo'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(6); + }); + + it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(5); + }); + }); + + context('Using refundAndRepay logic', () => { + let clock; + beforeEach(async () => { + clock = sinon.useFakeTimers(new Date('2022-01-01')); + data.updatedFrom = { logic: 'refundAndRepay' }; + }); + context('Upgrades within first half of subscription', () => { + it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-01-10')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); + }); + + it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-02-05')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); + }); + + it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-01-08')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-01-31')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + + it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_6mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-01-28')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + + it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-01-08')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_6mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-08-28')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + + it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-07-31')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + }); + context('Upgrades within second half of subscription', () => { + it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-01-20')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); + }); + + it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-02-24')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25); + }); + + it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-01-28')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_6mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-05-28')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(6); + }); + + it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-03-03')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(5); + }); + + it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => { + data.sub.key = 'basic_earned'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(0); + + data.sub.key = 'basic_6mo'; + data.updatedFrom.key = 'basic_earned'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2022-05-28')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_6mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2023-05-28')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(6); + }); + + it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_3mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(1); + + data.sub.key = 'basic_12mo'; + data.updatedFrom.key = 'basic_3mo'; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2023-09-03')); + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(5); + }); + }); + afterEach(async () => { + if (clock !== null) clock.restore(); + }); + }); + }); + + context('Downgrades subscription', () => { + it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); + + data.sub.key = 'basic_earned'; + data.updatedFrom = { key: 'basic_6mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); + }); + + it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + data.sub.key = 'basic_12mo'; + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); + + data.sub.key = 'basic_3mo'; + data.updatedFrom = { key: 'basic_12mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); + }); + + it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => { + data.sub.key = 'basic_6mo'; + expect(user.purchased.plan.planId).to.not.exist; + + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_6mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + + data.sub.key = 'basic_earned'; + data.updatedFrom = { key: 'basic_6mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.planId).to.eql('basic_earned'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(2); + }); + + it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => { + expect(user.purchased.plan.planId).to.not.exist; + + data.sub.key = 'basic_12mo'; + await api.createSubscription(data); + + expect(user.purchased.plan.planId).to.eql('basic_12mo'); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + + data.sub.key = 'basic_3mo'; + data.updatedFrom = { key: 'basic_12mo' }; + await api.createSubscription(data); + expect(user.purchased.plan.consecutive.trinkets).to.eql(4); + }); + }); }); context('Mystery Items', () => { diff --git a/test/api/v3/integration/payments/apple/POST-payments_apple_subscribe.test.js b/test/api/v3/integration/payments/apple/POST-payments_apple_subscribe.test.js index faf5f0e6ea..5a9aa456e7 100644 --- a/test/api/v3/integration/payments/apple/POST-payments_apple_subscribe.test.js +++ b/test/api/v3/integration/payments/apple/POST-payments_apple_subscribe.test.js @@ -45,11 +45,10 @@ describe('payments : apple #subscribe', () => { }); expect(subscribeStub).to.be.calledOnce; - expect(subscribeStub.args[0][0]).to.eql(sku); - expect(subscribeStub.args[0][1]._id).to.eql(user._id); - expect(subscribeStub.args[0][2]).to.eql('receipt'); - expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken); - expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id); + expect(subscribeStub.args[0][0]._id).to.eql(user._id); + expect(subscribeStub.args[0][1]).to.eql('receipt'); + expect(subscribeStub.args[0][2]['x-api-key']).to.eql(user.apiToken); + expect(subscribeStub.args[0][2]['x-api-user']).to.eql(user._id); }); }); }); diff --git a/test/common/libs/cron.test.js b/test/common/libs/cron.test.js index d082e1e7b0..e8be92fceb 100644 --- a/test/common/libs/cron.test.js +++ b/test/common/libs/cron.test.js @@ -215,6 +215,7 @@ describe('cron utility functions', () => { it('monthly plan, next date in 3 months', () => { const user = baseUserData(60, 0, 'group_plan_auto'); + user.purchased.plan.perkMonthCount = 0; const planContext = getPlanContext(user, now); @@ -224,6 +225,7 @@ describe('cron utility functions', () => { it('monthly plan, next date in 1 month', () => { const user = baseUserData(62, 0, 'group_plan_auto'); + user.purchased.plan.perkMonthCount = 2; const planContext = getPlanContext(user, now); @@ -248,5 +250,15 @@ describe('cron utility functions', () => { expect(planContext.nextHourglassDate) .to.be.sameMoment('2022-07-10T02:00:00.144Z'); }); + + it('multi-month plan with perk count', () => { + const user = baseUserData(60, 1, 'basic_3mo'); + user.purchased.plan.perkMonthCount = 2; + + const planContext = getPlanContext(user, now); + + expect(planContext.nextHourglassDate) + .to.be.sameMoment('2022-07-10T02:00:00.144Z'); + }); }); }); diff --git a/test/helpers/api-integration/external-server.js b/test/helpers/api-integration/external-server.js index 5b199d058d..5f02eb03c3 100644 --- a/test/helpers/api-integration/external-server.js +++ b/test/helpers/api-integration/external-server.js @@ -12,8 +12,9 @@ const webhookData = {}; app.use(bodyParser.urlencoded({ extended: true, + limit: '10mb', })); -app.use(bodyParser.json()); +app.use(bodyParser.json({ limit: '10mb' })); app.post('/webhooks/:id', (req, res) => { const { id } = req.params; diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 42dd961794..de8d44f062 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -13318,11 +13318,34 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -13354,6 +13377,38 @@ "ansi-regex": "^5.0.1" } }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.8.3", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", + "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -30574,76 +30629,6 @@ } } }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.8.3", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", - "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "vue-mugen-scroll": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/vue-mugen-scroll/-/vue-mugen-scroll-0.2.6.tgz", diff --git a/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue b/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue index 8fa3cffb0b..9e7ffff117 100644 --- a/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue +++ b/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue @@ -46,6 +46,10 @@ Perk offset months: {{ hero.purchased.plan.consecutive.offset }} +