plugins/SalesforceSC/Public/peoplestage/Invoke-UploadWithAccounts.ps1
function Invoke-UploadWithAccounts { [CmdletBinding()] param ( [Parameter(Mandatory=$false)][Hashtable] $InputHashtable ) begin { #----------------------------------------------- # START TIMER #----------------------------------------------- $processStart = [datetime]::now #$inserts = 0 #----------------------------------------------- # LOG #----------------------------------------------- $moduleName = "UPLOAD" # Start the log Write-Log -message $Script:logDivider Write-Log -message $moduleName -Severity INFO # Log the params, if existing Write-Log -message "INPUT:" if ( $InputHashtable ) { $InputHashtable.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $InputHashtable[$param] )'" -writeToHostToo $false } } #----------------------------------------------- # DEBUG MODE #----------------------------------------------- Write-Log "Debug Mode: $( $Script:debugMode )" #----------------------------------------------- # PARSE MESSAGE #----------------------------------------------- #$script:debug = $InputHashtable $uploadOnly = $false If ( "" -eq $InputHashtable.MessageName ) { $uploadOnly = $true $mailing = [Mailing]::new(999, "UploadOnly") } else { Write-Log "Parsing message: '$( $InputHashtable.MessageName )' with '$( $Script:settings.nameConcatChar )' as separator" $mailing = [Mailing]::new($InputHashtable.MessageName) Write-Log "Got chosen message entry with id '$( $mailing.mailingId )' and name '$( $mailing.mailingName )'" } #----------------------------------------------- # CHECK INPUT FILE #----------------------------------------------- # Checks input file automatically $file = Get-Item -Path $InputHashtable.Path Write-Log -Message "Got a file at $( $file.FullName )" # Add note in log file, that the file is a converted file if ( $file.FullName -match "\.converted$") { Write-Log -message "Be aware, that the exports are generated in Codepage 1252 and not UTF8. Please change this in the Channel Editor." -severity ( [LogSeverity]::WARNING ) } # Count the rows $rowsCount = 0 # if this needs to much performance, this is not needed If ( $Script:settings.upload.countRowsInputFile -eq $true ) { $rowsCount = Measure-Rows -Path $file.FullName -SkipFirstRow Write-Log -Message "Got a file with $( $rowsCount ) rows" } else { Write-Log -Message "RowCount of input file not activated" } #----------------------------------------------- # CHECK SALESFORCE CONNECTION #----------------------------------------------- try { #TODO Implement connection test } catch { Write-Log -Message $_.Exception -Severity ERROR throw [System.IO.InvalidDataException] $msg exit 0 } #----------------------------------------------- # CHECK INPUT PARAMETERS AND DATA #----------------------------------------------- If ( $Script:settings.upload.usePersonAccounts -eq $True ) { $fileHeaders = ( get-content -Encoding utf8 -TotalCount 1 -Path $file.FullName ) -split "`t" # When we have double quotes in the header, this will not work If ( $Script:settings.upload.personContactIdVariablename -ne "" -and $Script:settings.upload.personContactIdVariablename.length -gt 0 ) { # Check if the source variable is available If ( $fileHeaders -contains $Script:settings.upload.personContactIdVariablename ) { Write-Log -Severity VERBOSE -Message "The personContactIdVariablename variable '$( $Script:settings.upload.personContactIdVariablename )' is available. Proceeding..." } else { Write-Log -Severity ERROR -Message "The personContactIdVariablename variable '$( $Script:settings.upload.personContactIdVariablename )' is not present in the source file" throw "The personContactIdVariablename variable '$( $Script:settings.upload.personContactIdVariablename )' is not present in the source file" } } else { Write-Log -Severity ERROR -Message "You have to fill the personContactIdVariablename setting!" throw "You have to fill the personContactIdVariablename setting!" } If ( $Script:settings.upload.isPersonAccountVariablename -ne "" -and $Script:settings.upload.isPersonAccountVariablename.length -gt 0 ) { # Check if the source variable is available If ( $fileHeaders -contains $Script:settings.upload.isPersonAccountVariablename ) { Write-Log -Severity VERBOSE -Message "The isPersonAccountVariablename variable '$( $Script:settings.upload.isPersonAccountVariablename )' is available. Proceeding..." } else { Write-Log -Severity ERROR -Message "The isPersonAccountVariablename variable '$( $Script:settings.upload.isPersonAccountVariablename )' is not present in the source file" throw "The isPersonAccountVariablename variable '$( $Script:settings.upload.isPersonAccountVariablename )' is not present in the source file" } } else { Write-Log -Severity ERROR -Message "You have to fill the isPersonAccountVariablename setting!" throw "You have to fill the isPersonAccountVariablename setting!" } } #----------------------------------------------- # VARIABLES #----------------------------------------------- $successful = 0 $failed = 0 $processed = 0 } process { try { #----------------------------------------------- # CHECK CAMPAIGN #----------------------------------------------- $campaignId = $mailing.mailingId $campaign = @( Get-SFSCObjectData -Object "Campaign" -Fields "id", "name" -Where $Script:settings.upload.campaignFilter -limit 200 ) | where-object { $_.Id -like "*$( $campaignId )" } | Select-Object -first 1 Write-Log "Using salesforce campaign '$( $campaign.Name )' with id '$( $campaign.id )'" #----------------------------------------------- # CHECK SUBCAMPAIGNS #----------------------------------------------- If ( $Script:settings.upload.useDatedSubCampaigns -eq $True ) { $createSubCampaign = $True } else { # Load subcampaigns to the chosen one $subCampaignsTable = @( Get-SFSCObjectData -Object "Campaign" -Fields "id", "name" -Where "IsDeleted = false and Status = 'Planned' and ParentId = '$( $campaign.id )' and name like '%$( $Script:settings.upload.subCampaignIdentifier )%' order by LastModifiedDate desc" -limit 1000 ) # Regular expression to match the date format YYYYMMDD_HHMMSS $pattern = '\d{8}_\d{6}$' # Check if there are subcampaigns with the date in the name If ( $subCampaignsTable.Count -gt 0 ) { $foundSubCampaign = $False Write-Log "Found $( $subCampaignsTable.Count ) subcampaigns. Checking for ones without date in the name, using the first one" $subCampaignsTable | where-object { $_.Name -notmatch $pattern } | ForEach-Object { $sc = $_ Write-Log " Subcampaign '$( $sc.Name )' with id '$( $sc.id )' matches" If ( $foundSubCampaign -eq $False ) { $subCampaign = $sc Write-Log " Using subcampaign '$( $subCampaign.Name )' with id '$( $subCampaign.id )'" $foundSubCampaign = $True $createSubCampaign = $false } } If ( $foundSubCampaign -eq $False ) { Write-Log "No subcampaigns found" $createSubCampaign = $True } } else { Write-Log "No subcampaigns found" $createSubCampaign = $True } } #----------------------------------------------- # OUTPUT CURRENT API USAGE #----------------------------------------------- Write-Log "Current API Limit: $( $Script:variableCache.api_rate_limit )" #----------------------------------------------- # CHECK CAMPAIGN MEMBER STATUS #----------------------------------------------- $list = [MailingList]::new($InputHashtable.ListName) $listName = $list.mailingListName $listId = $list.mailingListId Write-Log "Using salesforce campaign member status '$( $listName )'" #----------------------------------------------- # TRANSFORM THE DATA #----------------------------------------------- # SF fields metadata $sfFields = Get-SFSCObjectField -Object "CampaignMember" | where-object { $_.createable -eq $True } $sfFieldsNames = $sfFields.Name # CSV fields metadata $urnFieldName = $InputHashtable.UrnFieldName $isPersonAccountVariablename = $Script:settings.upload.isPersonAccountVariablename $personContactIdVariablename = $Script:settings.upload.personContactIdVariablename $excludeColumns = $Script:settings.upload.reservedFields # Add a new subcampaign or use an existing one If ( $createSubCampaign -eq $True ) { If ( $Script:settings.upload.useDatedSubCampaigns -eq $True ) { # Create a new subcampaign with the current date $subCampaignName = "$( $campaign.Name ) - $( $Script:settings.upload.subCampaignIdentifier ) - $( [datetime]::now.ToString("yyyyMMdd_HHmmss") )" } else { # Create a new subcampaign without the current date $subCampaignName = "$( $campaign.Name ) - $( $Script:settings.upload.subCampaignIdentifier )" } # Set the campaignType If ( $InputHashtable.Keys -contains "CampaignType" ) { If ( $InputHashtable.CampaignType -ne "" ) { $campaignType = $InputHashtable.CampaignType } else { $campaignType = $Script:settings.upload.defaultCampaignType } } else { $campaignType = $Script:settings.upload.defaultCampaignType } # Add a new subcampaign $campaign = [PSCustomObject]@{ "Name" = $subCampaignName # TODO add a switch to append datetime or use an existing one "Type" = $campaignType "ParentId" = $campaign.id } $subCampaign = Add-SFSCObjectData -Object "Campaign" -Attributes $campaign } # Import the file and go through the lines $newCsv = [System.Collections.ArrayList]@() # This object are campaign members with contact id $c = 0 $skippedLines = 0 Import-csv -Delimiter "`t" -Path $file.FullName -Encoding UTF8 | ForEach-Object { $row = $_ If ( $newCsv.Count -eq 0 ) { $rowColumns = ( $row.psobject.properties | Where-Object { $_.name -notin $excludeColumns } ).name Write-Log "Got the row columns: '$( $rowColumns -join "', '" )'" $props = [System.Collections.ArrayList]@() ForEach ( $prop in $rowColumns ) { #$prop = $_ If ( $sfFieldsNames -contains $prop ) { [void]$props.Add($prop) } } } # Check the id/urn first, if it is Salesforce If ( ( Test-SalesforceId $row.$urnFieldName ) -eq $True ) { # Check if the contact is a person account If ( $row.$isPersonAccountVariablename -eq "True" -or $row.$isPersonAccountVariablename -eq "true" ) { # This is a person account $line = [Ordered]@{ "CampaignID" = $subCampaign.id "Status" = $list.mailingListId "AccountId" = "" "ContactId" = $row.$personContactIdVariablename } } else { # This is an account $line = [Ordered]@{ "CampaignID" = $subCampaign.id "Status" = $list.mailingListId "AccountId" = $row.$urnFieldName "ContactId" = "" } } ForEach ( $prop in $props ) { $line.Add( $prop, $row.$prop ) } [void]$newCsv.add([PSCustomObject]$line) } else { # Just skip this line $skippedLines += 1 } $c += 1 If ( $c % 10000 -eq 0 ) { Write-Log -Severity VERBOSE -Message "Checked $( $c ) lines" } } Write-Log "Stats after converting file" Write-Log " Checked $( $c ) lines in total" Write-Log " Converted $( $newCsv.count ) accounts lines" Write-Log " Skipped $( $skippedLines ) accounts lines" # TODO should this trigger an error? # Checking the campaigns stats # Write-Log -Severity VERBOSE -Message "Campaign summmary:" # $newCsv | where-object { $_.ContactId -ne "" } | group-object CampaignID | Sort-Object Count -Descending | ForEach-Object { # $c = $_ # Write-Log -Severity VERBOSE -Message " $( $c.Name ): $( $c.Count ) contacts" # } #----------------------------------------------- # WRITE THE DATA FILE #----------------------------------------------- # Create all files to upload Write-Log "Writing campaign member files" $campaignMemberFilesToUpload = [System.Collections.ArrayList]@() $batches = [math]::Ceiling( $newCsv.Count / $Script:settings.upload.uploadSize ) For ( $i = 0; $i -lt $batches; $i++ ) { $start = $i * $Script:settings.upload.uploadSize $end = $start + $Script:settings.upload.uploadSize -1 $nf = Join-Path -Path $Env:tmp -ChildPath "cm_$( $Script:processId )_$( $i ).csv" # TODO delete afterwards $cmCsvContent = $newCsv[$start..$end] | convertto-csv -NoTypeInformation -Delimiter "`t" # skipped the sorting by campaign id here as in other processes [IO.File]::WriteAllLines( $nf, $cmCsvContent ) [void]$campaignMemberFilesToUpload.Add( $nf ) Write-Log " Written file $( $i+1 ) to '$( $nf )'" } Write-Log "$( $campaignMemberFilesToUpload.Count ) CampaignMember files are written" Write-Log "Will do the upload in $( $batches ) batches with size of $( $Script:settings.upload.uploadSize )" #----------------------------------------------- # UPLOAD THE DATA #----------------------------------------------- $cmJobs = [System.Collections.ArrayList]@() For ( $j = 0; $j -lt $campaignMemberFilesToUpload.Count; $j++ ) { Write-Log "Starting with run $( $j ) and file '$( $campaignMemberFilesToUpload[$j] )'" $cmJobParams = [Hashtable]@{ "Object" = "CampaignMember" "Path" = $campaignMemberFilesToUpload[$j] "CheckSeconds" = $Script:settings.upload.checkSeconds # TOD0 [x] maybe put this into settings "MaxSecondsWait" = $Script:settings.upload.maximumWaitUntilJobFinished "DownloadFailures" = $Script:settings.upload.downloadFailedResults "FailureFilename" = ".\failed_$( $Script:processId )_$( [datetime]::now.toString("yyyyMMdd_HHmmss") )_$( $j ).csv" #"ExternalIdFieldName" = "ContactId" # This does not work ;-) } If ( $InputHashtable.operation -ne "" ) { Switch ( $InputHashtable.operation ) { # TODO implement more operations "delete" { $cmJobParams.Add( "Operation", "delete" ) } default { $cmJobParams.Add( "Operation", "insert" ) } } } [void]$cmJobs.Add((Add-BulkJob @cmJobParams)) } #----------------------------------------------- # CHECK THE RESULTS #----------------------------------------------- # Count all numbers together and log them $cmJobs | ForEach-Object { $j = $_ $successful += $j.successful $failed += $j.failed $processed += $j.processed Write-Log -Severity VERBOSE -Message "Job $( $j.jobid ): $( $j.processed ) processed, $( $j.successful ) successful, $( $j.failed ) failed " If ( $j.failed -gt 0 ) { $j.failureObj | Group-Object "sf__Error" | Sort-Object Count -Descending | ForEach-Object { $fail = $_ Write-Log -Severity WARNING -Message " $( $fail.Count ) Error '$( $fail.Name )'" } } } #----------------------------------------------- # CHECK IF IT SHOULD ERROR #----------------------------------------------- Write-Log "$( $processed ) total processed records" -severity INFO Write-Log "$( $failed ) total failed" -severity INFO If ( $processed -gt 0 ) { $errorRate = $failed / $processed * 100 If ( $errorRate -ge $Script:settings.upload.errorThreshold ) { throw "There has been a problem with $( $errorRate )% error rate. There are more than $( $Script:settings.upload.errorThreshold )% errors." } } } catch { Write-Log -Message "Trying to get the failures of last job" -Severity WARNING If ( $Script:variableCache.Keys -contains "last_jobid" ) { $failures = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $Script:variableCache.last_jobid )/failedResults" $failures | Group-Object "sf__Error" | Sort-Object Count -Descending | ForEach-Object { $fail = $_ Write-Log -Severity ERROR -Message " $( $fail.Count ) Error '$( $fail.Name )'" } } $msg = "Error during uploading data. Abort!" Write-Log -Message $msg -Severity ERROR -WriteToHostToo $false Write-Log -Message $_.Exception -Severity ERROR throw $_.Exception } finally { <# # Delete created files If ( Test-Path $nf ) { #Remove-item -Path $nf # TODO take that back in } If ( Test-Path $lf ) { #$lf } If ( Test-Path $successfulFilename ) { #$successfulFilename } #> #----------------------------------------------- # OUTPUT CURRENT API USAGE #----------------------------------------------- Write-Log "Current API Limit: $( $Script:variableCache.api_rate_limit )" #----------------------------------------------- # STOP TIMER #----------------------------------------------- $processEnd = [datetime]::now $processDuration = New-TimeSpan -Start $processStart -End $processEnd Write-Log -Message "Needed $( [int]$processDuration.TotalSeconds ) seconds in total" } #----------------------------------------------- # RETURN VALUES TO PEOPLESTAGE #----------------------------------------------- # count the number of successful upload rows $recipients = $successful # there could be multiple jobs per upload, so better using the guid here $transactionId = $Script:processId # return object $return = [Hashtable]@{ # Mandatory return values "Recipients"=$recipients "TransactionId"=$transactionId # General return value to identify this custom channel in the broadcasts detail tables "CustomProvider"= $Script:settings.providername "ProcessId" = $Script:processId # More information about the different status of the import "RecipientsProcessed" = $processed "RecipientsFailed" = $failed "RecipientsSuccessful" = $successful } # log the return object Write-Log -message "RETURN:" $return.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $return[$param] )'" -writeToHostToo $false } # return the results $return } end { } } |