mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Merge branch 'apple_sub_fix' into release
This commit is contained in:
@@ -231,13 +231,16 @@ describe('cron', async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// user1 has a 1-month recurring subscription starting today
|
// user1 has a 1-month recurring subscription starting today
|
||||||
|
beforeEach(async () => {
|
||||||
user1.purchased.plan.customerId = 'subscribedId';
|
user1.purchased.plan.customerId = 'subscribedId';
|
||||||
user1.purchased.plan.dateUpdated = moment().toDate();
|
user1.purchased.plan.dateUpdated = moment().toDate();
|
||||||
user1.purchased.plan.planId = 'basic';
|
user1.purchased.plan.planId = 'basic';
|
||||||
user1.purchased.plan.consecutive.count = 0;
|
user1.purchased.plan.consecutive.count = 0;
|
||||||
|
user1.purchased.plan.perkMonthCount = 0;
|
||||||
user1.purchased.plan.consecutive.offset = 0;
|
user1.purchased.plan.consecutive.offset = 0;
|
||||||
user1.purchased.plan.consecutive.trinkets = 0;
|
user1.purchased.plan.consecutive.trinkets = 0;
|
||||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||||
|
});
|
||||||
|
|
||||||
it('does not increment consecutive benefits after the first month', async () => {
|
it('does not increment consecutive benefits after the first month', async () => {
|
||||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
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);
|
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 () => {
|
it('increments consecutive benefits after the third month', async () => {
|
||||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||||
.add(2, 'days')
|
.add(2, 'days')
|
||||||
@@ -315,6 +336,30 @@ describe('cron', async () => {
|
|||||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
|
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
|
||||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
|
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 () => {
|
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 has a 3-month recurring subscription starting today
|
||||||
|
beforeEach(async () => {
|
||||||
user3.purchased.plan.customerId = 'subscribedId';
|
user3.purchased.plan.customerId = 'subscribedId';
|
||||||
user3.purchased.plan.dateUpdated = moment().toDate();
|
user3.purchased.plan.dateUpdated = moment().toDate();
|
||||||
user3.purchased.plan.planId = 'basic_3mo';
|
user3.purchased.plan.planId = 'basic_3mo';
|
||||||
|
user3.purchased.plan.perkMonthCount = 0;
|
||||||
user3.purchased.plan.consecutive.count = 0;
|
user3.purchased.plan.consecutive.count = 0;
|
||||||
user3.purchased.plan.consecutive.offset = 3;
|
user3.purchased.plan.consecutive.offset = 3;
|
||||||
user3.purchased.plan.consecutive.trinkets = 1;
|
user3.purchased.plan.consecutive.trinkets = 1;
|
||||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
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 () => {
|
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')
|
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);
|
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 () => {
|
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')
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
|
||||||
.add(2, 'days')
|
.add(2, 'days')
|
||||||
@@ -456,13 +519,16 @@ describe('cron', async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// user6 has a 6-month recurring subscription starting today
|
// user6 has a 6-month recurring subscription starting today
|
||||||
|
beforeEach(async () => {
|
||||||
user6.purchased.plan.customerId = 'subscribedId';
|
user6.purchased.plan.customerId = 'subscribedId';
|
||||||
user6.purchased.plan.dateUpdated = moment().toDate();
|
user6.purchased.plan.dateUpdated = moment().toDate();
|
||||||
user6.purchased.plan.planId = 'google_6mo';
|
user6.purchased.plan.planId = 'google_6mo';
|
||||||
|
user6.purchased.plan.perkMonthCount = 0;
|
||||||
user6.purchased.plan.consecutive.count = 0;
|
user6.purchased.plan.consecutive.count = 0;
|
||||||
user6.purchased.plan.consecutive.offset = 6;
|
user6.purchased.plan.consecutive.offset = 6;
|
||||||
user6.purchased.plan.consecutive.trinkets = 2;
|
user6.purchased.plan.consecutive.trinkets = 2;
|
||||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
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 () => {
|
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')
|
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);
|
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 () => {
|
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')
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||||
.add(2, 'days')
|
.add(2, 'days')
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ describe('Apple Payments', () => {
|
|||||||
.resolves();
|
.resolves();
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
iapValidateStub = sinon.stub(iap, 'validate')
|
||||||
.resolves({});
|
.resolves({});
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||||
.returns(true);
|
sinon.stub(iap, 'isExpired').returns(false);
|
||||||
|
sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||||
@@ -44,6 +45,8 @@ describe('Apple Payments', () => {
|
|||||||
iap.setup.restore();
|
iap.setup.restore();
|
||||||
iap.validate.restore();
|
iap.validate.restore();
|
||||||
iap.isValidated.restore();
|
iap.isValidated.restore();
|
||||||
|
iap.isExpired.restore();
|
||||||
|
iap.isCanceled.restore();
|
||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
payments.buySkuItem.restore();
|
payments.buySkuItem.restore();
|
||||||
gems.validateGiftMessage.restore();
|
gems.validateGiftMessage.restore();
|
||||||
@@ -218,6 +221,7 @@ describe('Apple Payments', () => {
|
|||||||
headers = {};
|
headers = {};
|
||||||
receipt = `{"token": "${token}"}`;
|
receipt = `{"token": "${token}"}`;
|
||||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||||
|
user = new User();
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
iapSetupStub = sinon.stub(iap, 'setup')
|
||||||
.resolves();
|
.resolves();
|
||||||
@@ -228,14 +232,17 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
}, {
|
}, {
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: 'wrongsku',
|
productId: 'wrongsku',
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
}, {
|
}, {
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||||
|
purchaseDate: moment.utc().valueOf(),
|
||||||
productId: sku,
|
productId: sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
}]);
|
}]);
|
||||||
@@ -250,21 +257,12 @@ describe('Apple Payments', () => {
|
|||||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
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 () => {
|
it('should throw an error if receipt is invalid', async () => {
|
||||||
iap.isValidated.restore();
|
iap.isValidated.restore();
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||||
.returns(false);
|
.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({
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
httpCode: 401,
|
httpCode: 401,
|
||||||
name: 'NotAuthorized',
|
name: 'NotAuthorized',
|
||||||
@@ -295,13 +293,15 @@ describe('Apple Payments', () => {
|
|||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||||
|
purchaseDate: new Date(),
|
||||||
productId: option.sku,
|
productId: option.sku,
|
||||||
transactionId: token,
|
transactionId: token,
|
||||||
|
originalTransactionId: token,
|
||||||
}]);
|
}]);
|
||||||
sub = common.content.subscriptionBlocks[option.subKey];
|
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(iapSetupStub).to.be.calledOnce;
|
||||||
expect(iapValidateStub).to.be.calledOnce;
|
expect(iapValidateStub).to.be.calledOnce;
|
||||||
@@ -321,22 +321,254 @@ describe('Apple Payments', () => {
|
|||||||
nextPaymentProcessing,
|
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 () => {
|
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(user, receipt, headers, nextPaymentProcessing);
|
||||||
|
|
||||||
|
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();
|
payments.createSubscription.restore();
|
||||||
user = new User();
|
iap.getPurchaseData.restore();
|
||||||
await user.save();
|
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(sku, user, receipt, headers, nextPaymentProcessing);
|
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||||
|
|
||||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||||
.to.eventually.be.rejected.and.to.eql({
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
httpCode: 401,
|
httpCode: 401,
|
||||||
name: 'NotAuthorized',
|
name: 'NotAuthorized',
|
||||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cancelSubscribe ', () => {
|
describe('cancelSubscribe ', () => {
|
||||||
@@ -360,9 +592,9 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||||
.returns(true);
|
sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
|
sinon.stub(iap, 'isExpired').returns(true);
|
||||||
user = new User();
|
user = new User();
|
||||||
user.profile.name = 'sender';
|
user.profile.name = 'sender';
|
||||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||||
@@ -377,6 +609,8 @@ describe('Apple Payments', () => {
|
|||||||
iap.setup.restore();
|
iap.setup.restore();
|
||||||
iap.validate.restore();
|
iap.validate.restore();
|
||||||
iap.isValidated.restore();
|
iap.isValidated.restore();
|
||||||
|
iap.isExpired.restore();
|
||||||
|
iap.isCanceled.restore();
|
||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
payments.cancelSubscription.restore();
|
payments.cancelSubscription.restore();
|
||||||
});
|
});
|
||||||
@@ -396,6 +630,8 @@ describe('Apple Payments', () => {
|
|||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
|
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
|
||||||
|
iap.isExpired.restore();
|
||||||
|
sinon.stub(iap, 'isExpired').returns(false);
|
||||||
|
|
||||||
await expect(applePayments.cancelSubscribe(user, headers))
|
await expect(applePayments.cancelSubscribe(user, headers))
|
||||||
.to.eventually.be.rejected.and.to.eql({
|
.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);
|
await applePayments.cancelSubscribe(user, headers);
|
||||||
|
|
||||||
expect(iapSetupStub).to.be.calledOnce;
|
expect(iapSetupStub).to.be.calledOnce;
|
||||||
|
|||||||
@@ -203,6 +203,28 @@ describe('payments/index', () => {
|
|||||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
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 () => {
|
it('does not change plan.customerId if it already exists', async () => {
|
||||||
recipient.purchased.plan = plan;
|
recipient.purchased.plan = plan;
|
||||||
data.customerId = 'purchaserCustomerId';
|
data.customerId = 'purchaserCustomerId';
|
||||||
@@ -213,6 +235,65 @@ describe('payments/index', () => {
|
|||||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
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 () => {
|
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
||||||
expect(recipient.purchased.plan.customerId).to.not.exist;
|
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.customerId).to.eql('customer-id');
|
||||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||||
expect(user.purchased.plan.gemsBought).to.eql(0);
|
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.paymentMethod).to.eql('Payment Method');
|
||||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||||
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
||||||
@@ -386,6 +468,63 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.dateCreated).to.exist;
|
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 () => {
|
it('awards the Royal Purple Jackalope pet', async () => {
|
||||||
await api.createSubscription(data);
|
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', () => {
|
context('Block subscription perks', () => {
|
||||||
@@ -488,7 +710,6 @@ describe('payments/index', () => {
|
|||||||
|
|
||||||
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
||||||
data.sub.key = 'basic_6mo';
|
data.sub.key = 'basic_6mo';
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
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 () => {
|
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||||
data.sub.key = 'basic_12mo';
|
data.sub.key = 'basic_12mo';
|
||||||
|
|
||||||
await api.createSubscription(data);
|
await api.createSubscription(data);
|
||||||
|
|
||||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
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);
|
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', () => {
|
context('Mystery Items', () => {
|
||||||
|
|||||||
@@ -45,11 +45,10 @@ describe('payments : apple #subscribe', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(subscribeStub).to.be.calledOnce;
|
expect(subscribeStub).to.be.calledOnce;
|
||||||
expect(subscribeStub.args[0][0]).to.eql(sku);
|
expect(subscribeStub.args[0][0]._id).to.eql(user._id);
|
||||||
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
|
expect(subscribeStub.args[0][1]).to.eql('receipt');
|
||||||
expect(subscribeStub.args[0][2]).to.eql('receipt');
|
expect(subscribeStub.args[0][2]['x-api-key']).to.eql(user.apiToken);
|
||||||
expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
|
expect(subscribeStub.args[0][2]['x-api-user']).to.eql(user._id);
|
||||||
expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ describe('cron utility functions', () => {
|
|||||||
|
|
||||||
it('monthly plan, next date in 3 months', () => {
|
it('monthly plan, next date in 3 months', () => {
|
||||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||||
|
user.purchased.plan.perkMonthCount = 0;
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
const planContext = getPlanContext(user, now);
|
||||||
|
|
||||||
@@ -224,6 +225,7 @@ describe('cron utility functions', () => {
|
|||||||
|
|
||||||
it('monthly plan, next date in 1 month', () => {
|
it('monthly plan, next date in 1 month', () => {
|
||||||
const user = baseUserData(62, 0, 'group_plan_auto');
|
const user = baseUserData(62, 0, 'group_plan_auto');
|
||||||
|
user.purchased.plan.perkMonthCount = 2;
|
||||||
|
|
||||||
const planContext = getPlanContext(user, now);
|
const planContext = getPlanContext(user, now);
|
||||||
|
|
||||||
@@ -248,5 +250,15 @@ describe('cron utility functions', () => {
|
|||||||
expect(planContext.nextHourglassDate)
|
expect(planContext.nextHourglassDate)
|
||||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
.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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ const webhookData = {};
|
|||||||
|
|
||||||
app.use(bodyParser.urlencoded({
|
app.use(bodyParser.urlencoded({
|
||||||
extended: true,
|
extended: true,
|
||||||
|
limit: '10mb',
|
||||||
}));
|
}));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
|
||||||
app.post('/webhooks/:id', (req, res) => {
|
app.post('/webhooks/:id', (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|||||||
125
website/client/package-lock.json
generated
125
website/client/package-lock.json
generated
@@ -13318,11 +13318,34 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"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": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
"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": {
|
"semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
@@ -13354,6 +13377,38 @@
|
|||||||
"ansi-regex": "^5.0.1"
|
"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": {
|
"wrap-ansi": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"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": {
|
"vue-mugen-scroll": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-mugen-scroll/-/vue-mugen-scroll-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-mugen-scroll/-/vue-mugen-scroll-0.2.6.tgz",
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
Perk offset months:
|
Perk offset months:
|
||||||
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
|
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
Perk month count:
|
||||||
|
<strong>{{ hero.purchased.plan.perkMonthCount }}</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Next Mystic Hourglass:
|
Next Mystic Hourglass:
|
||||||
<strong>{{ nextHourglassDate }}</strong>
|
<strong>{{ nextHourglassDate }}</strong>
|
||||||
@@ -149,7 +153,7 @@ export default {
|
|||||||
nextHourglassDate () {
|
nextHourglassDate () {
|
||||||
const currentPlanContext = getPlanContext(this.hero, new Date());
|
const currentPlanContext = getPlanContext(this.hero, new Date());
|
||||||
|
|
||||||
return currentPlanContext.nextHourglassDate.format('MMMM');
|
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -804,7 +804,7 @@ export default {
|
|||||||
return currentPlanContext.nextHourglassDate;
|
return currentPlanContext.nextHourglassDate;
|
||||||
},
|
},
|
||||||
nextHourGlass () {
|
nextHourGlass () {
|
||||||
const nextHourglassMonth = this.nextHourGlassDate.format('MMM');
|
const nextHourglassMonth = this.nextHourGlassDate.format('MMM YYYY');
|
||||||
|
|
||||||
return nextHourglassMonth;
|
return nextHourglassMonth;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export function getPlanContext (user, now) {
|
|||||||
if (planMonths > 1) {
|
if (planMonths > 1) {
|
||||||
monthsTillNextHourglass = plan.consecutive.offset + 1;
|
monthsTillNextHourglass = plan.consecutive.offset + 1;
|
||||||
} else {
|
} else {
|
||||||
monthsTillNextHourglass = 3 - (plan.consecutive.count % 3);
|
monthsTillNextHourglass = 3 - plan.perkMonthCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleNextHourglassDate = moment(plan.dateUpdated)
|
const possibleNextHourglassDate = moment(plan.dateUpdated)
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ api.iapSubscriptioniOS = {
|
|||||||
if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode'));
|
if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode'));
|
||||||
if (!req.body.receipt) throw new BadRequest(res.t('missingReceipt'));
|
if (!req.body.receipt) throw new BadRequest(res.t('missingReceipt'));
|
||||||
|
|
||||||
await applePayments.subscribe(req.body.sku, res.locals.user, req.body.receipt, req.headers);
|
await applePayments.subscribe(res.locals.user, req.body.receipt, req.headers);
|
||||||
|
|
||||||
res.respond(200);
|
res.respond(200);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ const CLEAR_BUFFS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function grantEndOfTheMonthPerks (user, now) {
|
async function grantEndOfTheMonthPerks (user, now) {
|
||||||
// multi-month subscriptions are for multiples of 3 months
|
|
||||||
const SUBSCRIPTION_BASIC_BLOCK_LENGTH = 3;
|
|
||||||
|
|
||||||
const { plan, elapsedMonths } = getPlanContext(user, now);
|
const { plan, elapsedMonths } = getPlanContext(user, now);
|
||||||
|
|
||||||
if (elapsedMonths > 0) {
|
if (elapsedMonths > 0) {
|
||||||
@@ -106,32 +103,17 @@ async function grantEndOfTheMonthPerks (user, now) {
|
|||||||
planMonthsLength = getPlanMonths(plan);
|
planMonthsLength = getPlanMonths(plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// every 3 months you get one set of perks - this variable records how many sets you need
|
|
||||||
let perkAmountNeeded = 0;
|
|
||||||
if (planMonthsLength === 1) {
|
if (planMonthsLength === 1) {
|
||||||
// User has a single-month recurring subscription and are due for perks
|
|
||||||
// IF they've been subscribed for a multiple of 3 months.
|
|
||||||
if (plan.consecutive.count % SUBSCRIPTION_BASIC_BLOCK_LENGTH === 0) { // every 3 months
|
|
||||||
perkAmountNeeded = 1;
|
|
||||||
}
|
|
||||||
plan.consecutive.offset = 0; // allow the same logic to be run next month
|
plan.consecutive.offset = 0; // allow the same logic to be run next month
|
||||||
} else {
|
} else {
|
||||||
// User has a multi-month recurring subscription
|
// User has a multi-month recurring subscription
|
||||||
// and it renewed in the previous calendar month.
|
// and it renewed in the previous calendar month.
|
||||||
|
|
||||||
// e.g., for a 6-month subscription, give two sets of perks
|
|
||||||
perkAmountNeeded = planMonthsLength / SUBSCRIPTION_BASIC_BLOCK_LENGTH;
|
|
||||||
// don't need to check for perks again for this many months
|
// don't need to check for perks again for this many months
|
||||||
// (subtract 1 because we should have run this when the payment was taken last month)
|
// (subtract 1 because we should have run this when the payment was taken last month)
|
||||||
plan.consecutive.offset = planMonthsLength - 1;
|
plan.consecutive.offset = planMonthsLength - 1;
|
||||||
}
|
}
|
||||||
if (perkAmountNeeded > 0) {
|
// eslint-disable-next-line no-await-in-loop
|
||||||
// one Hourglass every 3 months
|
await plan.incrementPerkCounterAndReward(user._id, planMonthsLength);
|
||||||
await plan.updateHourglasses(user._id, perkAmountNeeded, 'subscription_perks'); // eslint-disable-line no-await-in-loop
|
|
||||||
plan.consecutive.gemCapExtra += 5 * perkAmountNeeded; // 5 extra Gems every 3 months
|
|
||||||
// cap it at 50 (hard 25 limit + extra 25)
|
|
||||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,6 +279,8 @@ export async function cron (options = {}) {
|
|||||||
|
|
||||||
if (user.isSubscribed()) {
|
if (user.isSubscribed()) {
|
||||||
await grantEndOfTheMonthPerks(user, now);
|
await grantEndOfTheMonthPerks(user, now);
|
||||||
|
} if (!user.isSubscribed() && user.purchased.plan.perkMonthCount > 0) {
|
||||||
|
user.purchased.plan.perkMonthCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { plan } = user.purchased;
|
const { plan } = user.purchased;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export default {
|
|||||||
setup: util.promisify(iap.setup.bind(iap)),
|
setup: util.promisify(iap.setup.bind(iap)),
|
||||||
validate: util.promisify(iap.validate.bind(iap)),
|
validate: util.promisify(iap.validate.bind(iap)),
|
||||||
isValidated: iap.isValidated,
|
isValidated: iap.isValidated,
|
||||||
|
isCanceled: iap.isCanceled,
|
||||||
|
isExpired: iap.isExpired,
|
||||||
getPurchaseData: iap.getPurchaseData,
|
getPurchaseData: iap.getPurchaseData,
|
||||||
GOOGLE: iap.GOOGLE,
|
GOOGLE: iap.GOOGLE,
|
||||||
APPLE: iap.APPLE,
|
APPLE: iap.APPLE,
|
||||||
|
|||||||
@@ -74,15 +74,32 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
|||||||
return appleRes;
|
return appleRes;
|
||||||
};
|
};
|
||||||
|
|
||||||
api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing) {
|
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||||
if (user && user.isSubscribed()) {
|
await iap.setup();
|
||||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
|
||||||
|
const appleRes = await iap.validate(iap.APPLE, receipt);
|
||||||
|
const isValidated = iap.isValidated(appleRes);
|
||||||
|
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||||
|
|
||||||
|
const purchaseDataList = iap.getPurchaseData(appleRes);
|
||||||
|
if (purchaseDataList.length === 0) {
|
||||||
|
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
let purchase;
|
||||||
|
let newestDate;
|
||||||
|
|
||||||
|
for (const purchaseData of purchaseDataList) {
|
||||||
|
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||||
|
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||||
|
if ((!newestDate || datePurchased > newestDate) && dateTerminated > new Date()) {
|
||||||
|
purchase = purchaseData;
|
||||||
|
newestDate = datePurchased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let subCode;
|
let subCode;
|
||||||
switch (sku) { // eslint-disable-line default-case
|
switch (purchase.productId) { // eslint-disable-line default-case
|
||||||
case 'subscription1month':
|
case 'subscription1month':
|
||||||
subCode = 'basic_earned';
|
subCode = 'basic_earned';
|
||||||
break;
|
break;
|
||||||
@@ -97,45 +114,56 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false;
|
const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false;
|
||||||
if (!sub) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
|
|
||||||
await iap.setup();
|
|
||||||
|
|
||||||
const appleRes = await iap.validate(iap.APPLE, receipt);
|
if (purchase.originalTransactionId) {
|
||||||
const isValidated = iap.isValidated(appleRes);
|
let existingSub;
|
||||||
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
if (user && user.isSubscribed()) {
|
||||||
|
if (user.purchased.plan.customerId !== purchase.originalTransactionId) {
|
||||||
const purchaseDataList = iap.getPurchaseData(appleRes);
|
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||||
if (purchaseDataList.length === 0) {
|
|
||||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
|
||||||
}
|
}
|
||||||
|
existingSub = shared.content.subscriptionBlocks[user.purchased.plan.planId];
|
||||||
let transactionId;
|
if (existingSub === sub) {
|
||||||
|
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||||
for (const purchaseData of purchaseDataList) {
|
|
||||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
|
||||||
if (purchaseData.productId === sku && dateTerminated > new Date()) {
|
|
||||||
transactionId = purchaseData.transactionId;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const existingUsers = await User.find({
|
||||||
if (transactionId) {
|
$or: [
|
||||||
const existingUser = await User.findOne({
|
{ 'purchased.plan.customerId': purchase.originalTransactionId },
|
||||||
'purchased.plan.customerId': transactionId,
|
{ 'purchased.plan.customerId': purchase.transactionId },
|
||||||
|
],
|
||||||
}).exec();
|
}).exec();
|
||||||
if (existingUser) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
if (existingUsers.length > 0) {
|
||||||
|
if (purchase.originalTransactionId === purchase.transactionId) {
|
||||||
|
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||||
|
}
|
||||||
|
for (const existingUser of existingUsers) {
|
||||||
|
if (existingUser._id !== user._id && !existingUser.purchased.plan.dateTerminated) {
|
||||||
|
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line max-len, no-param-reassign
|
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line max-len, no-param-reassign
|
||||||
|
const terminationDate = moment(Number(purchase.expirationDate));
|
||||||
|
if (nextPaymentProcessing > terminationDate) {
|
||||||
|
// For test subscriptions that have a significantly shorter expiration period, this is better
|
||||||
|
nextPaymentProcessing = terminationDate; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
|
||||||
await payments.createSubscription({
|
const data = {
|
||||||
user,
|
user,
|
||||||
customerId: transactionId,
|
customerId: purchase.originalTransactionId,
|
||||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||||
sub,
|
sub,
|
||||||
headers,
|
headers,
|
||||||
nextPaymentProcessing,
|
nextPaymentProcessing,
|
||||||
additionalData: receipt,
|
additionalData: receipt,
|
||||||
});
|
};
|
||||||
|
if (existingSub) {
|
||||||
|
data.updatedFrom = existingSub;
|
||||||
|
data.updatedFrom.logic = 'refundAndRepay';
|
||||||
|
}
|
||||||
|
await payments.createSubscription(data);
|
||||||
} else {
|
} else {
|
||||||
throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||||
}
|
}
|
||||||
@@ -227,8 +255,6 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
|||||||
|
|
||||||
await iap.setup();
|
await iap.setup();
|
||||||
|
|
||||||
let dateTerminated;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
||||||
|
|
||||||
@@ -237,10 +263,27 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
|||||||
|
|
||||||
const purchases = iap.getPurchaseData(appleRes);
|
const purchases = iap.getPurchaseData(appleRes);
|
||||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||||
const subscriptionData = purchases[0];
|
let newestDate;
|
||||||
|
let newestPurchase;
|
||||||
|
|
||||||
dateTerminated = new Date(Number(subscriptionData.expirationDate));
|
for (const purchaseData of purchases) {
|
||||||
if (dateTerminated > new Date()) throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||||
|
if (!newestDate || datePurchased > newestDate) {
|
||||||
|
newestDate = datePurchased;
|
||||||
|
newestPurchase = purchaseData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!iap.isCanceled(newestPurchase) && !iap.isExpired(newestPurchase)) {
|
||||||
|
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
await payments.cancelSubscription({
|
||||||
|
user,
|
||||||
|
nextBill: new Date(Number(newestPurchase.expirationDate)),
|
||||||
|
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If we have an invalid receipt, cancel anyway
|
// If we have an invalid receipt, cancel anyway
|
||||||
if (
|
if (
|
||||||
@@ -250,13 +293,6 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await payments.cancelSubscription({
|
|
||||||
user,
|
|
||||||
nextBill: dateTerminated,
|
|
||||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -74,7 +74,39 @@ async function prepareSubscriptionValues (data) {
|
|||||||
? data.gift.subscription.key
|
? data.gift.subscription.key
|
||||||
: data.sub.key];
|
: data.sub.key];
|
||||||
const autoRenews = data.autoRenews !== undefined ? data.autoRenews : true;
|
const autoRenews = data.autoRenews !== undefined ? data.autoRenews : true;
|
||||||
const months = Number(block.months);
|
const updatedFrom = data.updatedFrom
|
||||||
|
? shared.content.subscriptionBlocks[data.updatedFrom.key]
|
||||||
|
: undefined;
|
||||||
|
let months;
|
||||||
|
if (updatedFrom && Number(updatedFrom.months) !== 1) {
|
||||||
|
if (Number(updatedFrom.months) > Number(block.months)) {
|
||||||
|
months = 0;
|
||||||
|
} else if (data.updatedFrom.logic === 'payDifference') {
|
||||||
|
months = Math.max(0, Number(block.months) - Number(updatedFrom.months));
|
||||||
|
} else if (data.updatedFrom.logic === 'payFull') {
|
||||||
|
months = Number(block.months);
|
||||||
|
} else if (data.updatedFrom.logic === 'refundAndRepay') {
|
||||||
|
const originalMonths = Number(updatedFrom.months);
|
||||||
|
let currentCycleBegin = moment(recipient.purchased.plan.dateCurrentTypeCreated);
|
||||||
|
const today = moment();
|
||||||
|
while (currentCycleBegin.isBefore()) {
|
||||||
|
currentCycleBegin = currentCycleBegin.add({ months: originalMonths });
|
||||||
|
}
|
||||||
|
// Subtract last iteration again, because we overshot
|
||||||
|
currentCycleBegin = currentCycleBegin.subtract({ months: originalMonths });
|
||||||
|
// For simplicity we round every month to 30 days since moment can not add half months
|
||||||
|
if (currentCycleBegin.add({ days: (originalMonths * 30) / 2.0 }).isBefore(today)) {
|
||||||
|
// user is in second half of their subscription cycle. Give them full benefits.
|
||||||
|
months = Number(block.months);
|
||||||
|
} else {
|
||||||
|
// user is in first half of their subscription cycle. Give them the difference.
|
||||||
|
months = Math.max(0, Number(block.months) - Number(updatedFrom.months));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (months === undefined) {
|
||||||
|
months = Number(block.months);
|
||||||
|
}
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
let group;
|
let group;
|
||||||
let groupId;
|
let groupId;
|
||||||
@@ -82,6 +114,7 @@ async function prepareSubscriptionValues (data) {
|
|||||||
let purchaseType = 'subscribe';
|
let purchaseType = 'subscribe';
|
||||||
let emailType = 'subscription-begins';
|
let emailType = 'subscription-begins';
|
||||||
let recipientIsSubscribed = recipient.isSubscribed();
|
let recipientIsSubscribed = recipient.isSubscribed();
|
||||||
|
const isNewSubscription = !recipientIsSubscribed;
|
||||||
|
|
||||||
// If we are buying a group subscription
|
// If we are buying a group subscription
|
||||||
if (data.groupId) {
|
if (data.groupId) {
|
||||||
@@ -122,6 +155,10 @@ async function prepareSubscriptionValues (data) {
|
|||||||
|
|
||||||
const { plan } = recipient.purchased;
|
const { plan } = recipient.purchased;
|
||||||
|
|
||||||
|
if (isNewSubscription) {
|
||||||
|
plan.perkMonthCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.gift || !autoRenews) {
|
if (data.gift || !autoRenews) {
|
||||||
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
||||||
plan.extraMonths += months;
|
plan.extraMonths += months;
|
||||||
@@ -136,6 +173,7 @@ async function prepareSubscriptionValues (data) {
|
|||||||
plan.dateTerminated = moment().add({ months }).toDate();
|
plan.dateTerminated = moment().add({ months }).toDate();
|
||||||
plan.dateCreated = today;
|
plan.dateCreated = today;
|
||||||
}
|
}
|
||||||
|
plan.dateCurrentTypeCreated = today;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!plan.customerId) {
|
if (!plan.customerId) {
|
||||||
@@ -152,6 +190,7 @@ async function prepareSubscriptionValues (data) {
|
|||||||
planId: block.key,
|
planId: block.key,
|
||||||
customerId: data.customerId,
|
customerId: data.customerId,
|
||||||
dateUpdated: today,
|
dateUpdated: today,
|
||||||
|
dateCurrentTypeCreated: today,
|
||||||
paymentMethod: data.paymentMethod,
|
paymentMethod: data.paymentMethod,
|
||||||
extraMonths: Number(plan.extraMonths) + _dateDiff(today, plan.dateTerminated),
|
extraMonths: Number(plan.extraMonths) + _dateDiff(today, plan.dateTerminated),
|
||||||
dateTerminated: null,
|
dateTerminated: null,
|
||||||
@@ -194,6 +233,7 @@ async function prepareSubscriptionValues (data) {
|
|||||||
itemPurchased,
|
itemPurchased,
|
||||||
purchaseType,
|
purchaseType,
|
||||||
emailType,
|
emailType,
|
||||||
|
isNewSubscription,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,15 +249,17 @@ async function createSubscription (data) {
|
|||||||
itemPurchased,
|
itemPurchased,
|
||||||
purchaseType,
|
purchaseType,
|
||||||
emailType,
|
emailType,
|
||||||
|
isNewSubscription,
|
||||||
} = await prepareSubscriptionValues(data);
|
} = await prepareSubscriptionValues(data);
|
||||||
|
|
||||||
// Block sub perks
|
// Block sub perks
|
||||||
const perks = Math.floor(months / 3);
|
if (months > 0 && (!data.gift || !isNewSubscription)) {
|
||||||
if (perks) {
|
if (!data.gift && !groupId) {
|
||||||
plan.consecutive.offset += months;
|
plan.consecutive.offset = block.months;
|
||||||
plan.consecutive.gemCapExtra += perks * 5;
|
}
|
||||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
|
if (months > 1 || data.gift) {
|
||||||
await plan.updateHourglasses(recipient._id, perks, 'subscription_perks'); // one Hourglass every 3 months
|
await plan.incrementPerkCounterAndReward(recipient._id, months);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient !== group) {
|
if (recipient !== group) {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export default function attachMiddlewares (app, server) {
|
|||||||
|
|
||||||
app.use(bodyParser.urlencoded({
|
app.use(bodyParser.urlencoded({
|
||||||
extended: true, // Uses 'qs' library as old connect middleware
|
extended: true, // Uses 'qs' library as old connect middleware
|
||||||
|
limit: '10mb',
|
||||||
}));
|
}));
|
||||||
app.use(function bodyMiddleware (req, res, next) { // eslint-disable-line prefer-arrow-callback
|
app.use(function bodyMiddleware (req, res, next) { // eslint-disable-line prefer-arrow-callback
|
||||||
if (req.path === '/stripe/webhooks') {
|
if (req.path === '/stripe/webhooks') {
|
||||||
@@ -79,7 +80,7 @@ export default function attachMiddlewares (app, server) {
|
|||||||
// See https://stripe.com/docs/webhooks/signatures#verify-official-libraries
|
// See https://stripe.com/docs/webhooks/signatures#verify-official-libraries
|
||||||
bodyParser.raw({ type: 'application/json' })(req, res, next);
|
bodyParser.raw({ type: 'application/json' })(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
bodyParser.json()(req, res, next);
|
bodyParser.json({ limit: '10mb' })(req, res, next);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import validator from 'validator';
|
|||||||
import baseModel from '../libs/baseModel';
|
import baseModel from '../libs/baseModel';
|
||||||
import { TransactionModel as Transaction } from './transaction';
|
import { TransactionModel as Transaction } from './transaction';
|
||||||
|
|
||||||
|
// multi-month subscriptions are for multiples of 3 months
|
||||||
|
const SUBSCRIPTION_BASIC_BLOCK_LENGTH = 3;
|
||||||
|
|
||||||
export const schema = new mongoose.Schema({
|
export const schema = new mongoose.Schema({
|
||||||
planId: String,
|
planId: String,
|
||||||
subscriptionId: String,
|
subscriptionId: String,
|
||||||
@@ -13,7 +16,9 @@ export const schema = new mongoose.Schema({
|
|||||||
dateCreated: Date,
|
dateCreated: Date,
|
||||||
dateTerminated: Date,
|
dateTerminated: Date,
|
||||||
dateUpdated: Date,
|
dateUpdated: Date,
|
||||||
|
dateCurrentTypeCreated: Date,
|
||||||
extraMonths: { $type: Number, default: 0 },
|
extraMonths: { $type: Number, default: 0 },
|
||||||
|
perkMonthCount: { $type: Number, default: -1 },
|
||||||
gemsBought: { $type: Number, default: 0 },
|
gemsBought: { $type: Number, default: 0 },
|
||||||
mysteryItems: { $type: Array, default: () => [] },
|
mysteryItems: { $type: Array, default: () => [] },
|
||||||
lastReminderDate: Date, // indicates the last time a subscription reminder was sent
|
lastReminderDate: Date, // indicates the last time a subscription reminder was sent
|
||||||
@@ -45,6 +50,33 @@ schema.plugin(baseModel, {
|
|||||||
_id: false,
|
_id: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
schema.methods.incrementPerkCounterAndReward = async function incrementPerkCounterAndReward
|
||||||
|
(userID, adding) {
|
||||||
|
let addingNumber = adding;
|
||||||
|
if (typeof adding === 'string' || adding instanceof String) {
|
||||||
|
addingNumber = parseInt(adding, 10);
|
||||||
|
}
|
||||||
|
// if perkMonthCount wasn't used before, initialize it.
|
||||||
|
if (this.perkMonthCount === undefined || this.perkMonthCount === -1) {
|
||||||
|
if (this.planId === 'basic_earned') {
|
||||||
|
this.perkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH;
|
||||||
|
} else {
|
||||||
|
this.perkMonthCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.perkMonthCount += addingNumber;
|
||||||
|
|
||||||
|
const perks = Math.floor(this.perkMonthCount / 3);
|
||||||
|
if (perks > 0) {
|
||||||
|
this.consecutive.gemCapExtra += 5 * perks; // 5 extra Gems every 3 months
|
||||||
|
// cap it at 50 (hard 25 limit + extra 25)
|
||||||
|
if (this.consecutive.gemCapExtra > 25) this.consecutive.gemCapExtra = 25;
|
||||||
|
this.perkMonthCount -= (perks * 3);
|
||||||
|
// one Hourglass every 3 months
|
||||||
|
await this.updateHourglasses(userID, perks, 'subscription_perks'); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
schema.methods.updateHourglasses = async function updateHourglasses (userId,
|
schema.methods.updateHourglasses = async function updateHourglasses (userId,
|
||||||
amount,
|
amount,
|
||||||
transactionType,
|
transactionType,
|
||||||
|
|||||||
Reference in New Issue
Block a user