mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -05:00
Used models internally and for exported API (#195)
no-issue Using models internally and in the exported API means that we avoid expensive `toJSON` calls, which affects performance when looping through large lists of members. It also allows us to take advantage of the new relations used in the models. The addition of "ByID" methods for linking stripe customers and setting complimentary subscriptions allows bulk imports to avoid the overhead of creating a model for each members, instead passing an id string. n.b. currently the impl _does_ still create models, but it makes it easier to optimise and refactor in the future.
This commit is contained in:
parent
85800c57f7
commit
117309b4e8
4 changed files with 108 additions and 233 deletions
|
@ -100,6 +100,11 @@ module.exports = function MembersApi({
|
||||||
getSubject
|
getSubject
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const users = Users({
|
||||||
|
stripe,
|
||||||
|
Member
|
||||||
|
});
|
||||||
|
|
||||||
async function sendEmailWithMagicLink({email, requestedType, payload, options = {forceEmailType: false}}) {
|
async function sendEmailWithMagicLink({email, requestedType, payload, options = {forceEmailType: false}}) {
|
||||||
if (options.forceEmailType) {
|
if (options.forceEmailType) {
|
||||||
return magicLinkService.sendMagicLink({email, payload, subject: email, type: requestedType});
|
return magicLinkService.sendMagicLink({email, payload, subject: email, type: requestedType});
|
||||||
|
@ -117,11 +122,6 @@ module.exports = function MembersApi({
|
||||||
return magicLinkService.getMagicLink({email, subject: email, type: 'signin'});
|
return magicLinkService.getMagicLink({email, subject: email, type: 'signin'});
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = Users({
|
|
||||||
stripe,
|
|
||||||
Member
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getMemberDataFromMagicLinkToken(token) {
|
async function getMemberDataFromMagicLinkToken(token) {
|
||||||
const email = await magicLinkService.getUserFromToken(token);
|
const email = await magicLinkService.getUserFromToken(token);
|
||||||
const {labels = [], name = '', oldEmail} = await magicLinkService.getPayloadFromToken(token);
|
const {labels = [], name = '', oldEmail} = await magicLinkService.getPayloadFromToken(token);
|
||||||
|
@ -148,7 +148,11 @@ module.exports = function MembersApi({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMemberIdentityData(email) {
|
async function getMemberIdentityData(email) {
|
||||||
return users.get({email});
|
const model = await users.get({email}, {withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer']});
|
||||||
|
if (!model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return model.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMemberIdentityToken(email) {
|
async function getMemberIdentityToken(email) {
|
||||||
|
@ -261,10 +265,10 @@ module.exports = function MembersApi({
|
||||||
return res.end('Unauthorized');
|
return res.end('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
const member = email ? await users.get({email}) : null;
|
const member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
|
||||||
|
|
||||||
// Do not allow members already with a subscription to initiate a new checkout session
|
// Do not allow members already with a subscription to initiate a new checkout session
|
||||||
if (member && member.stripe.subscriptions.length > 0) {
|
if (member && member.related('stripeSubscriptions').length > 0) {
|
||||||
res.writeHead(403);
|
res.writeHead(403);
|
||||||
return res.end('No permission');
|
return res.end('No permission');
|
||||||
}
|
}
|
||||||
|
@ -374,8 +378,8 @@ module.exports = function MembersApi({
|
||||||
|
|
||||||
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
|
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
|
||||||
|
|
||||||
if (payerName && !member.name) {
|
if (payerName && !member.get('name')) {
|
||||||
await users.update({name: payerName}, {id: member.id});
|
await users.update({name: payerName}, {id: member.get('id')});
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailType = 'signup';
|
const emailType = 'signup';
|
||||||
|
@ -411,7 +415,7 @@ module.exports = function MembersApi({
|
||||||
|
|
||||||
const claims = await decodeToken(identity);
|
const claims = await decodeToken(identity);
|
||||||
const email = claims.sub;
|
const email = claims.sub;
|
||||||
member = email ? await users.get({email}) : null;
|
member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
throw new common.errors.BadRequestError({
|
throw new common.errors.BadRequestError({
|
||||||
|
@ -429,7 +433,9 @@ module.exports = function MembersApi({
|
||||||
message: 'Updating subscription failed! Could not find plan'
|
message: 'Updating subscription failed! Could not find plan'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const subscription = member.stripe.subscriptions.find(sub => sub.id === subscriptionId);
|
const subscription = member.related('stripeSubscriptions').models.find(
|
||||||
|
subscription => subscription.get('subscription_id') === subscriptionId
|
||||||
|
);
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.writeHead(403);
|
res.writeHead(403);
|
||||||
return res.end('No permission');
|
return res.end('No permission');
|
||||||
|
@ -442,7 +448,7 @@ module.exports = function MembersApi({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const subscriptionUpdate = {
|
const subscriptionUpdate = {
|
||||||
id: subscription.id
|
id: subscription.get('subscription_id')
|
||||||
};
|
};
|
||||||
if (cancelAtPeriodEnd !== undefined) {
|
if (cancelAtPeriodEnd !== undefined) {
|
||||||
subscriptionUpdate.cancel_at_period_end = !!(cancelAtPeriodEnd);
|
subscriptionUpdate.cancel_at_period_end = !!(cancelAtPeriodEnd);
|
||||||
|
|
|
@ -46,16 +46,16 @@ module.exports = function ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customers = (await StripeCustomer.findAll({
|
if (!member.relations.stripeCustomers) {
|
||||||
filter: `member_id:${member.id}`
|
await member.load(['stripeCustomers']);
|
||||||
})).toJSON();
|
}
|
||||||
|
|
||||||
const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => {
|
if (!member.relations.stripeSubscriptions) {
|
||||||
const customerSubscriptions = await StripeCustomerSubscription.findAll({
|
await member.load(['stripeSubscriptions', 'stripeSubscriptions.customer']);
|
||||||
filter: `customer_id:${customer.customer_id}`
|
}
|
||||||
});
|
|
||||||
return (await subscriptionsPromise).concat(customerSubscriptions.toJSON());
|
const customers = member.related('stripeCustomers').toJSON();
|
||||||
}, []);
|
const subscriptions = member.related('stripeSubscriptions').toJSON();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customers: customers,
|
customers: customers,
|
||||||
|
|
|
@ -5,15 +5,6 @@ const api = require('./api');
|
||||||
|
|
||||||
const STRIPE_API_VERSION = '2019-09-09';
|
const STRIPE_API_VERSION = '2019-09-09';
|
||||||
|
|
||||||
const CURRENCY_SYMBOLS = {
|
|
||||||
usd: '$',
|
|
||||||
aud: '$',
|
|
||||||
cad: '$',
|
|
||||||
gbp: '£',
|
|
||||||
eur: '€',
|
|
||||||
inr: '₹'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = class StripePaymentProcessor {
|
module.exports = class StripePaymentProcessor {
|
||||||
constructor(config, storage, logging) {
|
constructor(config, storage, logging) {
|
||||||
this.logging = logging;
|
this.logging = logging;
|
||||||
|
@ -244,7 +235,7 @@ module.exports = class StripePaymentProcessor {
|
||||||
payment_method_types: ['card'],
|
payment_method_types: ['card'],
|
||||||
success_url: options.successUrl || this._billingSuccessUrl,
|
success_url: options.successUrl || this._billingSuccessUrl,
|
||||||
cancel_url: options.cancelUrl || this._billingCancelUrl,
|
cancel_url: options.cancelUrl || this._billingCancelUrl,
|
||||||
customer_email: member.email,
|
customer_email: member.get('email'),
|
||||||
setup_intent_data: {
|
setup_intent_data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
customer_id: customer.id
|
customer_id: customer.id
|
||||||
|
@ -296,35 +287,7 @@ module.exports = class StripePaymentProcessor {
|
||||||
async getSubscriptions(member) {
|
async getSubscriptions(member) {
|
||||||
const metadata = await this.storage.get(member);
|
const metadata = await this.storage.get(member);
|
||||||
|
|
||||||
const customers = metadata.customers.reduce((customers, customer) => {
|
return metadata.subscriptions;
|
||||||
return Object.assign(customers, {
|
|
||||||
[customer.customer_id]: {
|
|
||||||
id: customer.customer_id,
|
|
||||||
name: customer.name,
|
|
||||||
email: customer.email
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return metadata.subscriptions.map((subscription) => {
|
|
||||||
return {
|
|
||||||
id: subscription.subscription_id,
|
|
||||||
customer: customers[subscription.customer_id],
|
|
||||||
plan: {
|
|
||||||
id: subscription.plan_id,
|
|
||||||
nickname: subscription.plan_nickname,
|
|
||||||
interval: subscription.plan_interval,
|
|
||||||
amount: subscription.plan_amount,
|
|
||||||
currency: String.prototype.toUpperCase.call(subscription.plan_currency),
|
|
||||||
currency_symbol: CURRENCY_SYMBOLS[subscription.plan_currency]
|
|
||||||
},
|
|
||||||
status: subscription.status,
|
|
||||||
start_date: subscription.start_date,
|
|
||||||
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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setComplimentarySubscription(member) {
|
async setComplimentarySubscription(member) {
|
||||||
|
@ -438,11 +401,11 @@ module.exports = class StripePaymentProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateCustomer(member, customer) {
|
async _updateCustomer(member, customer) {
|
||||||
debug(`Attaching customer to member ${member.email} ${customer.id}`);
|
debug(`Attaching customer to member ${member.get('email')} ${customer.id}`);
|
||||||
await this.storage.set({
|
await this.storage.set({
|
||||||
customer: {
|
customer: {
|
||||||
customer_id: customer.id,
|
customer_id: customer.id,
|
||||||
member_id: member.id,
|
member_id: member.get('id'),
|
||||||
name: customer.name,
|
name: customer.name,
|
||||||
email: customer.email
|
email: customer.email
|
||||||
}
|
}
|
||||||
|
@ -500,9 +463,9 @@ module.exports = class StripePaymentProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(`Creating customer for member ${member.email}`);
|
debug(`Creating customer for member ${member.get('email')}`);
|
||||||
const customer = await create(this._stripe, 'customers', {
|
const customer = await create(this._stripe, 'customers', {
|
||||||
email: member.email
|
email: member.get('email')
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._updateCustomer(member, customer);
|
await this._updateCustomer(member, customer);
|
||||||
|
|
|
@ -1,184 +1,89 @@
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const debug = require('ghost-ignition').debug('users');
|
const debug = require('ghost-ignition').debug('users');
|
||||||
const common = require('./common');
|
|
||||||
|
|
||||||
module.exports = function ({
|
module.exports = function ({
|
||||||
stripe,
|
stripe,
|
||||||
Member
|
Member
|
||||||
}) {
|
}) {
|
||||||
async function createMember({email, name, note, labels, geolocation}) {
|
async function get(data, options) {
|
||||||
const model = await Member.add({
|
debug(`get id:${data.id} email:${data.email}`);
|
||||||
|
return Member.findOne(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function destroy(data, options) {
|
||||||
|
debug(`destroy id:${data.id} email:${data.email}`);
|
||||||
|
const member = await Member.findOne(data, options);
|
||||||
|
if (!member) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stripe && options.cancelStripeSubscriptions) {
|
||||||
|
await stripe.cancelStripeSubscriptions(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Member.destroy({
|
||||||
|
id: data.id
|
||||||
|
}, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(data, options) {
|
||||||
|
debug(`update id:${options.id}`);
|
||||||
|
return Member.edit(_.pick(data, [
|
||||||
|
'email',
|
||||||
|
'name',
|
||||||
|
'note',
|
||||||
|
'labels',
|
||||||
|
'geolocation'
|
||||||
|
]), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function list(options = {}) {
|
||||||
|
return Member.findPage(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create({email, name, note, labels, geolocation}, options) {
|
||||||
|
debug(`create email:${email}`);
|
||||||
|
|
||||||
|
/** Member.add model method expects label object array*/
|
||||||
|
if (labels) {
|
||||||
|
labels.forEach((label, index) => {
|
||||||
|
if (_.isString(label)) {
|
||||||
|
labels[index] = {name: label};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Member.add({
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
note,
|
note,
|
||||||
labels,
|
labels,
|
||||||
geolocation
|
geolocation
|
||||||
});
|
}, options);
|
||||||
const member = model.toJSON();
|
|
||||||
return member;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMember(data, options = {}) {
|
function safeStripe(methodName) {
|
||||||
if (!data.email && !data.id && !data.uuid) {
|
return async function (...args) {
|
||||||
return null;
|
if (stripe) {
|
||||||
}
|
return await stripe[methodName](...args);
|
||||||
const model = await Member.findOne(data, options);
|
}
|
||||||
if (!model) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const member = model.toJSON(options);
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMember(data, options = {}) {
|
|
||||||
const attrs = _.pick(data, ['email', 'name', 'note', 'subscribed', 'geolocation']);
|
|
||||||
|
|
||||||
const model = await Member.edit(attrs, options);
|
|
||||||
|
|
||||||
const member = model.toJSON(options);
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteMember(options) {
|
|
||||||
options = options || {};
|
|
||||||
return Member.destroy(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function listMembers(options) {
|
|
||||||
return Member.findPage(options).then((models) => {
|
|
||||||
return {
|
|
||||||
members: models.data.map(model => model.toJSON(options)),
|
|
||||||
meta: models.meta
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStripeSubscriptions(member) {
|
|
||||||
if (!stripe) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return await stripe.getActiveSubscriptions(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cancelStripeSubscriptions(member) {
|
|
||||||
if (stripe) {
|
|
||||||
await stripe.cancelAllSubscriptions(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setComplimentarySubscription(member) {
|
|
||||||
if (stripe) {
|
|
||||||
await stripe.setComplimentarySubscription(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cancelComplimentarySubscription(member) {
|
|
||||||
if (stripe) {
|
|
||||||
await stripe.cancelComplimentarySubscription(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function linkStripeCustomer(id, member) {
|
|
||||||
if (stripe) {
|
|
||||||
await stripe.linkStripeCustomer(id, member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStripeCustomer(id) {
|
|
||||||
if (stripe) {
|
|
||||||
return await stripe.getStripeCustomer(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function get(data, options) {
|
|
||||||
debug(`get id:${data.id} email:${data.email}`);
|
|
||||||
const member = await getMember(data, options);
|
|
||||||
if (!member) {
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subscriptions = await getStripeSubscriptions(member);
|
|
||||||
|
|
||||||
return Object.assign(member, {
|
|
||||||
stripe: {
|
|
||||||
subscriptions
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
common.logging.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function destroy(data, options) {
|
|
||||||
debug(`destroy id:${data.id} email:${data.email}`);
|
|
||||||
const member = await getMember(data, options);
|
|
||||||
if (!member) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await cancelStripeSubscriptions(member);
|
|
||||||
|
|
||||||
return deleteMember(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function update(data, options) {
|
|
||||||
debug(`update id:${options.id}`);
|
|
||||||
|
|
||||||
const member = await updateMember(data, options);
|
|
||||||
if (!member) {
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subscriptions = await getStripeSubscriptions(member);
|
|
||||||
|
|
||||||
return Object.assign(member, {
|
|
||||||
stripe: {
|
|
||||||
subscriptions
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
common.logging.error(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function list(options) {
|
|
||||||
const {meta, members} = await listMembers(options);
|
|
||||||
|
|
||||||
const membersWithSubscriptions = await Promise.all(members.map(async function (member) {
|
|
||||||
const subscriptions = await getStripeSubscriptions(member);
|
|
||||||
|
|
||||||
return Object.assign(member, {
|
|
||||||
stripe: {
|
|
||||||
subscriptions
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
meta,
|
|
||||||
members: membersWithSubscriptions
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(data) {
|
async function linkStripeCustomerById(customerId, memberId) {
|
||||||
debug(`create email:${data.email}`);
|
if (!stripe) {
|
||||||
|
return;
|
||||||
/** Member.add model method expects label object array*/
|
|
||||||
if (data.labels) {
|
|
||||||
data.labels.forEach((label, index) => {
|
|
||||||
if (_.isString(label)) {
|
|
||||||
data.labels[index] = {name: label};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
const member = await get({id: memberId});
|
||||||
|
return stripe.linkStripeCustomer(customerId, member);
|
||||||
|
}
|
||||||
|
|
||||||
const member = await createMember(data);
|
async function setComplimentarySubscriptionById(memberId) {
|
||||||
return member;
|
if (!stripe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const member = await get({id: memberId});
|
||||||
|
return stripe.setComplimentarySubscription(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -187,11 +92,12 @@ module.exports = function ({
|
||||||
list,
|
list,
|
||||||
get,
|
get,
|
||||||
destroy,
|
destroy,
|
||||||
getStripeSubscriptions,
|
setComplimentarySubscription: safeStripe('setComplimentarySubscription'),
|
||||||
setComplimentarySubscription,
|
setComplimentarySubscriptionById,
|
||||||
cancelComplimentarySubscription,
|
cancelComplimentarySubscription: safeStripe('cancelComplimentarySubscription'),
|
||||||
cancelStripeSubscriptions,
|
cancelStripeSubscriptions: safeStripe('cancelComplimentarySubscription'),
|
||||||
getStripeCustomer,
|
getStripeCustomer: safeStripe('getStripeCustomer'),
|
||||||
linkStripeCustomer
|
linkStripeCustomer: safeStripe('linkStripeCustomer'),
|
||||||
|
linkStripeCustomerById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue