2020-06-12 08:22:36 +03:00
import MemberImportError from 'ghost-admin/errors/member-import-error' ;
import Service , { inject as service } from '@ember/service' ;
import validator from 'validator' ;
2020-07-06 15:28:30 +03:00
import { isEmpty } from '@ember/utils' ;
2020-06-12 08:22:36 +03:00
export default Service . extend ( {
ajax : service ( ) ,
membersUtils : service ( ) ,
ghostPaths : service ( ) ,
async check ( data ) {
if ( ! data || ! data . length ) {
2020-07-06 15:28:30 +03:00
return {
validationErrors : [ new MemberImportError ( {
message : 'File is empty, nothing to import. Please select a different file.'
} ) ]
} ;
2020-06-12 08:22:36 +03:00
}
2020-07-06 15:28:30 +03:00
let sampledData = this . _sampleData ( data ) ;
let mapping = this . _detectDataTypes ( sampledData ) ;
2020-06-12 08:22:36 +03:00
2020-07-06 15:28:30 +03:00
let validationErrors = [ ] ;
2020-06-12 08:22:36 +03:00
2020-07-06 15:28:30 +03:00
const hasStripeIds = ! ! mapping . stripe _customer _id ;
const hasEmails = ! ! mapping . email ;
2020-06-12 08:22:36 +03:00
2020-07-06 15:28:30 +03:00
if ( ! hasEmails ) {
validationErrors . push ( new MemberImportError ( {
message : 'No email addresses found in provided data.'
} ) ) ;
2020-06-12 08:22:36 +03:00
} else {
2020-07-06 15:28:30 +03:00
// check can be done on whole set as it won't be too slow
const { invalidCount , emptyCount , duplicateCount } = this . _checkEmails ( data , mapping ) ;
if ( invalidCount ) {
validationErrors . push ( new MemberImportError ( {
message : ` Invalid email address ( ${ invalidCount } ) ` ,
type : 'warning'
} ) ) ;
}
2020-06-12 08:22:36 +03:00
2020-07-06 15:28:30 +03:00
if ( emptyCount ) {
validationErrors . push ( new MemberImportError ( {
message : ` Missing email address ( ${ emptyCount } ) ` ,
type : 'warning'
} ) ) ;
}
2020-06-12 08:22:36 +03:00
2020-07-06 15:28:30 +03:00
if ( duplicateCount ) {
validationErrors . push ( new MemberImportError ( {
message : ` Duplicate email address ( ${ duplicateCount } ) ` ,
type : 'warning'
} ) ) ;
}
}
if ( hasStripeIds ) {
2020-06-12 15:23:58 +03:00
// check can be done on whole set as it won't be too slow
2020-07-06 15:28:30 +03:00
const { totalCount , duplicateCount } = this . _checkStripeIds ( data , mapping ) ;
2020-06-12 15:23:58 +03:00
if ( ! this . membersUtils . isStripeEnabled ) {
2020-07-06 15:28:30 +03:00
validationErrors . push ( new MemberImportError ( {
message : 'Missing stripe connection' ,
context : ` ${ totalCount } Stripe customers won't be imported. You need to <a href="#/settings/labs">connect to Stripe</a> to import stripe customers. ` ,
type : 'warning'
} ) ) ;
2020-06-12 15:23:58 +03:00
} else {
2020-07-06 15:28:30 +03:00
let stripeSeverValidation = await this . _checkStripeServer ( sampledData , mapping ) ;
2020-06-12 08:22:36 +03:00
if ( stripeSeverValidation !== true ) {
2020-07-06 15:28:30 +03:00
validationErrors . push ( new MemberImportError ( {
message : 'Wrong Stripe account' ,
context : ` The CSV contains Stripe customers from a different Stripe account. These members will not be imported. Make sure you're connected to the correct <a href="#/settings/labs">Stripe account</a>. ` ,
type : 'warning'
} ) ) ;
2020-06-12 08:22:36 +03:00
}
}
2020-07-06 15:28:30 +03:00
if ( duplicateCount ) {
validationErrors . push ( new MemberImportError ( {
message : ` Duplicate Stripe ID ( ${ duplicateCount } ) ` ,
type : 'warning'
} ) ) ;
}
2020-06-12 08:22:36 +03:00
}
2020-07-06 15:28:30 +03:00
return { validationErrors , mapping } ;
} ,
/ * *
* Method implements foollowing sampling logic :
* Locate 10 non - empty cells from the start / middle ( ish ) / end of each column ( 30 non - empty values in total ) .
* If the data contains 30 rows or fewer , all rows should be validated .
*
* @ param { Array } data JSON objects mapped from CSV file
* /
_sampleData ( data , validationSampleSize = 30 ) {
let validatedSet = [ { } ] ;
if ( data && data . length > validationSampleSize ) {
let sampleKeys = Object . keys ( data [ 0 ] ) ;
sampleKeys . forEach ( function ( key ) {
const nonEmptyKeyEntries = data . filter ( entry => ! isEmpty ( entry [ key ] ) ) ;
let sampledEntries = [ ] ;
if ( nonEmptyKeyEntries . length <= validationSampleSize ) {
sampledEntries = nonEmptyKeyEntries ;
} else {
// take 3 equal parts from head, tail and middle of the data set
const partitionSize = validationSampleSize / 3 ;
const head = data . slice ( 0 , partitionSize ) ;
const tail = data . slice ( ( data . length - partitionSize ) , data . length ) ;
const middleIndex = Math . floor ( data . length / 2 ) ;
const middleStartIndex = middleIndex - 2 ;
const middleEndIndex = middleIndex + 3 ;
const middle = data . slice ( middleStartIndex , middleEndIndex ) ;
validatedSet . push ( ... head ) ;
validatedSet . push ( ... middle ) ;
validatedSet . push ( ... tail ) ;
}
sampledEntries . forEach ( ( entry , index ) => {
if ( ! validatedSet [ index ] ) {
validatedSet [ index ] = { } ;
}
validatedSet [ index ] [ key ] = entry [ key ] ;
} ) ;
} ) ;
2020-06-12 08:22:36 +03:00
} else {
2020-07-06 15:28:30 +03:00
validatedSet = data ;
2020-06-12 08:22:36 +03:00
}
2020-07-06 15:28:30 +03:00
return validatedSet ;
} ,
/ * *
* Detects validated data types :
* 1. email
* 2. stripe _customer _id
*
* Returned "mapping" object contains mappings that could be accepted by the API
* to map validated types .
* @ param { Array } data sampled data containing non empty values
* /
_detectDataTypes ( data ) {
let mapping = { } ;
let i = 0 ;
// loopping through all sampled data until needed data types are detected
while ( i <= ( data . length - 1 ) ) {
if ( mapping . email && mapping . stripe _customer _id ) {
break ;
}
let entry = data [ i ] ;
for ( const [ key , value ] of Object . entries ( entry ) ) {
if ( ! mapping . email && validator . isEmail ( value ) ) {
mapping . email = key ;
continue ;
}
if ( ! mapping . stripe _customer _id && value && value . startsWith && value . startsWith ( 'cus_' ) ) {
mapping . stripe _customer _id = key ;
continue ;
}
}
i += 1 ;
}
return mapping ;
2020-06-12 08:22:36 +03:00
} ,
_containsRecordsWithStripeId ( validatedSet ) {
let memberWithStripeId = validatedSet . find ( m => ! ! ( m . stripe _customer _id ) ) ;
return ! ! memberWithStripeId ;
} ,
2020-07-06 15:28:30 +03:00
_checkEmails ( validatedSet , mapping ) {
let emptyCount = 0 ;
let invalidCount = 0 ;
let duplicateCount = 0 ;
let emailMap = { } ;
2020-06-12 08:22:36 +03:00
validatedSet . forEach ( ( member ) => {
2020-07-06 15:28:30 +03:00
let emailValue = member [ mapping . email ] ;
if ( ! emailValue ) {
emptyCount += 1 ;
}
if ( emailValue && ! validator . isEmail ( emailValue ) ) {
invalidCount += 1 ;
} else if ( emailValue ) {
if ( emailMap [ emailValue ] ) {
emailMap [ emailValue ] += 1 ;
duplicateCount += 1 ;
} else {
emailMap [ emailValue ] = 1 ;
}
2020-06-12 08:22:36 +03:00
}
2020-07-06 15:28:30 +03:00
} ) ;
2020-06-12 08:22:36 +03:00
2020-07-06 15:28:30 +03:00
return { invalidCount , emptyCount , duplicateCount } ;
} ,
_countStripeRecors ( validatedSet , mapping ) {
let count = 0 ;
validatedSet . forEach ( ( member ) => {
if ( ! isEmpty ( member [ mapping . stripe _customer _id ] ) ) {
count += 1 ;
2020-06-12 08:22:36 +03:00
}
} ) ;
2020-07-06 15:28:30 +03:00
return count ;
2020-06-12 08:22:36 +03:00
} ,
2020-07-06 15:28:30 +03:00
_checkStripeIds ( validatedSet , mapping ) {
let totalCount = 0 ;
let duplicateCount = 0 ;
validatedSet . reduce ( ( acc , member ) => {
let stripeCustomerIdValue = member [ mapping . stripe _customer _id ] ;
if ( stripeCustomerIdValue && stripeCustomerIdValue !== 'undefined' ) {
totalCount += 1 ;
if ( acc [ stripeCustomerIdValue ] ) {
acc [ stripeCustomerIdValue ] += 1 ;
duplicateCount += 1 ;
2020-06-12 15:23:58 +03:00
} else {
2020-07-06 15:28:30 +03:00
acc [ stripeCustomerIdValue ] = 1 ;
2020-06-12 15:23:58 +03:00
}
}
return acc ;
} , { } ) ;
2020-07-06 15:28:30 +03:00
return { totalCount , duplicateCount } ;
2020-06-12 15:23:58 +03:00
} ,
_checkContainsStripeIDs ( validatedSet ) {
2020-06-12 08:22:36 +03:00
let result = true ;
2020-06-12 09:28:15 +03:00
if ( ! this . membersUtils . isStripeEnabled ) {
2020-06-12 08:22:36 +03:00
validatedSet . forEach ( ( member ) => {
if ( member . stripe _customer _id ) {
result = false ;
}
} ) ;
}
return result ;
} ,
2020-07-06 15:28:30 +03:00
async _checkStripeServer ( validatedSet , mapping ) {
2020-06-12 14:36:00 +03:00
const url = this . ghostPaths . get ( 'url' ) . api ( 'members/upload/validate' ) ;
2020-07-06 15:28:30 +03:00
const mappedValidatedSet = validatedSet . map ( ( entry ) => {
return {
stripe _customer _id : entry [ mapping . stripe _customer _id ]
} ;
} ) ;
2020-06-12 08:22:36 +03:00
let response ;
try {
response = await this . ajax . post ( url , {
data : {
2020-07-06 15:28:30 +03:00
members : mappedValidatedSet
2020-06-12 08:22:36 +03:00
}
} ) ;
} catch ( e ) {
return false ;
}
if ( response . errors ) {
return false ;
}
return true ;
}
} ) ;