Skip to content
Open
150 changes: 54 additions & 96 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"habitica-markdown": "^4.1.0",
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
"in-app-purchase": "github:HabitRPG/in-app-purchase",
"ioredis": "^5.10.1",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2",
Expand Down
2 changes: 2 additions & 0 deletions website/server/libs/localAnalytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function trackSubscriptionEvent (eventData) {
customerId,
planId,
cancellationReason,
isGroupSubscription,
} = eventData;

const subscriptionEvent = new SubscriptionEventModel({
Expand All @@ -45,6 +46,7 @@ export async function trackSubscriptionEvent (eventData) {
customerId,
planId,
cancellationReason,
isGroupSubscription,
});
return subscriptionEvent.save();
}
3 changes: 3 additions & 0 deletions website/server/libs/payments/payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { // eslint-disable-line import/no-cycle
import { // eslint-disable-line import/no-cycle
createSubscription,
cancelSubscription,
handleSubscriptionRenewal,
} from './subscriptions';
import { // eslint-disable-line import/no-cycle
buyGems,
Expand All @@ -32,6 +33,8 @@ api.createSubscription = createSubscription;

api.cancelSubscription = cancelSubscription;

api.handleSubscriptionRenewal = handleSubscriptionRenewal;

api.buyGems = buyGems;

api.buySkuItem = buySkuItem;
Expand Down
62 changes: 38 additions & 24 deletions website/server/libs/payments/paypal.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

const BASE_URL = nconf.get('BASE_URL');
const PAYPAL_MODE = nconf.get('PAYPAL_MODE');
const PAYPAL_DISABLE_IPN_VERIFICATION = nconf.get('PAYPAL_DISABLE_IPN_VERIFICATION') === 'true';
const { i18n } = shared;

// This is the plan.id for paypal subscriptions.
Expand Down Expand Up @@ -248,7 +249,7 @@
const customer = await this.paypalBillingAgreementGet(customerId);
if (!customer) throw new NotFound(i18n.t('subscriptionNotFound'));

console.log('PayPal subscription details:', customer);

Check warning on line 252 in website/server/libs/payments/paypal.js

View workflow job for this annotation

GitHub Actions / lint (21.x)

Unexpected console statement

Check warning on line 252 in website/server/libs/payments/paypal.js

View workflow job for this annotation

GitHub Actions / lint (21.x)

Unexpected console statement
return {
customerId: customer.id,
originalPurchaseDate: customer.start_date,
Expand Down Expand Up @@ -320,42 +321,25 @@
});
};

api.ipn = async function ipnApi (options = {}) {
await this.ipnVerifyAsync(options, {
allow_sandbox: PAYPAL_MODE === 'sandbox',
});

const { txn_type, recurring_payment_id } = options;

const ipnAcceptableTypes = [
'recurring_payment_profile_cancel',
'recurring_payment_failed',
'recurring_payment_expired',
'recurring_payment_skipped',
'subscr_cancel',
'subscr_failed',
];

if (ipnAcceptableTypes.indexOf(txn_type) === -1) return;

async function cancelSubscription (transactionType, paymentId) {
// @TODO: Should this request billing date?
const user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec();
const user = await User.findOne({ 'purchased.plan.customerId': paymentId }).exec();
if (user) {
// If the user has already cancelled the subscription, return
// Otherwise the subscription would be cancelled twice
// resulting in the loss of subscription credits
if (user.hasCancelled()) return;

if (txn_type === 'recurring_payment_skipped') {
await this.paypalBillingAgreementCancel(recurring_payment_id, { note: 'Missed payment' });
if (transactionType === 'recurring_payment_skipped') {
await api.paypalBillingAgreementCancel(paymentId, { note: 'Missed payment' });
}
await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD });
await payments.cancelSubscription({ user, paymentMethod: api.constants.PAYMENT_METHOD });
return;
}

const groupFields = basicGroupFields.concat(' purchased');
const group = await Group
.findOne({ 'purchased.plan.customerId': recurring_payment_id })
.findOne({ 'purchased.plan.customerId': paymentId })
.select(groupFields)
.exec();

Expand All @@ -367,9 +351,39 @@

await payments.cancelSubscription({
groupId: group._id,
paymentMethod: this.constants.PAYMENT_METHOD,
paymentMethod: api.constants.PAYMENT_METHOD,
});
}
}

const repayTypes = [
'recurring_payment',
'subscr_payment',
];

const cancelTypes = [
'recurring_payment_profile_cancel',
'recurring_payment_failed',
'recurring_payment_expired',
'recurring_payment_skipped',
'subscr_cancel',
'subscr_failed',
];

api.ipn = async function ipnApi (options = {}) {
if (!PAYPAL_DISABLE_IPN_VERIFICATION) {
await this.ipnVerifyAsync(options, {
allow_sandbox: PAYPAL_MODE === 'sandbox',
});
}
Comment on lines +374 to +378

const { txn_type, recurring_payment_id } = options;

if (repayTypes.indexOf(txn_type) !== -1) {
payments.handleSubscriptionRenewal(recurring_payment_id);
} else if (cancelTypes.indexOf(txn_type) !== -1) {
await cancelSubscription(txn_type, recurring_payment_id);
}
};

export default api;
13 changes: 11 additions & 2 deletions website/server/libs/payments/stripe/webhooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,21 @@ export async function handleWebhooks (options, stripeInc) {
case 'invoice.updated':
case 'invoice.finalized':
case 'invoice.paid':
case 'invoice.upcoming':
case 'invoice.payment_succeeded': {
case 'invoice.upcoming': {
// Events sent even if not active in the Stripe dashboard when a payment is being made
// This is to avoid error logs from the webhook handler not being implemented
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object;
/* eslint-disable camelcase */
const { customer: customerId, billing_reason } = invoice;
if (billing_reason === 'subscription_cycle') {
await payments.handleSubscriptionRenewal(customerId);
}
/* eslint-enable camelcase */
break;
}
case 'checkout.session.completed': {
const session = event.data.object;
const { metadata } = session;
Expand Down
Loading
Loading