mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -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
|
||||
});
|
||||
|
||||
const users = Users({
|
||||
stripe,
|
||||
Member
|
||||
});
|
||||
|
||||
async function sendEmailWithMagicLink({email, requestedType, payload, options = {forceEmailType: false}}) {
|
||||
if (options.forceEmailType) {
|
||||
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'});
|
||||
}
|
||||
|
||||
const users = Users({
|
||||
stripe,
|
||||
Member
|
||||
});
|
||||
|
||||
async function getMemberDataFromMagicLinkToken(token) {
|
||||
const email = await magicLinkService.getUserFromToken(token);
|
||||
const {labels = [], name = '', oldEmail} = await magicLinkService.getPayloadFromToken(token);
|
||||
|
@ -148,7 +148,11 @@ module.exports = function MembersApi({
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -261,10 +265,10 @@ module.exports = function MembersApi({
|
|||
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
|
||||
if (member && member.stripe.subscriptions.length > 0) {
|
||||
if (member && member.related('stripeSubscriptions').length > 0) {
|
||||
res.writeHead(403);
|
||||
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');
|
||||
|
||||
if (payerName && !member.name) {
|
||||
await users.update({name: payerName}, {id: member.id});
|
||||
if (payerName && !member.get('name')) {
|
||||
await users.update({name: payerName}, {id: member.get('id')});
|
||||
}
|
||||
|
||||
const emailType = 'signup';
|
||||
|
@ -411,7 +415,7 @@ module.exports = function MembersApi({
|
|||
|
||||
const claims = await decodeToken(identity);
|
||||
const email = claims.sub;
|
||||
member = email ? await users.get({email}) : null;
|
||||
member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
|
||||
|
||||
if (!member) {
|
||||
throw new common.errors.BadRequestError({
|
||||
|
@ -429,7 +433,9 @@ module.exports = function MembersApi({
|
|||
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) {
|
||||
res.writeHead(403);
|
||||
return res.end('No permission');
|
||||
|
@ -442,7 +448,7 @@ module.exports = function MembersApi({
|
|||
});
|
||||
}
|
||||
const subscriptionUpdate = {
|
||||
id: subscription.id
|
||||
id: subscription.get('subscription_id')
|
||||
};
|
||||
if (cancelAtPeriodEnd !== undefined) {
|
||||
subscriptionUpdate.cancel_at_period_end = !!(cancelAtPeriodEnd);
|
||||
|
|
|
@ -46,16 +46,16 @@ module.exports = function ({
|
|||
return;
|
||||
}
|
||||
|
||||
const customers = (await StripeCustomer.findAll({
|
||||
filter: `member_id:${member.id}`
|
||||
})).toJSON();
|
||||
if (!member.relations.stripeCustomers) {
|
||||
await member.load(['stripeCustomers']);
|
||||
}
|
||||
|
||||
const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => {
|
||||
const customerSubscriptions = await StripeCustomerSubscription.findAll({
|
||||
filter: `customer_id:${customer.customer_id}`
|
||||
});
|
||||
return (await subscriptionsPromise).concat(customerSubscriptions.toJSON());
|
||||
}, []);
|
||||
if (!member.relations.stripeSubscriptions) {
|
||||
await member.load(['stripeSubscriptions', 'stripeSubscriptions.customer']);
|
||||
}
|
||||
|
||||
const customers = member.related('stripeCustomers').toJSON();
|
||||
const subscriptions = member.related('stripeSubscriptions').toJSON();
|
||||
|
||||
return {
|
||||
customers: customers,
|
||||
|
|
|
@ -5,15 +5,6 @@ const api = require('./api');
|
|||
|
||||
const STRIPE_API_VERSION = '2019-09-09';
|
||||
|
||||
const CURRENCY_SYMBOLS = {
|
||||
usd: '$',
|
||||
aud: '$',
|
||||
cad: '$',
|
||||
gbp: '£',
|
||||
eur: '€',
|
||||
inr: '₹'
|
||||
};
|
||||
|
||||
module.exports = class StripePaymentProcessor {
|
||||
constructor(config, storage, logging) {
|
||||
this.logging = logging;
|
||||
|
@ -244,7 +235,7 @@ module.exports = class StripePaymentProcessor {
|
|||
payment_method_types: ['card'],
|
||||
success_url: options.successUrl || this._billingSuccessUrl,
|
||||
cancel_url: options.cancelUrl || this._billingCancelUrl,
|
||||
customer_email: member.email,
|
||||
customer_email: member.get('email'),
|
||||
setup_intent_data: {
|
||||
metadata: {
|
||||
customer_id: customer.id
|
||||
|
@ -296,35 +287,7 @@ module.exports = class StripePaymentProcessor {
|
|||
async getSubscriptions(member) {
|
||||
const metadata = await this.storage.get(member);
|
||||
|
||||
const customers = metadata.customers.reduce((customers, customer) => {
|
||||
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
|
||||
};
|
||||
});
|
||||
return metadata.subscriptions;
|
||||
}
|
||||
|
||||
async setComplimentarySubscription(member) {
|
||||
|
@ -438,11 +401,11 @@ module.exports = class StripePaymentProcessor {
|
|||
}
|
||||
|
||||
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({
|
||||
customer: {
|
||||
customer_id: customer.id,
|
||||
member_id: member.id,
|
||||
member_id: member.get('id'),
|
||||
name: customer.name,
|
||||
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', {
|
||||
email: member.email
|
||||
email: member.get('email')
|
||||
});
|
||||
|
||||
await this._updateCustomer(member, customer);
|
||||
|
|
|
@ -1,184 +1,89 @@
|
|||
const _ = require('lodash');
|
||||
const debug = require('ghost-ignition').debug('users');
|
||||
const common = require('./common');
|
||||
|
||||
module.exports = function ({
|
||||
stripe,
|
||||
Member
|
||||
}) {
|
||||
async function createMember({email, name, note, labels, geolocation}) {
|
||||
const model = await Member.add({
|
||||
async function get(data, options) {
|
||||
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,
|
||||
name,
|
||||
note,
|
||||
labels,
|
||||
geolocation
|
||||
});
|
||||
const member = model.toJSON();
|
||||
return member;
|
||||
}, options);
|
||||
}
|
||||
|
||||
async function getMember(data, options = {}) {
|
||||
if (!data.email && !data.id && !data.uuid) {
|
||||
return null;
|
||||
}
|
||||
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
|
||||
function safeStripe(methodName) {
|
||||
return async function (...args) {
|
||||
if (stripe) {
|
||||
return await stripe[methodName](...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
debug(`create email:${data.email}`);
|
||||
|
||||
/** Member.add model method expects label object array*/
|
||||
if (data.labels) {
|
||||
data.labels.forEach((label, index) => {
|
||||
if (_.isString(label)) {
|
||||
data.labels[index] = {name: label};
|
||||
}
|
||||
});
|
||||
async function linkStripeCustomerById(customerId, memberId) {
|
||||
if (!stripe) {
|
||||
return;
|
||||
}
|
||||
const member = await get({id: memberId});
|
||||
return stripe.linkStripeCustomer(customerId, member);
|
||||
}
|
||||
|
||||
const member = await createMember(data);
|
||||
return member;
|
||||
async function setComplimentarySubscriptionById(memberId) {
|
||||
if (!stripe) {
|
||||
return;
|
||||
}
|
||||
const member = await get({id: memberId});
|
||||
return stripe.setComplimentarySubscription(member);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -187,11 +92,12 @@ module.exports = function ({
|
|||
list,
|
||||
get,
|
||||
destroy,
|
||||
getStripeSubscriptions,
|
||||
setComplimentarySubscription,
|
||||
cancelComplimentarySubscription,
|
||||
cancelStripeSubscriptions,
|
||||
getStripeCustomer,
|
||||
linkStripeCustomer
|
||||
setComplimentarySubscription: safeStripe('setComplimentarySubscription'),
|
||||
setComplimentarySubscriptionById,
|
||||
cancelComplimentarySubscription: safeStripe('cancelComplimentarySubscription'),
|
||||
cancelStripeSubscriptions: safeStripe('cancelComplimentarySubscription'),
|
||||
getStripeCustomer: safeStripe('getStripeCustomer'),
|
||||
linkStripeCustomer: safeStripe('linkStripeCustomer'),
|
||||
linkStripeCustomerById
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue