Added "complimentary" subscription handling (#118)

refs https://github.com/TryGhost/Ghost/pull/11537

- Adds ability to assign and cancel "complimentary" type of subscriptions to the member
- The functionality is needed to be able to provide free premium plans for members (e.g. family members, trials, gifts)
- When member already has an active paid subscription and complimentary one is applied the old one is upgraded. Proration is not given
- When deleting a subscription we need to update localy stored records right away to be albe to reflect the change in the UI. This behavior will also be in line with how subscriptions updates/creates are handled
- Blocked any client update for complimentary subscription. We should prevent non authenticated clients from upgrading/subscribing themselves to "complimentary" plan.
This commit is contained in:
Naz Gargol 2020-01-27 12:34:22 +07:00 committed by GitHub
parent 89b78a883d
commit 28d3a37824
3 changed files with 63 additions and 3 deletions

View File

@ -154,6 +154,12 @@ module.exports = function MembersApi({
return res.end('Bad Request.');
}
// NOTE: never allow "Complimenatry" plan to be subscribed to from the client
if (plan.toLowerCase() === 'complimentary') {
res.writeHead(400);
return res.end('Bad Request.');
}
let email;
try {
if (!identity) {
@ -276,6 +282,11 @@ module.exports = function MembersApi({
return res.end('No permission');
}
if (subscription.plan.nickname === 'Complimentary') {
res.writeHead(400);
return res.end('Bad request');
}
if (cancelAtPeriodEnd === undefined) {
throw new common.errors.BadRequestError({
message: 'Canceling membership failed!',

View File

@ -128,9 +128,10 @@ module.exports = class StripePaymentProcessor {
return subscription.status !== 'canceled';
});
await Promise.all(activeSubscriptions.map((subscription) => {
return del(this._stripe, 'subscriptions', subscription.id);
}));
for (const subscription of activeSubscriptions) {
const updatedSubscription = await del(this._stripe, 'subscriptions', subscription.id);
await this._updateSubscription(updatedSubscription);
}
return true;
}
@ -177,6 +178,40 @@ module.exports = class StripePaymentProcessor {
});
}
async setComplimentarySubscription(member) {
const subscriptions = await this.getActiveSubscriptions(member);
const complimentaryPlan = this._plans.find(plan => (plan.nickname === 'Complimentary'));
const customer = await this._customerForMemberCheckoutSession(member);
if (!subscriptions.length) {
const subscription = await create(this._stripe, 'subscriptions', {
customer: customer.id,
items: [{
plan: complimentaryPlan.id
}]
});
await this._updateSubscription(subscription);
} else {
// NOTE: we should only ever have 1 active subscription, but just in case there is more update is done on all of them
for (const subscription of subscriptions) {
const updatedSubscription = await update(this._stripe, 'subscriptions', subscription.id, {
proration_behavior: 'none',
plan: complimentaryPlan.id
});
await this._updateSubscription(updatedSubscription);
}
}
}
async cancelComplimentarySubscription(member) {
// NOTE: a more explicit way would be cancelling just the "Complimentary" subscription, but doing it
// through existing method achieves the same as there should be only one subscription at a time
await this.cancelAllSubscriptions(member);
}
async getActiveSubscriptions(member) {
const subscriptions = await this.getSubscriptions(member);

View File

@ -75,6 +75,18 @@ module.exports = function ({
}
}
async function setComplimentarySubscription(member) {
if (stripe) {
await stripe.setComplimentarySubscription(member);
}
}
async function cancelComplimentarySubscription(member) {
if (stripe) {
await stripe.cancelComplimentarySubscription(member);
}
}
async function get(data, options) {
debug(`get id:${data.id} email:${data.email}`);
const member = await getMember(data, options);
@ -146,6 +158,8 @@ module.exports = function ({
get,
destroy,
getStripeSubscriptions,
setComplimentarySubscription,
cancelComplimentarySubscription,
destroyStripeSubscriptions
};
};