mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 21:57:22 +01:00
Handle subscription cancelation better
This commit is contained in:
committed by
Phillip Thelen
parent
de48925341
commit
87558a325e
@@ -9,7 +9,7 @@ import * as gems from '../../../../../website/server/libs/payments/gems';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Apple Payments', () => {
|
||||
describe.only('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
@@ -29,8 +29,9 @@ describe('Apple Payments', () => {
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -44,6 +45,8 @@ describe('Apple Payments', () => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
@@ -449,6 +452,81 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows second user to subscribe if initial subscription is cancelled', async () => {
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = token;
|
||||
user.purchased.plan.planId = common.content.subscriptionBlocks.basic_3mo.key;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
user.purchased.plan.dateTerminated = moment.utc().subtract({ day: 1 }).toDate();
|
||||
await user.save()
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 3 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: token + "new",
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
const secondUser = new User();
|
||||
await secondUser.save();
|
||||
await applePayments.subscribe(secondUser, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user: secondUser,
|
||||
customerId: token,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('allows second user to subscribe if multiple initial subscription are cancelled', async () => {
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = token;
|
||||
user.purchased.plan.planId = common.content.subscriptionBlocks.basic_3mo.key;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
user.purchased.plan.dateTerminated = moment.utc().subtract({ day: 1 }).toDate();
|
||||
await user.save();
|
||||
|
||||
const secondUser = new User();
|
||||
secondUser.purchased.plan = user.purchased.plan;
|
||||
await secondUser.save()
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 3 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: token + "new",
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
const thirdUser = new User();
|
||||
await thirdUser.save();
|
||||
await applePayments.subscribe(thirdUser, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user: thirdUser,
|
||||
customerId: token,
|
||||
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();
|
||||
@@ -513,6 +591,7 @@ describe('Apple Payments', () => {
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
const secondUser = new User();
|
||||
await secondUser.save();
|
||||
await expect(applePayments.subscribe(
|
||||
secondUser, receipt, headers, nextPaymentProcessing,
|
||||
))
|
||||
@@ -522,6 +601,49 @@ describe('Apple Payments', () => {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -546,9 +668,9 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -563,6 +685,8 @@ describe('Apple Payments', () => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -582,6 +706,8 @@ describe('Apple Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
|
||||
iap.isExpired.restore();
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
@@ -604,7 +730,38 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
it('should cancel a cancelled subscription with termination date in the future', async () => {
|
||||
const futureDate = expirationDate.add({ day: 1 });
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: futureDate }]);
|
||||
iap.isExpired.restore();
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
|
||||
iap.isCanceled.restore();
|
||||
sinon.stub(iap, 'isCanceled').returns(true);
|
||||
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate: futureDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
nextBill: futureDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel an expired subscription', async () => {
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
|
||||
@@ -21,6 +21,8 @@ export default {
|
||||
setup: util.promisify(iap.setup.bind(iap)),
|
||||
validate: util.promisify(iap.validate.bind(iap)),
|
||||
isValidated: iap.isValidated,
|
||||
isCanceled: iap.isCanceled,
|
||||
isExpired: iap.isExpired,
|
||||
getPurchaseData: iap.getPurchaseData,
|
||||
GOOGLE: iap.GOOGLE,
|
||||
APPLE: iap.APPLE,
|
||||
|
||||
@@ -126,14 +126,20 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
}
|
||||
const existingUser = await User.findOne({
|
||||
const existingUsers = await User.find({
|
||||
'purchased.plan.customerId': purchase.originalTransactionId,
|
||||
}).exec();
|
||||
if (existingUser
|
||||
&& (purchase.originalTransactionId === purchase.transactionId
|
||||
|| existingUser._id !== user._id)) {
|
||||
if (existingUsers.length > 0) {
|
||||
if (purchase.originalTransactionId === purchase.transactionId) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
for (const index in existingUsers) {
|
||||
const existingUser = existingUsers[index];
|
||||
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
|
||||
const terminationDate = moment(Number(purchase.expirationDate));
|
||||
@@ -247,8 +253,6 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
|
||||
await iap.setup();
|
||||
|
||||
let dateTerminated;
|
||||
|
||||
try {
|
||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
||||
|
||||
@@ -258,16 +262,24 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
const purchases = iap.getPurchaseData(appleRes);
|
||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
||||
let newestDate;
|
||||
let newestPurchase
|
||||
|
||||
for (const purchaseData of purchases) {
|
||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||
if (!newestDate || datePurchased > newestDate) {
|
||||
dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
newestDate = datePurchased;
|
||||
newestPurchase = purchaseData;
|
||||
}
|
||||
}
|
||||
|
||||
if (dateTerminated > new Date()) throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
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) {
|
||||
// If we have an invalid receipt, cancel anyway
|
||||
if (
|
||||
@@ -278,12 +290,6 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
}
|
||||
}
|
||||
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
nextBill: dateTerminated,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
Reference in New Issue
Block a user