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' ) ;
2019-09-03 07:10:32 +03:00
const common = require ( '../../lib/common' ) ;
2019-10-03 20:59:19 +03:00
const fsLib = require ( '../../lib/fs' ) ;
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-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 ) => {
if ( member . stripe _customer _id ) {
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-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 ) {
2019-09-03 07:10:32 +03:00
throw new common . errors . NotFoundError ( {
message : common . i18n . t ( 'errors.api.members.memberNotFound' )
} ) ;
}
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-01-28 07:25:00 +03:00
await membersService . api . sendEmailWithMagicLink ( model . get ( 'email' ) , 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 ) {
throw new common . errors . ValidationError ( { message : common . i18n . t ( 'errors.api.members.memberAlreadyExists' ) } ) ;
}
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
let member = await models . Member . findOne ( frame . data , frame . options ) ;
if ( ! member ) {
throw new common . errors . NotFoundError ( {
message : common . i18n . t ( 'errors.api.resource.resourceNotFound' , {
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 , ( ) => {
throw new common . errors . NotFoundError ( {
message : common . i18n . t ( 'errors.api.resource.resourceNotFound' , {
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 ) {
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
} ] ;
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 ;
return Promise . all ( sanitized . map ( ( entry ) => {
2019-10-03 20:59:19 +03:00
const api = require ( './index' ) ;
2020-02-04 08:51:24 +03:00
cleanupUndefined ( entry ) ;
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 ,
subscribed : ( String ( entry . subscribed _to _emails ) . toLowerCase ( ) === 'true' ) ,
stripe _customer _id : entry . stripe _customer _id ,
comped : ( String ( entry . complimentary _plan ) . toLocaleLowerCase ( ) === 'true' )
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 ( ) ;
2019-10-03 20:59:19 +03:00
} ) ) . each ( ( inspection ) => {
if ( inspection . isFulfilled ( ) ) {
fulfilled = fulfilled + 1 ;
} else {
if ( inspection . reason ( ) instanceof common . errors . ValidationError ) {
duplicates = duplicates + 1 ;
} else {
invalid = invalid + 1 ;
}
}
} ) ;
} ) . then ( ( ) => {
return {
meta : {
stats : {
imported : fulfilled ,
duplicates : duplicates ,
invalid : invalid
}
}
} ;
} ) ;
}
2019-08-09 17:11:24 +03:00
}
} ;
module . exports = members ;