Compare commits

...

7 Commits

Author SHA1 Message Date
Phillip Thelen 835706cd81 fix argument order 2026-06-09 14:02:19 +02:00
Phillip Thelen 2baca238b2 check that purchase token is also the same 2026-06-09 13:57:40 +02:00
Phillip Thelen 3915b04aed fix test 2026-06-09 12:38:39 +02:00
Phillip Thelen 763adbdf8b fix 2026-06-08 15:03:55 +02:00
Phillip Thelen 2f74e0e8e7 handle next billing date when deferred sub 2026-06-08 14:47:53 +02:00
Phillip Thelen 729c8bc72b handle deferred sub changes on android 2026-06-08 12:11:17 +02:00
Phillip Thelen c0d812ad8d handle subscription upgrades or downgrades for android 2026-05-28 15:46:35 +02:00
5 changed files with 51 additions and 14 deletions
+2 -1
View File
@@ -236,8 +236,9 @@ describe('Google Payments', () => {
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
sub,
headers,
additionalData: { data: receipt, signature },
nextPaymentProcessing,
nextBillingDate: undefined,
additionalData: { data: receipt, signature },
});
});
});
@@ -52,6 +52,7 @@ api.iapSubscriptionAndroid = {
req.body.transaction.receipt,
req.body.transaction.signature,
req.headers,
req.body.deferredSku,
);
res.respond(200);
+43 -13
View File
@@ -119,15 +119,7 @@ api.getSubscriptionPaymentDetails = async function getDetails (userId, subscript
};
};
api.subscribe = async function subscribe (
sku,
user,
receipt,
signature,
headers,
nextPaymentProcessing = undefined,
) {
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
function getSubCodeFromSku (sku) {
let subCode;
switch (sku) { // eslint-disable-line default-case
case 'com.habitrpg.android.habitica.subscription.1month':
@@ -143,6 +135,19 @@ api.subscribe = async function subscribe (
subCode = 'basic_12mo';
break;
}
return subCode;
}
api.subscribe = async function subscribe (
sku,
user,
receipt,
signature,
headers,
deferredSku = undefined,
) {
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
const subCode = getSubCodeFromSku(sku);
const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false;
if (!sub) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
@@ -156,6 +161,14 @@ api.subscribe = async function subscribe (
const receiptObj = typeof receipt === 'string' ? JSON.parse(receipt) : receipt; // passed as a string
const token = receiptObj.token || receiptObj.purchaseToken;
let existingSub;
if (user && user.isSubscribed()) {
existingSub = shared.content.subscriptionBlocks[user.purchased.plan.planId];
if (existingSub === sub && user.purchased.plan.customerId === token) {
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
}
}
const existingUser = await User.findOne({
'purchased.plan.customerId': token,
}).exec();
@@ -166,17 +179,34 @@ api.subscribe = async function subscribe (
const isValidated = iap.isValidated(googleRes);
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line no-param-reassign, max-len
await payments.createSubscription({
let nextPaymentProcessing = moment.utc().add({ days: 2 }); // eslint-disable-line no-param-reassign, max-len
let nextBillingDate;
if (googleRes.expirationDate) {
nextBillingDate = new Date(Number(googleRes.expirationDate));
if (nextBillingDate < nextPaymentProcessing.toDate()) {
nextPaymentProcessing = moment(nextBillingDate);
}
}
const data = {
user,
customerId: token,
paymentMethod: this.constants.PAYMENT_METHOD_GOOGLE,
sub,
headers,
nextPaymentProcessing,
nextBillingDate,
additionalData: testObj,
});
};
if (existingSub) {
if (deferredSku) {
const deferredSubCode = getSubCodeFromSku(deferredSku);
data.updatedTo = shared.content.subscriptionBlocks[deferredSubCode];
} else {
data.updatedFrom = existingSub;
data.updatedFrom.logic = 'payFull';
}
}
await payments.createSubscription(data);
};
api.noRenewSubscribe = async function noRenewSubscribe (options) {
@@ -214,6 +214,10 @@ async function prepareSubscriptionValues (data) {
if (data.subscriptionId) {
plan.subscriptionId = data.subscriptionId;
}
if (data.updatingTo && data.updatingTo.key) {
// Subscription change is deferred until next payment processing
plan.deferredPlanId = data.updatingTo.key;
}
}
return {
@@ -35,6 +35,7 @@ export const schema = new mongoose.Schema({
trinkets: { $type: Number, default: 0 },
lastHourglassReceived: Date,
},
deferredPlanId: String,
}, {
strict: true,
minimize: false, // So empty objects are returned