2019-08-09 17:11:24 +03:00
|
|
|
|
// NOTE: We must not cache references to membersService.api
|
|
|
|
|
// as it is a getter and may change during runtime.
|
2019-10-03 20:59:19 +03:00
|
|
|
|
const Promise = require('bluebird');
|
2020-05-26 12:38:42 +03:00
|
|
|
|
const moment = require('moment-timezone');
|
|
|
|
|
const errors = require('@tryghost/errors');
|
2019-12-06 08:04:10 +03:00
|
|
|
|
const models = require('../../models');
|
2019-08-09 17:11:24 +03:00
|
|
|
|
const membersService = require('../../services/members');
|
2021-07-07 23:41:34 +03:00
|
|
|
|
const labsService = require('../../../shared/labs');
|
2021-01-14 04:05:22 +03:00
|
|
|
|
|
2021-06-30 16:56:57 +03:00
|
|
|
|
const settingsCache = require('../../../shared/settings-cache');
|
2021-10-06 13:40:32 +03:00
|
|
|
|
const tpl = require('@tryghost/tpl');
|
2021-02-15 21:47:04 +03:00
|
|
|
|
const _ = require('lodash');
|
2019-08-09 17:11:24 +03:00
|
|
|
|
|
2021-10-06 13:40:32 +03:00
|
|
|
|
const messages = {
|
|
|
|
|
memberNotFound: 'Member not found.',
|
|
|
|
|
memberAlreadyExists: {
|
|
|
|
|
message: 'Member already exists',
|
|
|
|
|
context: 'Attempting to {action} member with existing email address.'
|
|
|
|
|
},
|
|
|
|
|
stripeNotConnected: {
|
|
|
|
|
message: 'Missing Stripe connection.',
|
|
|
|
|
context: 'Attempting to import members with Stripe data when there is no Stripe account connected.',
|
|
|
|
|
help: 'help'
|
|
|
|
|
},
|
|
|
|
|
stripeCustomerNotFound: {
|
|
|
|
|
context: 'Missing Stripe customer.',
|
|
|
|
|
help: 'Make sure you’re connected to the correct Stripe Account.'
|
|
|
|
|
},
|
|
|
|
|
resourceNotFound: '{resource} not found.'
|
|
|
|
|
};
|
|
|
|
|
|
2021-04-20 13:33:35 +03:00
|
|
|
|
const allowedIncludes = ['email_recipients', 'products'];
|
2020-02-04 08:51:24 +03:00
|
|
|
|
|
2020-08-12 16:17:44 +03:00
|
|
|
|
module.exports = {
|
2019-08-09 17:11:24 +03:00
|
|
|
|
docName: 'members',
|
2020-06-18 19:07:02 +03:00
|
|
|
|
|
|
|
|
|
hasActiveStripeSubscriptions: {
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
|
|
|
|
async query() {
|
|
|
|
|
const hasActiveStripeSubscriptions = await membersService.api.hasActiveStripeSubscriptions();
|
|
|
|
|
return {
|
|
|
|
|
hasActiveStripeSubscriptions
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-08-09 17:11:24 +03:00
|
|
|
|
browse: {
|
|
|
|
|
options: [
|
|
|
|
|
'limit',
|
|
|
|
|
'fields',
|
|
|
|
|
'filter',
|
|
|
|
|
'order',
|
|
|
|
|
'debug',
|
2020-05-28 12:14:02 +03:00
|
|
|
|
'page',
|
2021-01-28 19:31:02 +03:00
|
|
|
|
'search'
|
2019-08-09 17:11:24 +03:00
|
|
|
|
],
|
|
|
|
|
permissions: true,
|
|
|
|
|
validation: {},
|
2020-01-15 13:52:47 +03:00
|
|
|
|
async query(frame) {
|
2021-04-26 19:14:34 +03:00
|
|
|
|
frame.options.withRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'];
|
2020-08-12 16:17:44 +03:00
|
|
|
|
const page = await membersService.api.members.list(frame.options);
|
|
|
|
|
|
2020-09-30 12:22:22 +03:00
|
|
|
|
return page;
|
2019-08-09 17:11:24 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
read: {
|
2020-12-10 13:04:05 +03:00
|
|
|
|
options: [
|
|
|
|
|
'include'
|
|
|
|
|
],
|
2019-08-09 17:11:24 +03:00
|
|
|
|
headers: {},
|
|
|
|
|
data: [
|
|
|
|
|
'id',
|
|
|
|
|
'email'
|
|
|
|
|
],
|
2020-12-10 13:04:05 +03:00
|
|
|
|
validation: {
|
|
|
|
|
options: {
|
|
|
|
|
include: {
|
|
|
|
|
values: allowedIncludes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2019-08-09 17:11:24 +03:00
|
|
|
|
permissions: true,
|
2019-09-03 07:10:32 +03:00
|
|
|
|
async query(frame) {
|
2021-08-25 15:54:38 +03:00
|
|
|
|
let member = await membersService.api.memberBREADService.read(frame.data, frame.options);
|
2020-12-10 13:04:05 +03:00
|
|
|
|
|
2021-08-25 15:54:38 +03:00
|
|
|
|
if (!member) {
|
2020-05-22 21:22:20 +03:00
|
|
|
|
throw new errors.NotFoundError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.memberNotFound)
|
2019-09-03 07:10:32 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
2020-01-15 13:52:47 +03:00
|
|
|
|
|
2021-08-25 15:54:38 +03:00
|
|
|
|
return member;
|
2019-08-09 17:11:24 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-10-03 12:15:50 +03:00
|
|
|
|
add: {
|
|
|
|
|
statusCode: 201,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
|
|
|
|
'send_email',
|
|
|
|
|
'email_type'
|
|
|
|
|
],
|
|
|
|
|
validation: {
|
|
|
|
|
data: {
|
|
|
|
|
email: {required: true}
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
email_type: {
|
|
|
|
|
values: ['signin', 'signup', 'subscribe']
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
permissions: true,
|
2019-10-09 10:14:26 +03:00
|
|
|
|
async query(frame) {
|
2020-08-12 16:17:44 +03:00
|
|
|
|
let member;
|
2021-04-26 19:14:34 +03:00
|
|
|
|
frame.options.withRelated = ['stripeSubscriptions', 'products', 'labels', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'];
|
2021-06-17 14:44:25 +03:00
|
|
|
|
if (!labsService.isSet('multipleProducts')) {
|
|
|
|
|
delete frame.data.products;
|
|
|
|
|
}
|
2019-10-09 10:14:26 +03:00
|
|
|
|
try {
|
2020-10-14 03:24:09 +03:00
|
|
|
|
if (!membersService.config.isStripeConnected()
|
|
|
|
|
&& (frame.data.members[0].stripe_customer_id || frame.data.members[0].comped)) {
|
|
|
|
|
const property = frame.data.members[0].comped ? 'comped' : 'stripe_customer_id';
|
|
|
|
|
|
|
|
|
|
throw new errors.ValidationError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.stripeNotConnected.message),
|
|
|
|
|
context: tpl(messages.stripeNotConnected.context),
|
|
|
|
|
help: tpl(messages.stripeNotConnected.help),
|
2020-10-14 03:24:09 +03:00
|
|
|
|
property
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-12 16:17:44 +03:00
|
|
|
|
member = await membersService.api.members.create(frame.data.members[0], frame.options);
|
2020-02-04 08:51:24 +03:00
|
|
|
|
|
|
|
|
|
if (frame.data.members[0].stripe_customer_id) {
|
2021-05-20 15:44:35 +03:00
|
|
|
|
await membersService.api.members.linkStripeCustomer({
|
|
|
|
|
customer_id: frame.data.members[0].stripe_customer_id,
|
|
|
|
|
member_id: member.id
|
|
|
|
|
}, frame.options);
|
2020-02-04 08:51:24 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-10 14:40:54 +03:00
|
|
|
|
if (!labsService.isSet('multipleProducts')) {
|
|
|
|
|
if (frame.data.members[0].comped) {
|
|
|
|
|
await membersService.api.members.setComplimentarySubscription(member);
|
|
|
|
|
}
|
2020-02-04 08:51:24 +03:00
|
|
|
|
}
|
2020-01-15 13:52:47 +03:00
|
|
|
|
|
|
|
|
|
if (frame.options.send_email) {
|
2020-08-12 16:17:44 +03:00
|
|
|
|
await membersService.api.sendEmailWithMagicLink({email: member.get('email'), requestedType: frame.options.email_type});
|
2020-01-15 13:52:47 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-30 12:22:22 +03:00
|
|
|
|
return member;
|
2019-10-09 10:14:26 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
2020-06-29 15:22:52 +03:00
|
|
|
|
throw new errors.ValidationError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.memberAlreadyExists.message),
|
|
|
|
|
context: tpl(messages.memberAlreadyExists.context, {
|
2020-09-10 07:03:57 +03:00
|
|
|
|
action: 'add'
|
|
|
|
|
})
|
2020-06-29 15:22:52 +03:00
|
|
|
|
});
|
2019-10-09 10:14:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-01 10:03:12 +03:00
|
|
|
|
// NOTE: failed to link Stripe customer/plan/subscription or have thrown custom Stripe connection error.
|
|
|
|
|
// It's a bit ugly doing regex matching to detect errors, but it's the easiest way that works without
|
|
|
|
|
// introducing additional logic/data format into current error handling
|
2020-10-14 03:24:09 +03:00
|
|
|
|
const isStripeLinkingError = error.message && (error.message.match(/customer|plan|subscription/g));
|
2020-08-12 16:17:44 +03:00
|
|
|
|
if (member && isStripeLinkingError) {
|
2020-06-05 15:06:19 +03:00
|
|
|
|
if (error.message.indexOf('customer') && error.code === 'resource_missing') {
|
2020-06-12 07:33:45 +03:00
|
|
|
|
error.message = `Member not imported. ${error.message}`;
|
2021-10-06 13:40:32 +03:00
|
|
|
|
error.context = tpl(messages.stripeCustomerNotFound.context);
|
|
|
|
|
error.help = tpl(messages.stripeCustomerNotFound.help);
|
2020-06-05 15:06:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-12 16:17:44 +03:00
|
|
|
|
await membersService.api.members.destroy({
|
|
|
|
|
id: member.get('id')
|
|
|
|
|
}, frame.options);
|
2020-02-04 08:51:24 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-09 10:14:26 +03:00
|
|
|
|
throw error;
|
|
|
|
|
}
|
2019-10-03 12:15:50 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-10-03 14:38:22 +03:00
|
|
|
|
edit: {
|
|
|
|
|
statusCode: 200,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
|
|
|
|
'id'
|
|
|
|
|
],
|
|
|
|
|
validation: {
|
|
|
|
|
options: {
|
|
|
|
|
id: {
|
|
|
|
|
required: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
permissions: true,
|
|
|
|
|
async query(frame) {
|
2021-06-17 14:44:25 +03:00
|
|
|
|
if (!labsService.isSet('multipleProducts')) {
|
|
|
|
|
delete frame.data.products;
|
|
|
|
|
}
|
2020-09-10 07:03:57 +03:00
|
|
|
|
try {
|
2021-04-26 19:14:34 +03:00
|
|
|
|
frame.options.withRelated = ['stripeSubscriptions', 'products', 'labels', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'];
|
2020-09-10 07:03:57 +03:00
|
|
|
|
const member = await membersService.api.members.update(frame.data.members[0], frame.options);
|
2020-01-28 07:25:00 +03:00
|
|
|
|
|
2021-01-04 20:12:57 +03:00
|
|
|
|
const hasCompedSubscription = !!member.related('stripeSubscriptions').find(sub => sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active');
|
2020-01-28 07:25:00 +03:00
|
|
|
|
|
2021-09-10 14:40:54 +03:00
|
|
|
|
if (!labsService.isSet('multipleProducts')) {
|
|
|
|
|
if (typeof frame.data.members[0].comped === 'boolean') {
|
|
|
|
|
if (frame.data.members[0].comped && !hasCompedSubscription) {
|
|
|
|
|
await membersService.api.members.setComplimentarySubscription(member);
|
|
|
|
|
} else if (!(frame.data.members[0].comped) && hasCompedSubscription) {
|
|
|
|
|
await membersService.api.members.cancelComplimentarySubscription(member);
|
|
|
|
|
}
|
2020-09-10 07:03:57 +03:00
|
|
|
|
|
2021-09-10 14:40:54 +03:00
|
|
|
|
await member.load(['stripeSubscriptions', 'products', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']);
|
|
|
|
|
}
|
2020-01-28 07:25:00 +03:00
|
|
|
|
}
|
2020-08-12 16:17:44 +03:00
|
|
|
|
|
2021-05-19 20:02:15 +03:00
|
|
|
|
await member.load(['stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']);
|
2020-01-15 13:52:47 +03:00
|
|
|
|
|
2020-09-30 12:22:22 +03:00
|
|
|
|
return member;
|
2020-09-10 07:03:57 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
|
|
|
|
throw new errors.ValidationError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.memberAlreadyExists.message),
|
|
|
|
|
context: tpl(messages.memberAlreadyExists.context, {
|
2020-09-10 07:03:57 +03:00
|
|
|
|
action: 'edit'
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-08-12 16:17:44 +03:00
|
|
|
|
|
2020-09-10 07:03:57 +03:00
|
|
|
|
throw error;
|
|
|
|
|
}
|
2019-10-03 14:38:22 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-08-20 14:58:11 +03:00
|
|
|
|
editSubscription: {
|
|
|
|
|
statusCode: 200,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
|
|
|
|
'id',
|
|
|
|
|
'subscription_id'
|
|
|
|
|
],
|
|
|
|
|
data: [
|
2021-06-16 13:25:19 +03:00
|
|
|
|
'cancel_at_period_end',
|
|
|
|
|
'status'
|
2020-08-20 14:58:11 +03:00
|
|
|
|
],
|
|
|
|
|
validation: {
|
|
|
|
|
options: {
|
|
|
|
|
id: {
|
|
|
|
|
required: true
|
|
|
|
|
},
|
|
|
|
|
subscription_id: {
|
|
|
|
|
required: true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
cancel_at_period_end: {
|
|
|
|
|
required: true
|
2021-06-16 13:25:19 +03:00
|
|
|
|
},
|
|
|
|
|
status: {
|
|
|
|
|
values: ['canceled']
|
2020-08-20 14:58:11 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'edit'
|
|
|
|
|
},
|
|
|
|
|
async query(frame) {
|
2021-06-16 13:25:19 +03:00
|
|
|
|
if (frame.data.status === 'canceled') {
|
|
|
|
|
await membersService.api.members.cancelSubscription({
|
|
|
|
|
id: frame.options.id,
|
|
|
|
|
subscription: {
|
|
|
|
|
subscription_id: frame.options.subscription_id
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await membersService.api.members.updateSubscription({
|
|
|
|
|
id: frame.options.id,
|
|
|
|
|
subscription: {
|
|
|
|
|
subscription_id: frame.options.subscription_id,
|
|
|
|
|
cancel_at_period_end: frame.data.cancel_at_period_end
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-08-20 14:58:11 +03:00
|
|
|
|
let model = await membersService.api.members.get({id: frame.options.id}, {
|
2021-04-26 19:14:34 +03:00
|
|
|
|
withRelated: ['labels', 'products', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']
|
|
|
|
|
});
|
|
|
|
|
if (!model) {
|
|
|
|
|
throw new errors.NotFoundError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.memberNotFound)
|
2021-04-26 19:14:34 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
createSubscription: {
|
|
|
|
|
statusCode: 200,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
|
|
|
|
'id'
|
|
|
|
|
],
|
|
|
|
|
data: [
|
|
|
|
|
'stripe_price_id'
|
|
|
|
|
],
|
|
|
|
|
validation: {
|
|
|
|
|
options: {
|
|
|
|
|
id: {
|
|
|
|
|
required: true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
stripe_price_id: {
|
|
|
|
|
required: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'edit'
|
|
|
|
|
},
|
|
|
|
|
async query(frame) {
|
|
|
|
|
await membersService.api.members.createSubscription({
|
|
|
|
|
id: frame.options.id,
|
|
|
|
|
subscription: {
|
|
|
|
|
stripe_price_id: frame.data.stripe_price_id
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
let model = await membersService.api.members.get({id: frame.options.id}, {
|
|
|
|
|
withRelated: ['labels', 'products', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']
|
2020-08-20 14:58:11 +03:00
|
|
|
|
});
|
|
|
|
|
if (!model) {
|
|
|
|
|
throw new errors.NotFoundError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.memberNotFound)
|
2020-08-20 14:58:11 +03:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-30 12:22:22 +03:00
|
|
|
|
return model;
|
2020-08-20 14:58:11 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-08-09 17:11:24 +03:00
|
|
|
|
destroy: {
|
|
|
|
|
statusCode: 204,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
2020-07-24 13:39:08 +03:00
|
|
|
|
'id',
|
|
|
|
|
'cancel'
|
2019-08-09 17:11:24 +03:00
|
|
|
|
],
|
|
|
|
|
validation: {
|
|
|
|
|
options: {
|
|
|
|
|
id: {
|
|
|
|
|
required: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
permissions: true,
|
2019-10-02 11:25:49 +03:00
|
|
|
|
async query(frame) {
|
2019-08-09 17:11:24 +03:00
|
|
|
|
frame.options.require = true;
|
2020-08-12 16:17:44 +03:00
|
|
|
|
frame.options.cancelStripeSubscriptions = frame.options.cancel;
|
2020-01-15 13:52:47 +03:00
|
|
|
|
|
2020-08-21 12:44:37 +03:00
|
|
|
|
await Promise.resolve(membersService.api.members.destroy({
|
|
|
|
|
id: frame.options.id
|
|
|
|
|
}, frame.options)).catch(models.Member.NotFoundError, () => {
|
|
|
|
|
throw new errors.NotFoundError({
|
2021-10-06 13:40:32 +03:00
|
|
|
|
message: tpl(messages.resourceNotFound, {
|
2020-08-21 12:44:37 +03:00
|
|
|
|
resource: 'Member'
|
|
|
|
|
})
|
2019-12-06 08:04:10 +03:00
|
|
|
|
});
|
2020-08-21 12:44:37 +03:00
|
|
|
|
});
|
2019-12-06 08:04:10 +03:00
|
|
|
|
|
2019-10-02 11:25:49 +03:00
|
|
|
|
return null;
|
2019-08-09 17:11:24 +03:00
|
|
|
|
}
|
2019-10-03 20:59:19 +03:00
|
|
|
|
},
|
|
|
|
|
|
2021-04-08 14:03:45 +03:00
|
|
|
|
bulkDestroy: {
|
|
|
|
|
statusCode: 200,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
|
|
|
|
'all',
|
|
|
|
|
'filter',
|
|
|
|
|
'search'
|
|
|
|
|
],
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'destroy'
|
|
|
|
|
},
|
|
|
|
|
async query(frame) {
|
2021-08-12 17:26:25 +03:00
|
|
|
|
const bulkDestroyResult = await membersService.api.members.bulkDestroy(frame.options);
|
2021-04-08 14:03:45 +03:00
|
|
|
|
|
|
|
|
|
// shaped to match the importer response
|
|
|
|
|
return {
|
|
|
|
|
meta: {
|
|
|
|
|
stats: {
|
|
|
|
|
successful: bulkDestroyResult.successful,
|
|
|
|
|
unsuccessful: bulkDestroyResult.unsuccessful
|
|
|
|
|
},
|
|
|
|
|
unsuccessfulIds: bulkDestroyResult.unsuccessfulIds,
|
|
|
|
|
errors: bulkDestroyResult.errors
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-13 16:18:57 +03:00
|
|
|
|
bulkEdit: {
|
|
|
|
|
statusCode: 200,
|
|
|
|
|
headers: {},
|
|
|
|
|
options: [
|
|
|
|
|
'all',
|
|
|
|
|
'filter',
|
|
|
|
|
'search'
|
|
|
|
|
],
|
|
|
|
|
data: [
|
|
|
|
|
'action',
|
|
|
|
|
'meta'
|
|
|
|
|
],
|
|
|
|
|
validation: {
|
|
|
|
|
data: {
|
|
|
|
|
action: {
|
|
|
|
|
required: true,
|
|
|
|
|
values: ['unsubscribe', 'addLabel', 'removeLabel']
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'edit'
|
|
|
|
|
},
|
|
|
|
|
async query(frame) {
|
2021-10-01 13:08:55 +03:00
|
|
|
|
return membersService.api.members.bulkEdit(frame.data.bulk, frame.options);
|
2021-08-13 16:18:57 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-10-03 21:36:22 +03:00
|
|
|
|
exportCSV: {
|
2019-10-29 07:50:32 +03:00
|
|
|
|
options: [
|
2020-09-23 13:46:08 +03:00
|
|
|
|
'limit',
|
|
|
|
|
'filter',
|
2021-01-28 19:31:02 +03:00
|
|
|
|
'search'
|
2019-10-29 07:50:32 +03:00
|
|
|
|
],
|
2019-10-03 21:36:22 +03:00
|
|
|
|
headers: {
|
|
|
|
|
disposition: {
|
|
|
|
|
type: 'csv',
|
|
|
|
|
value() {
|
|
|
|
|
const datetime = (new Date()).toJSON().substring(0, 10);
|
|
|
|
|
return `members.${datetime}.csv`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
response: {
|
|
|
|
|
format: 'plain'
|
|
|
|
|
},
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
|
|
|
|
validation: {},
|
2020-01-15 13:52:47 +03:00
|
|
|
|
async query(frame) {
|
2020-08-12 16:17:44 +03:00
|
|
|
|
frame.options.withRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer'];
|
2021-06-23 12:18:15 +03:00
|
|
|
|
if (labsService.isSet('multipleProducts')) {
|
|
|
|
|
frame.options.withRelated.push('products');
|
|
|
|
|
}
|
2020-08-12 16:17:44 +03:00
|
|
|
|
const page = await membersService.api.members.list(frame.options);
|
|
|
|
|
|
2020-09-30 12:22:22 +03:00
|
|
|
|
return page;
|
2019-10-03 21:36:22 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2019-10-03 20:59:19 +03:00
|
|
|
|
importCSV: {
|
2020-12-09 19:15:53 +03:00
|
|
|
|
statusCode(result) {
|
|
|
|
|
if (result && result.meta && result.meta.stats && result.meta.stats.imported !== null) {
|
|
|
|
|
return 201;
|
|
|
|
|
} else {
|
|
|
|
|
return 202;
|
|
|
|
|
}
|
|
|
|
|
},
|
2019-10-03 20:59:19 +03:00
|
|
|
|
permissions: {
|
|
|
|
|
method: 'add'
|
|
|
|
|
},
|
|
|
|
|
async query(frame) {
|
2020-12-09 19:15:53 +03:00
|
|
|
|
const siteTimezone = settingsCache.get('timezone');
|
2020-08-06 04:58:32 +03:00
|
|
|
|
|
2020-12-09 19:15:53 +03:00
|
|
|
|
const importLabel = {
|
|
|
|
|
name: `Import ${moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm')}`
|
|
|
|
|
};
|
2020-08-06 04:58:32 +03:00
|
|
|
|
|
2020-12-09 19:15:53 +03:00
|
|
|
|
const globalLabels = [importLabel].concat(frame.data.labels);
|
|
|
|
|
const pathToCSV = frame.file.path;
|
|
|
|
|
const headerMapping = frame.data.mapping;
|
2020-08-25 10:23:05 +03:00
|
|
|
|
|
2021-07-23 15:58:35 +03:00
|
|
|
|
return membersService.processImport({
|
2021-01-14 04:05:22 +03:00
|
|
|
|
pathToCSV,
|
|
|
|
|
headerMapping,
|
|
|
|
|
globalLabels,
|
|
|
|
|
importLabel,
|
|
|
|
|
LabelModel: models.Label,
|
|
|
|
|
user: {
|
|
|
|
|
email: frame.user.get('email')
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-08-06 04:58:32 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-02-15 21:47:04 +03:00
|
|
|
|
memberStats: {
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
|
|
|
|
async query() {
|
|
|
|
|
const memberStats = await membersService.api.events.getStatuses();
|
|
|
|
|
let totalMembers = _.last(memberStats) ? (_.last(memberStats).paid + _.last(memberStats).free + _.last(memberStats).comped) : 0;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
resource: 'members',
|
|
|
|
|
total: totalMembers,
|
|
|
|
|
data: memberStats.map((d) => {
|
|
|
|
|
const {paid, free, comped} = d;
|
|
|
|
|
return {
|
|
|
|
|
date: moment(d.date).format('YYYY-MM-DD'),
|
|
|
|
|
paid, free, comped
|
|
|
|
|
};
|
2021-03-05 14:09:12 +03:00
|
|
|
|
})
|
2021-02-15 21:47:04 +03:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
mrrStats: {
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
|
|
|
|
async query() {
|
|
|
|
|
const mrrData = await membersService.api.events.getMRR();
|
|
|
|
|
const mrrStats = Object.keys(mrrData).map((curr) => {
|
|
|
|
|
return {
|
|
|
|
|
currency: curr,
|
|
|
|
|
data: mrrData[curr].map((d) => {
|
|
|
|
|
return Object.assign({}, {
|
|
|
|
|
date: moment(d.date).format('YYYY-MM-DD'),
|
|
|
|
|
value: d.mrr
|
|
|
|
|
});
|
2021-03-05 14:09:12 +03:00
|
|
|
|
})
|
2021-02-15 21:47:04 +03:00
|
|
|
|
};
|
|
|
|
|
});
|
2021-03-05 14:09:12 +03:00
|
|
|
|
return {
|
|
|
|
|
resource: 'mrr',
|
|
|
|
|
data: mrrStats
|
|
|
|
|
};
|
2021-02-15 21:47:04 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
subscriberStats: {
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
|
|
|
|
async query() {
|
|
|
|
|
const statsData = await membersService.api.events.getSubscriptions();
|
|
|
|
|
const totalSubscriptions = (_.last(statsData) && _.last(statsData).subscribed) || 0;
|
|
|
|
|
statsData.forEach((d) => {
|
|
|
|
|
d.date = moment(d.date).format('YYYY-MM-DD');
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
resource: 'subscribers',
|
|
|
|
|
total: totalSubscriptions,
|
|
|
|
|
data: statsData.map((d) => {
|
|
|
|
|
return Object.assign({}, {
|
|
|
|
|
date: moment(d.date).format('YYYY-MM-DD'),
|
|
|
|
|
value: d.subscribed
|
|
|
|
|
});
|
2021-03-05 14:09:12 +03:00
|
|
|
|
})
|
2021-02-15 21:47:04 +03:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
grossVolumeStats: {
|
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
|
|
|
|
async query() {
|
|
|
|
|
const volumeData = await membersService.api.events.getVolume();
|
|
|
|
|
const volumeStats = Object.keys(volumeData).map((curr) => {
|
|
|
|
|
return {
|
|
|
|
|
currency: curr,
|
|
|
|
|
data: volumeData[curr].map((d) => {
|
|
|
|
|
return Object.assign({}, {
|
|
|
|
|
date: moment(d.date).format('YYYY-MM-DD'),
|
|
|
|
|
value: d.volume
|
|
|
|
|
});
|
2021-03-05 14:09:12 +03:00
|
|
|
|
})
|
2021-02-15 21:47:04 +03:00
|
|
|
|
};
|
|
|
|
|
});
|
2021-03-05 14:09:12 +03:00
|
|
|
|
return {
|
|
|
|
|
resource: 'gross-volume',
|
|
|
|
|
data: volumeStats
|
|
|
|
|
};
|
2021-02-15 21:47:04 +03:00
|
|
|
|
}
|
2021-02-18 14:52:18 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
activityFeed: {
|
2021-02-23 15:37:38 +03:00
|
|
|
|
options: [
|
|
|
|
|
'limit'
|
|
|
|
|
],
|
2021-02-18 14:52:18 +03:00
|
|
|
|
permissions: {
|
|
|
|
|
method: 'browse'
|
|
|
|
|
},
|
2021-02-23 15:37:38 +03:00
|
|
|
|
async query(frame) {
|
|
|
|
|
const events = await membersService.api.events.getEventTimeline(frame.options);
|
2021-02-18 14:52:18 +03:00
|
|
|
|
return {
|
|
|
|
|
events
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-08-09 17:11:24 +03:00
|
|
|
|
}
|
|
|
|
|
};
|