mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Added subscription update middleware (#107)
refs #https://github.com/TryGhost/Ghost/pull/11434 - Added method to allow updating single subscription. Only `cancel_at_period_end` field can be updated. - Middleware is needed to allow Ghost Core to cancel/uncancel member's subscription. - Relies on the request containing identity information to be able to verify if subscription belongs to the user - When member could not be identified by the identity information present in the request we should throw instead of continuing processing - Handling and messaging inspired by https://github.com/TryGhost/Ghost/blob/3.1.1/core/server/services/mega/mega.js#L132 - When the user initiates subscription cancellation we can safely mark the subscription as canceled so that it's not shown in the interface on subsequent request. Otherwise, we end up in a situation where we still return the subscription in the period until Stripe triggers the webhook. - Added boolean coercion for cancel_at_period_end parameter. If anything but boolean is passed to Stripe API it throws an error. Coercing the value on our side is a gives a better dev experience
This commit is contained in:
parent
94ef530b3c
commit
ff5fceafc8
3 changed files with 69 additions and 2 deletions
|
@ -114,7 +114,8 @@ module.exports = function MembersApi({
|
||||||
const middleware = {
|
const middleware = {
|
||||||
sendMagicLink: Router(),
|
sendMagicLink: Router(),
|
||||||
createCheckoutSession: Router(),
|
createCheckoutSession: Router(),
|
||||||
handleStripeWebhook: Router()
|
handleStripeWebhook: Router(),
|
||||||
|
updateSubscription: Router({mergeParams: true})
|
||||||
};
|
};
|
||||||
|
|
||||||
middleware.sendMagicLink.use(body.json(), async function (req, res) {
|
middleware.sendMagicLink.use(body.json(), async function (req, res) {
|
||||||
|
@ -231,6 +232,57 @@ module.exports = function MembersApi({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
middleware.updateSubscription.use(ensureStripe, body.json(), async function (req, res) {
|
||||||
|
const identity = req.body.identity;
|
||||||
|
const cancelAtPeriodEnd = req.body.cancel_at_period_end;
|
||||||
|
const subscriptionId = req.params.id;
|
||||||
|
|
||||||
|
let member;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!identity) {
|
||||||
|
throw new common.errors.BadRequestError({
|
||||||
|
message: 'Cancel membership failed! Could not find member'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const claims = await decodeToken(identity);
|
||||||
|
const email = claims.sub;
|
||||||
|
member = email ? await users.get({email}) : null;
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new common.errors.BadRequestError({
|
||||||
|
message: 'Cancel membership failed! Could not find member'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(401);
|
||||||
|
return res.end('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow removing subscriptions that don't belong to the member
|
||||||
|
const subscription = member.stripe.subscriptions.find(sub => sub.id === subscriptionId);
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
res.writeHead(403);
|
||||||
|
return res.end('No permission');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelAtPeriodEnd === undefined) {
|
||||||
|
throw new common.errors.BadRequestError({
|
||||||
|
message: 'Canceling membership failed!',
|
||||||
|
help: 'Request should contain boolean "cancel" field.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.cancel_at_period_end = !!(cancelAtPeriodEnd);
|
||||||
|
|
||||||
|
await stripe.updateSubscriptionFromClient(subscription);
|
||||||
|
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
const getPublicConfig = function () {
|
const getPublicConfig = function () {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
publicKey,
|
publicKey,
|
||||||
|
|
|
@ -14,5 +14,9 @@ module.exports = {
|
||||||
Object.assign(loggerInterface, Object.create(newLogger));
|
Object.assign(loggerInterface, Object.create(newLogger));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get errors() {
|
||||||
|
return require('ghost-ignition').errors;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const debug = require('ghost-ignition').debug('stripe');
|
const debug = require('ghost-ignition').debug('stripe');
|
||||||
const {retrieve, list, create, del} = require('./api/stripeRequests');
|
const {retrieve, list, create, update, del} = require('./api/stripeRequests');
|
||||||
const api = require('./api');
|
const api = require('./api');
|
||||||
|
|
||||||
const STRIPE_API_VERSION = '2019-09-09';
|
const STRIPE_API_VERSION = '2019-09-09';
|
||||||
|
@ -135,6 +135,15 @@ module.exports = class StripePaymentProcessor {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSubscriptionFromClient(subscription) {
|
||||||
|
const updatedSubscription = await update(this._stripe, 'subscriptions', subscription.id, {
|
||||||
|
cancel_at_period_end: subscription.cancel_at_period_end
|
||||||
|
});
|
||||||
|
await this._updateSubscription(updatedSubscription);
|
||||||
|
|
||||||
|
return updatedSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
async getSubscriptions(member) {
|
async getSubscriptions(member) {
|
||||||
const metadata = await this.storage.get(member);
|
const metadata = await this.storage.get(member);
|
||||||
|
|
||||||
|
@ -162,6 +171,7 @@ module.exports = class StripePaymentProcessor {
|
||||||
status: subscription.status,
|
status: subscription.status,
|
||||||
start_date: subscription.start_date,
|
start_date: subscription.start_date,
|
||||||
default_payment_card_last4: subscription.default_payment_card_last4,
|
default_payment_card_last4: subscription.default_payment_card_last4,
|
||||||
|
cancel_at_period_end: subscription.cancel_at_period_end,
|
||||||
current_period_end: subscription.current_period_end
|
current_period_end: subscription.current_period_end
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -235,6 +245,7 @@ module.exports = class StripePaymentProcessor {
|
||||||
|
|
||||||
subscription_id: subscription.id,
|
subscription_id: subscription.id,
|
||||||
status: subscription.status,
|
status: subscription.status,
|
||||||
|
cancel_at_period_end: subscription.cancel_at_period_end,
|
||||||
current_period_end: new Date(subscription.current_period_end * 1000),
|
current_period_end: new Date(subscription.current_period_end * 1000),
|
||||||
start_date: new Date(subscription.start_date * 1000),
|
start_date: new Date(subscription.start_date * 1000),
|
||||||
default_payment_card_last4: payment && payment.card && payment.card.last4 || null,
|
default_payment_card_last4: payment && payment.card && payment.card.last4 || null,
|
||||||
|
|
Loading…
Add table
Reference in a new issue