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' ) ;
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' ) ;
2020-05-22 21:22:20 +03:00
const { i18n , logging } = require ( '../../lib/common' ) ;
const errors = require ( '@tryghost/errors' ) ;
2019-10-03 20:59:19 +03:00
const fsLib = require ( '../../lib/fs' ) ;
2020-02-14 12:33:10 +03:00
const _ = require ( 'lodash' ) ;
2019-08-09 17:11:24 +03:00
2020-01-28 07:25:00 +03:00
const decorateWithSubscriptions = async function ( member ) {
2020-01-15 13:52:47 +03:00
// NOTE: this logic is here until relations between Members/MemberStripeCustomer/StripeCustomerSubscription
// are in place
2020-01-28 07:25:00 +03:00
const subscriptions = await membersService . api . members . getStripeSubscriptions ( member ) ;
2020-01-15 13:52:47 +03:00
2020-01-28 07:25:00 +03:00
return Object . assign ( member , {
stripe : {
subscriptions
}
} ) ;
} ;
2020-02-12 08:33:16 +03:00
/ * * N O T E : t h i s m e t h o d s h o u l d n o t e x i s t a t a l l a n d n e e d s t o b e c l e a n e d u p
it was created due to a bug in how CSV is currently created for exports
Export bug was fixed in 3.6 but method exists to handle older csv exports with undefined
* * /
2020-02-04 08:51:24 +03:00
const cleanupUndefined = ( obj ) => {
for ( let key in obj ) {
if ( obj [ key ] === 'undefined' ) {
delete obj [ key ] ;
}
}
} ;
// NOTE: this method can be removed once unique constraints are introduced ref.: https://github.com/TryGhost/Ghost/blob/e277c6b/core/server/data/schema/schema.js#L339
const sanitizeInput = ( members ) => {
const customersMap = members . reduce ( ( acc , member ) => {
2020-02-12 08:33:16 +03:00
if ( member . stripe _customer _id && member . stripe _customer _id !== 'undefined' ) {
2020-02-04 08:51:24 +03:00
if ( acc [ member . stripe _customer _id ] ) {
acc [ member . stripe _customer _id ] += 1 ;
} else {
acc [ member . stripe _customer _id ] = 1 ;
}
}
return acc ;
} , { } ) ;
const toRemove = [ ] ;
for ( const key in customersMap ) {
if ( customersMap [ key ] > 1 ) {
toRemove . push ( key ) ;
}
}
let sanitized = members . filter ( ( member ) => {
return ! ( toRemove . includes ( member . stripe _customer _id ) ) ;
} ) ;
return sanitized ;
} ;
2020-02-14 12:33:10 +03:00
function serializeMemberLabels ( labels ) {
if ( labels ) {
return labels . filter ( ( label ) => {
return ! ! label ;
} ) . map ( ( label ) => {
if ( _ . isString ( label ) ) {
return {
name : label . trim ( )
} ;
}
return label ;
} ) ;
}
return [ ] ;
}
2020-01-28 07:25:00 +03:00
const listMembers = async function ( options ) {
const res = ( await models . Member . findPage ( options ) ) ;
const memberModels = res . data . map ( model => model . toJSON ( options ) ) ;
const members = await Promise . all ( memberModels . map ( async function ( member ) {
return decorateWithSubscriptions ( member ) ;
2020-01-15 13:52:47 +03:00
} ) ) ;
return {
2020-01-28 07:25:00 +03:00
members : members ,
2020-01-15 13:52:47 +03:00
meta : res . meta
} ;
} ;
2019-08-09 17:11:24 +03:00
const members = {
docName : 'members' ,
browse : {
options : [
'limit' ,
'fields' ,
'filter' ,
'order' ,
'debug' ,
'page'
] ,
permissions : true ,
validation : { } ,
2020-01-15 13:52:47 +03:00
async query ( frame ) {
return listMembers ( frame . options ) ;
2019-08-09 17:11:24 +03:00
}
} ,
read : {
headers : { } ,
data : [
'id' ,
'email'
] ,
validation : { } ,
permissions : true ,
2019-09-03 07:10:32 +03:00
async query ( frame ) {
2020-01-28 07:25:00 +03:00
let model = await models . Member . findOne ( frame . data , frame . options ) ;
2020-01-15 13:52:47 +03:00
2020-01-28 07:25:00 +03:00
if ( ! model ) {
2020-05-22 21:22:20 +03:00
throw new errors . NotFoundError ( {
message : i18n . t ( 'errors.api.members.memberNotFound' )
2019-09-03 07:10:32 +03:00
} ) ;
}
2020-01-15 13:52:47 +03:00
2020-01-28 07:25:00 +03:00
const member = model . toJSON ( frame . options ) ;
2020-01-15 13:52:47 +03:00
2020-01-28 07:25:00 +03:00
return decorateWithSubscriptions ( 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-02-04 08:51:24 +03:00
let model ;
2019-10-09 10:14:26 +03:00
try {
2020-02-04 08:51:24 +03:00
model = await models . Member . add ( frame . data . members [ 0 ] , frame . options ) ;
const member = model . toJSON ( frame . options ) ;
if ( frame . data . members [ 0 ] . stripe _customer _id ) {
await membersService . api . members . linkStripeCustomer ( frame . data . members [ 0 ] . stripe _customer _id , member ) ;
}
if ( frame . data . members [ 0 ] . comped ) {
await membersService . api . members . setComplimentarySubscription ( member ) ;
}
2020-01-15 13:52:47 +03:00
if ( frame . options . send _email ) {
2020-02-14 12:33:10 +03:00
await membersService . api . sendEmailWithMagicLink ( { email : model . get ( 'email' ) , requestedType : frame . options . email _type } ) ;
2020-01-15 13:52:47 +03:00
}
2020-01-28 07:25:00 +03:00
return decorateWithSubscriptions ( member ) ;
2019-10-09 10:14:26 +03:00
} catch ( error ) {
if ( error . code && error . message . toLowerCase ( ) . indexOf ( 'unique' ) !== - 1 ) {
2020-05-22 21:22:20 +03:00
throw new errors . ValidationError ( { message : i18n . t ( 'errors.api.members.memberAlreadyExists' ) } ) ;
2019-10-09 10:14:26 +03:00
}
2020-02-04 08:51:24 +03:00
// NOTE: failed to link Stripe customer/plan/subscription
if ( model && error . message && ( error . message . indexOf ( 'customer' ) || error . message . indexOf ( 'plan' ) || error . message . indexOf ( 'subscription' ) ) ) {
const api = require ( './index' ) ;
await api . members . destroy . query ( {
options : {
context : frame . options . context ,
id : model . id
}
} ) ;
}
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 ) {
2020-01-28 07:25:00 +03:00
const model = await models . Member . edit ( frame . data . members [ 0 ] , frame . options ) ;
const member = model . toJSON ( frame . options ) ;
const subscriptions = await membersService . api . members . getStripeSubscriptions ( member ) ;
const compedSubscriptions = subscriptions . filter ( sub => ( sub . plan . nickname === 'Complimentary' ) ) ;
if ( frame . data . members [ 0 ] . comped !== undefined && ( frame . data . members [ 0 ] . comped !== compedSubscriptions ) ) {
const hasCompedSubscription = ! ! ( compedSubscriptions . length ) ;
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-01-15 13:52:47 +03:00
2020-01-28 07:25:00 +03:00
return decorateWithSubscriptions ( member ) ;
2019-10-03 14:38:22 +03:00
}
} ,
2019-08-09 17:11:24 +03:00
destroy : {
statusCode : 204 ,
headers : { } ,
options : [
'id'
] ,
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-01-15 13:52:47 +03:00
2020-02-11 11:35:18 +03:00
let member = await models . Member . findOne ( frame . options ) ;
2020-01-15 13:52:47 +03:00
if ( ! member ) {
2020-05-22 21:22:20 +03:00
throw new errors . NotFoundError ( {
message : i18n . t ( 'errors.api.resource.resourceNotFound' , {
2020-01-15 13:52:47 +03:00
resource : 'Member'
} )
} ) ;
}
// NOTE: move to a model layer once Members/MemberStripeCustomer relations are in place
await membersService . api . members . destroyStripeSubscriptions ( member ) ;
await models . Member . destroy ( frame . options )
2019-12-06 08:04:10 +03:00
. catch ( models . Member . NotFoundError , ( ) => {
2020-05-22 21:22:20 +03:00
throw new errors . NotFoundError ( {
message : i18n . t ( 'errors.api.resource.resourceNotFound' , {
2019-12-06 08:04:10 +03:00
resource : 'Member'
} )
} ) ;
} ) ;
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
} ,
2019-10-03 21:36:22 +03:00
exportCSV : {
2019-10-29 07:50:32 +03:00
options : [
'limit'
] ,
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-02-14 12:33:10 +03:00
frame . options . withRelated = [ 'labels' ] ;
2020-01-15 13:52:47 +03:00
return listMembers ( frame . options ) ;
2019-10-03 21:36:22 +03:00
}
} ,
2019-10-03 20:59:19 +03:00
importCSV : {
statusCode : 201 ,
permissions : {
method : 'add'
} ,
async query ( frame ) {
2020-02-04 08:51:24 +03:00
let filePath = frame . file . path ;
let fulfilled = 0 ;
let invalid = 0 ;
let duplicates = 0 ;
const columnsToExtract = [ {
name : 'email' ,
lookup : /^email/i
} , {
name : 'name' ,
lookup : /name/i
} , {
name : 'note' ,
lookup : /note/i
} , {
name : 'subscribed_to_emails' ,
lookup : /subscribed_to_emails/i
} , {
name : 'stripe_customer_id' ,
lookup : /stripe_customer_id/i
} , {
name : 'complimentary_plan' ,
lookup : /complimentary_plan/i
2020-02-14 12:33:10 +03:00
} , {
name : 'labels' ,
lookup : /labels/i
2020-02-19 14:55:09 +03:00
} , {
name : 'created_at' ,
lookup : /created_at/i
2020-02-04 08:51:24 +03:00
} ] ;
2019-10-03 20:59:19 +03:00
return fsLib . readCSV ( {
path : filePath ,
2020-02-04 08:51:24 +03:00
columnsToExtract : columnsToExtract
2019-10-03 20:59:19 +03:00
} ) . then ( ( result ) => {
2020-02-04 08:51:24 +03:00
const sanitized = sanitizeInput ( result ) ;
invalid += result . length - sanitized . length ;
2020-02-10 11:03:08 +03:00
return Promise . map ( sanitized , ( ( entry ) => {
2019-10-03 20:59:19 +03:00
const api = require ( './index' ) ;
2020-02-14 12:33:10 +03:00
entry . labels = ( entry . labels && entry . labels . split ( ',' ) ) || [ ] ;
const entryLabels = serializeMemberLabels ( entry . labels ) ;
2020-02-04 08:51:24 +03:00
cleanupUndefined ( entry ) ;
2020-02-18 06:34:20 +03:00
let subscribed ;
if ( _ . isUndefined ( entry . subscribed _to _emails ) ) {
subscribed = entry . subscribed _to _emails ;
} else {
subscribed = ( String ( entry . subscribed _to _emails ) . toLowerCase ( ) !== 'false' ) ;
}
2019-10-09 10:14:26 +03:00
return Promise . resolve ( api . members . add . query ( {
2019-10-03 20:59:19 +03:00
data : {
members : [ {
email : entry . email ,
2019-10-10 07:51:27 +03:00
name : entry . name ,
2020-02-04 08:51:24 +03:00
note : entry . note ,
2020-02-18 06:34:20 +03:00
subscribed : subscribed ,
2020-02-04 08:51:24 +03:00
stripe _customer _id : entry . stripe _customer _id ,
2020-02-14 12:33:10 +03:00
comped : ( String ( entry . complimentary _plan ) . toLocaleLowerCase ( ) === 'true' ) ,
2020-02-19 14:55:09 +03:00
labels : entryLabels ,
2020-03-09 15:12:02 +03:00
created _at : entry . created _at === '' ? undefined : entry . created _at
2019-10-03 20:59:19 +03:00
} ]
} ,
options : {
context : frame . options . context ,
options : { send _email : false }
}
2019-10-09 10:14:26 +03:00
} ) ) . reflect ( ) ;
2020-02-10 11:03:08 +03:00
} ) , { concurrency : 10 } )
. each ( ( inspection ) => {
if ( inspection . isFulfilled ( ) ) {
fulfilled = fulfilled + 1 ;
2019-10-03 20:59:19 +03:00
} else {
2020-05-22 21:22:20 +03:00
if ( inspection . reason ( ) instanceof errors . ValidationError ) {
2020-02-10 11:03:08 +03:00
duplicates = duplicates + 1 ;
} else {
// NOTE: if the error happens as a result of pure API call it doesn't get logged anywhere
// for this reason we have to make sure any unexpected errors are logged here
2020-02-10 11:25:56 +03:00
if ( Array . isArray ( inspection . reason ( ) ) ) {
2020-05-22 21:22:20 +03:00
logging . error ( inspection . reason ( ) [ 0 ] ) ;
2020-02-10 11:25:56 +03:00
} else {
2020-05-22 21:22:20 +03:00
logging . error ( inspection . reason ( ) ) ;
2020-02-10 11:25:56 +03:00
}
2020-02-10 11:03:08 +03:00
invalid = invalid + 1 ;
}
2019-10-03 20:59:19 +03:00
}
2020-02-10 11:03:08 +03:00
} ) ;
2019-10-03 20:59:19 +03:00
} ) . then ( ( ) => {
return {
meta : {
stats : {
imported : fulfilled ,
duplicates : duplicates ,
invalid : invalid
}
}
} ;
} ) ;
}
2019-08-09 17:11:24 +03:00
}
} ;
module . exports = members ;