2026-07-05 19:18:26 -05:00
# Core upload engine - called by run.ps1 with an entity directory path
2026-06-29 20:34:33 -05:00
param (
[ Parameter ( Mandatory = $true )]
[ string ] $EntityDir
)
$scriptRoot = Split-Path -Parent $PSCommandPath
$winscpDll = Join-Path $scriptRoot "WinSCP\WinSCPnet.dll"
$today = Get-Date -Format "yyyyMMdd"
2026-07-05 19:18:26 -05:00
# -- Logging setup ------------------------------------------------------------
2026-06-29 20:43:12 -05:00
$logsDir = Join-Path $scriptRoot "Logs"
if ( -not ( Test-Path $logsDir )) { New-Item -ItemType Directory -Path $logsDir | Out-Null }
$logFile = Join-Path $logsDir " $today .log"
$settingsFile = Join-Path $scriptRoot "settings.json"
$logLevel = "normal"
if ( Test-Path $settingsFile ) {
$settings = Get-Content $settingsFile -Raw | ConvertFrom-Json
if ( $settings . log_level ) { $logLevel = $settings . log_level . ToLower () }
}
function Write-Log($level , $message ) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[ $ts ] [ $( $level . ToUpper ()) ] $message "
Add-Content -Path $logFile -Value $line
if ( $level -eq "error" ) {
Write-Host $line -ForegroundColor Red
} elseif ( $logLevel -eq "debug" ) {
Write-Host $line -ForegroundColor DarkGray
}
}
function Write-Info($message ) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[ $ts ] [INFO ] $message "
Add-Content -Path $logFile -Value $line
}
function Write-Debug($message ) {
if ( $logLevel -eq "debug" ) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[ $ts ] [DEBUG] $message "
Add-Content -Path $logFile -Value $line
}
}
2026-07-05 19:18:26 -05:00
# -- WinSCP -------------------------------------------------------------------
2026-06-29 20:34:33 -05:00
if ( -not ( Test-Path $winscpDll )) {
Write-Host "ERROR: WinSCP not found. Run setup.bat first." -ForegroundColor Red
2026-06-29 20:43:12 -05:00
Write-Log "error" "WinSCP DLL not found at $winscpDll "
2026-06-29 20:34:33 -05:00
exit 1
}
2026-07-05 19:53:36 -05:00
# Unblock every run, regardless of how the WinSCP folder got here - Windows
# tags files as untrusted ("Mark of the Web") if they were ever downloaded or
# copied from a network share/zip, which blocks Add-Type from loading them.
Get-ChildItem -Path ( Join-Path $scriptRoot "WinSCP" ) -Recurse -File |
Unblock-File -ErrorAction SilentlyContinue
try {
Add-Type -Path $winscpDll
} catch {
Write-Host "ERROR: Could not load WinSCPnet.dll - $( $_ . Exception . Message ) " -ForegroundColor Red
Write-Host "Try deleting the WinSCP\ folder and running setup.bat again." -ForegroundColor Yellow
Write-Log "error" "Add-Type failed for $winscpDll : $( $_ . Exception . Message ) "
exit 1
}
2026-06-29 20:34:33 -05:00
2026-07-05 19:18:26 -05:00
# -- Entity config -------------------------------------------------------------
2026-06-29 20:34:33 -05:00
$entityConfigFile = Join-Path $EntityDir "entity.json"
if ( -not ( Test-Path $entityConfigFile )) {
Write-Host "ERROR: entity.json not found in $EntityDir " -ForegroundColor Red
2026-06-29 20:43:12 -05:00
Write-Log "error" "entity.json not found in $EntityDir "
2026-06-29 20:34:33 -05:00
exit 1
}
$entityConfig = Get-Content $entityConfigFile -Raw | ConvertFrom-Json
2026-07-05 20:19:34 -05:00
# Add any missing keys to entity.json as null so they never have to be typed
# by hand (a null value means "not used - ignore"). Writes the file back only
# when something was actually added.
$configTemplate = [ ordered ] @ {
workflow = $null
insurance_file_type = $null
ftps = [ ordered ] @ { host = $null ; port = $null ; tls = $null ; username = $null ; password = $null ; tiff_path = $null ; pdf_path = $null }
sftp = [ ordered ] @ { host = $null ; port = $null ; username = $null ; password = $null ; patient_path = $null }
}
$addedKeys = @ ()
foreach ( $topKey in $configTemplate . Keys ) {
if ( $null -eq $entityConfig . PSObject . Properties [ $topKey ]) {
$value = if ( $configTemplate [ $topKey ] -is [ System.Collections.Specialized.OrderedDictionary ]) {
[ PSCustomObject ] $configTemplate [ $topKey ]
} else { $configTemplate [ $topKey ] }
$entityConfig | Add-Member -NotePropertyName $topKey -NotePropertyValue $value
$addedKeys += $topKey
} elseif ( $configTemplate [ $topKey ] -is [ System.Collections.Specialized.OrderedDictionary ]) {
foreach ( $subKey in $configTemplate [ $topKey ]. Keys ) {
if ( $null -eq $entityConfig . $topKey . PSObject . Properties [ $subKey ]) {
$entityConfig . $topKey | Add-Member -NotePropertyName $subKey -NotePropertyValue $null
$addedKeys += " $topKey . $subKey "
}
}
}
}
if ( $addedKeys . Count -gt 0 ) {
$entityConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $entityConfigFile -Encoding UTF8
Write-Host "Added missing key(s) to entity.json (as null = ignored): $( $addedKeys -join ', ' ) " -ForegroundColor Yellow
Write-Info "CONFIG added missing keys to entity.json: $( $addedKeys -join ',' ) "
}
2026-06-29 20:34:33 -05:00
$workflow = $entityConfig . workflow
2026-07-05 19:45:53 -05:00
$insuranceExt = if ( $entityConfig . insurance_file_type ) { $entityConfig . insurance_file_type . TrimStart ( "." ) } else { "pdf" }
2026-06-29 20:34:33 -05:00
$export1 = Join-Path $EntityDir "Export1"
$entityName = Split-Path -Leaf $EntityDir
if ( -not ( Test-Path $export1 )) {
Write-Host "ERROR: Export1 folder not found in $EntityDir " -ForegroundColor Red
2026-06-29 20:43:12 -05:00
Write-Log "error" "Export1 not found in $EntityDir "
2026-06-29 20:34:33 -05:00
exit 1
}
Write-Host ""
2026-07-05 19:59:36 -05:00
Write-Host "Entity: $entityName | Workflow: $workflow | File type: . $insuranceExt | Date: $today | Log: $logLevel " -ForegroundColor Cyan
2026-06-29 20:34:33 -05:00
Write-Host ( "=" * 60 )
2026-07-05 19:59:36 -05:00
Write-Info "=== START entity= $entityName workflow= $workflow filetype= $insuranceExt ==="
2026-06-29 20:34:33 -05:00
2026-07-05 19:18:26 -05:00
# -- Helpers ------------------------------------------------------------------
2026-06-29 20:34:33 -05:00
function Merge-Config($base , $override ) {
if ( -not $override ) { return $base }
2026-06-29 20:43:12 -05:00
$merged = $base | ConvertTo-Json -Depth 10 | ConvertFrom-Json
2026-06-29 20:34:33 -05:00
foreach ( $section in $override . PSObject . Properties ) {
2026-07-05 20:10:04 -05:00
if ( $section . Name -like "_*" ) { continue }
$baseSection = $merged . PSObject . Properties [ $section . Name ]
if ( $null -eq $baseSection ) {
$merged | Add-Member -NotePropertyName $section . Name -NotePropertyValue $section . Value
} elseif ( $section . Value -is [ System.Management.Automation.PSCustomObject ]) {
2026-06-29 20:34:33 -05:00
foreach ( $prop in $section . Value . PSObject . Properties ) {
2026-07-05 20:10:04 -05:00
$merged . $ ( $section . Name ) | Add-Member -NotePropertyName $prop . Name -NotePropertyValue $prop . Value -Force
2026-06-29 20:34:33 -05:00
}
2026-07-05 20:10:04 -05:00
} else {
$merged . $ ( $section . Name ) = $section . Value
2026-06-29 20:34:33 -05:00
}
}
return $merged
}
function Expand-Path($template , $practice ) {
return $template -replace '\{practice\}' , $practice
}
2026-07-05 20:05:31 -05:00
function Get-MissingFields($section , $sectionName , $fieldNames ) {
$missing = @ ()
foreach ( $f in $fieldNames ) {
if ( -not $section -or [ string ]:: IsNullOrWhiteSpace ([ string ] $section . $f )) {
$missing += " $sectionName . $f "
}
}
return $missing
}
2026-06-29 20:34:33 -05:00
function Get-ArchiveDir($parent , $name ) {
$found = Get-ChildItem -Path $parent -Directory |
2026-06-29 20:43:12 -05:00
Where-Object { $_ . Name -ieq $name } | Select-Object -First 1
2026-06-29 20:34:33 -05:00
if ( $found ) { return $found . FullName }
$path = Join-Path $parent $name
New-Item -ItemType Directory -Path $path | Out-Null
return $path
}
2026-07-05 20:33:01 -05:00
# Host values may be pasted as URLs, e.g. "SFTP://server.com/" - pull out the
# scheme (decides the protocol) and the bare hostname.
function Get-HostInfo($rawHost ) {
$h = ([ string ] $rawHost ). Trim ()
$scheme = $null
if ( $h -imatch '^(\w+)://' ) {
$scheme = $Matches [ 1 ]. ToLower ()
$h = $h -replace '^\w+://' , ''
}
$h = $h . Split ( '/' )[ 0 ]
return @ { Scheme = $scheme ; HostName = $h }
}
2026-07-05 19:18:26 -05:00
# -- Upload via FTPS -----------------------------------------------------------
2026-06-29 20:43:12 -05:00
function Invoke-FTPSUpload($ftpConfig , $files , $remotePath , $entity , $practice ) {
2026-07-05 20:33:01 -05:00
$hostInfo = Get-HostInfo $ftpConfig . host
2026-06-29 20:34:33 -05:00
$opts = New-Object WinSCP . SessionOptions
2026-07-05 20:33:01 -05:00
if ( $hostInfo . Scheme -eq "sftp" ) {
$opts . Protocol = [ WinSCP.Protocol ]:: Sftp
$opts . PortNumber = if ( $ftpConfig . port ) { [ int ] $ftpConfig . port } else { 22 }
$opts . GiveUpSecurityAndAcceptAnySshHostKey = $true
} else {
$useTls = ( $ftpConfig . tls -eq $true ) -or ( $hostInfo . Scheme -eq "ftps" )
$opts . Protocol = [ WinSCP.Protocol ]:: Ftp
$opts . FtpSecure = if ( $useTls ) { [ WinSCP.FtpSecure ]:: Explicit } else { [ WinSCP.FtpSecure ]:: None }
$opts . PortNumber = if ( $ftpConfig . port ) { [ int ] $ftpConfig . port } else { 21 }
if ( $useTls ) { $opts . GiveUpSecurityAndAcceptAnyTlsHostCertificate = $true }
}
$opts . HostName = $hostInfo . HostName
2026-06-29 20:34:33 -05:00
$opts . UserName = $ftpConfig . username
$opts . Password = $ftpConfig . password
$session = New-Object WinSCP . Session
2026-06-29 20:43:12 -05:00
if ( $logLevel -eq "debug" ) {
$session . SessionLogPath = Join-Path $logsDir " ${today} _winscp_debug.log"
}
2026-06-29 20:34:33 -05:00
$ok = 0 ; $fail = 0
try {
2026-07-05 20:33:01 -05:00
Write-Debug "Connect: $( $opts . Protocol ) $( $hostInfo . HostName ) : $( $opts . PortNumber ) user= $( $ftpConfig . username ) "
2026-06-29 20:34:33 -05:00
$session . Open ( $opts )
foreach ( $file in $files ) {
$remote = $remotePath . TrimEnd ( "/" ) + "/" + $file . Name
try {
$result = $session . PutFiles ( $file . FullName , $remote )
$result . Check ()
Write-Host " $( $file . Name ) ... ok" -ForegroundColor Green
2026-06-29 20:43:12 -05:00
Write-Info "FTPS OK entity= $entity practice= $practice file= $( $file . Name ) remote= $remote "
2026-06-29 20:34:33 -05:00
$ok ++
} catch {
2026-06-29 20:43:12 -05:00
$err = $_ . Exception . Message
Write-Host " $( $file . Name ) ... FAILED" -ForegroundColor Red
Write-Log "error" "FTPS FAIL entity= $entity practice= $practice file= $( $file . Name ) remote= $remote error= $err "
2026-06-29 20:34:33 -05:00
$fail ++
}
}
2026-06-29 20:43:12 -05:00
} catch {
$err = $_ . Exception . Message
Write-Log "error" "FTPS connect FAILED entity= $entity practice= $practice host= $( $ftpConfig . host ) error= $err "
Write-Host " Connection failed: $err " -ForegroundColor Red
$fail += $files . Count
2026-06-29 20:34:33 -05:00
} finally {
$session . Dispose ()
}
return @ { Ok = $ok ; Fail = $fail }
}
2026-07-05 19:18:26 -05:00
# -- Upload via SFTP -----------------------------------------------------------
2026-06-29 20:43:12 -05:00
function Invoke-SFTPUpload($sftpConfig , $files , $remotePath , $entity , $practice ) {
2026-07-05 20:33:01 -05:00
$hostInfo = Get-HostInfo $sftpConfig . host
2026-06-29 20:34:33 -05:00
$opts = New-Object WinSCP . SessionOptions
$opts . Protocol = [ WinSCP.Protocol ]:: Sftp
2026-07-05 20:33:01 -05:00
$opts . HostName = $hostInfo . HostName
2026-06-29 20:34:33 -05:00
$opts . PortNumber = if ( $sftpConfig . port ) { [ int ] $sftpConfig . port } else { 22 }
$opts . UserName = $sftpConfig . username
$opts . Password = $sftpConfig . password
$opts . GiveUpSecurityAndAcceptAnySshHostKey = $true
$session = New-Object WinSCP . Session
2026-06-29 20:43:12 -05:00
if ( $logLevel -eq "debug" ) {
$session . SessionLogPath = Join-Path $logsDir " ${today} _winscp_debug.log"
}
2026-06-29 20:34:33 -05:00
$ok = 0 ; $fail = 0
try {
2026-06-29 20:43:12 -05:00
Write-Debug "SFTP connect: $( $sftpConfig . host ) user= $( $sftpConfig . username ) "
2026-06-29 20:34:33 -05:00
$session . Open ( $opts )
foreach ( $file in $files ) {
$remote = $remotePath . TrimEnd ( "/" ) + "/" + $file . Name
try {
$result = $session . PutFiles ( $file . FullName , $remote )
$result . Check ()
Write-Host " $( $file . Name ) ... ok" -ForegroundColor Green
2026-06-29 20:43:12 -05:00
Write-Info "SFTP OK entity= $entity practice= $practice file= $( $file . Name ) remote= $remote "
2026-06-29 20:34:33 -05:00
$ok ++
} catch {
2026-06-29 20:43:12 -05:00
$err = $_ . Exception . Message
Write-Host " $( $file . Name ) ... FAILED" -ForegroundColor Red
Write-Log "error" "SFTP FAIL entity= $entity practice= $practice file= $( $file . Name ) remote= $remote error= $err "
2026-06-29 20:34:33 -05:00
$fail ++
}
}
2026-06-29 20:43:12 -05:00
} catch {
$err = $_ . Exception . Message
Write-Log "error" "SFTP connect FAILED entity= $entity practice= $practice host= $( $sftpConfig . host ) error= $err "
Write-Host " Connection failed: $err " -ForegroundColor Red
$fail += $files . Count
2026-06-29 20:34:33 -05:00
} finally {
$session . Dispose ()
}
return @ { Ok = $ok ; Fail = $fail }
}
2026-07-05 19:18:26 -05:00
# -- Main loop -----------------------------------------------------------------
2026-06-29 20:34:33 -05:00
$totalOk = 0 ; $totalFail = 0
$practices = Get-ChildItem -Path $export1 -Directory
if ( $practices . Count -eq 0 ) {
Write-Host "No practice folders found in Export1." -ForegroundColor Yellow
2026-06-29 20:43:12 -05:00
Write-Info "No practice folders found in $export1 "
2026-06-29 20:34:33 -05:00
exit 0
}
foreach ( $practice in $practices ) {
$pracName = $practice . Name
Write-Host ""
Write-Host " [ $pracName ]" -ForegroundColor Cyan
2026-06-29 20:43:12 -05:00
Write-Info "--- practice= $pracName ---"
2026-06-29 20:34:33 -05:00
$overrideFile = Join-Path $practice . FullName "practice.json"
$override = if ( Test-Path $overrideFile ) { Get-Content $overrideFile -Raw | ConvertFrom-Json } else { $null }
$config = Merge-Config $entityConfig $override
2026-06-29 20:43:12 -05:00
if ( $override ) { Write-Debug "practice.json override loaded for $pracName " }
2026-06-29 20:34:33 -05:00
2026-07-05 20:15:29 -05:00
# tiff_path is the current name; insurance_path is accepted as a fallback
# so older config files keep working.
if ( $config . ftps -and -not $config . ftps . PSObject . Properties [ "tiff_path" ] -and $config . ftps . PSObject . Properties [ "insurance_path" ]) {
$config . ftps | Add-Member -NotePropertyName "tiff_path" -NotePropertyValue $config . ftps . insurance_path
}
2026-07-05 19:18:26 -05:00
# -- Insurance + PT STMT -> FTPS -------------------------------------------
2026-06-29 20:34:33 -05:00
$dateDir = Join-Path $practice . FullName $today
if ( -not ( Test-Path $dateDir )) {
2026-07-05 19:18:26 -05:00
Write-Host " No $today \ folder - skipping insurance." -ForegroundColor Gray
2026-06-29 20:43:12 -05:00
Write-Info "SKIP entity= $entityName practice= $pracName reason=no date folder"
2026-06-29 20:34:33 -05:00
} else {
2026-07-05 19:45:53 -05:00
$allInsuranceFiles = Get-ChildItem -Path $dateDir -Filter "*. $insuranceExt " -File
$ptStmts = $allInsuranceFiles | Where-Object { $_ . Name -imatch 'pt[\s_]*stmt' }
$insFiles = $allInsuranceFiles | Where-Object { $_ . Name -notmatch 'pt[\s_]*stmt' }
2026-06-29 20:34:33 -05:00
2026-07-05 20:15:29 -05:00
$insPath = Expand-Path $config . ftps . tiff_path $pracName
2026-06-29 20:34:33 -05:00
$pdfPath = Expand-Path $config . ftps . pdf_path $pracName
$insResult = @ { Ok = 0 ; Fail = 0 }
2026-07-05 20:28:46 -05:00
$connMissing = Get-MissingFields $config . ftps "ftps" @ ( "host" , "username" , "password" )
if (( $insFiles . Count -gt 0 -or $ptStmts . Count -gt 0 ) -and $connMissing . Count -gt 0 ) {
Write-Host " FTPS not configured ( $( $connMissing -join ', ' ) is null/blank) - $( $insFiles . Count + $ptStmts . Count ) file(s) ignored, left in place." -ForegroundColor Yellow
Write-Info "IGNORE entity= $entityName practice= $pracName reason=unconfigured fields= $( $connMissing -join ',' ) "
2026-07-05 20:19:34 -05:00
} else {
2026-07-05 20:28:46 -05:00
$ignored = 0
2026-07-05 20:19:34 -05:00
if ( $insFiles . Count -gt 0 ) {
2026-07-05 20:28:46 -05:00
if ([ string ]:: IsNullOrWhiteSpace ([ string ] $config . ftps . tiff_path )) {
Write-Host " ftps.tiff_path is null/blank - $( $insFiles . Count ) insurance file(s) ignored, left in place." -ForegroundColor Yellow
Write-Info "IGNORE entity= $entityName practice= $pracName reason=tiff_path null count= $( $insFiles . Count ) "
$ignored += $insFiles . Count
} else {
Write-Host " Insurance ( $( $insFiles . Count ) ) -> $insPath " -ForegroundColor White
$r = Invoke-FTPSUpload $config . ftps $insFiles $insPath $entityName $pracName
$insResult . Ok += $r . Ok ; $insResult . Fail += $r . Fail
}
2026-07-05 20:19:34 -05:00
}
if ( $ptStmts . Count -gt 0 ) {
2026-07-05 20:28:46 -05:00
if ([ string ]:: IsNullOrWhiteSpace ([ string ] $config . ftps . pdf_path )) {
Write-Host " ftps.pdf_path is null/blank - $( $ptStmts . Count ) PT STMT file(s) ignored, left in place." -ForegroundColor Yellow
Write-Info "IGNORE entity= $entityName practice= $pracName reason=pdf_path null count= $( $ptStmts . Count ) "
$ignored += $ptStmts . Count
} else {
Write-Host " PT STMT ( $( $ptStmts . Count ) ) -> $pdfPath " -ForegroundColor White
$r = Invoke-FTPSUpload $config . ftps $ptStmts $pdfPath $entityName $pracName
$insResult . Ok += $r . Ok ; $insResult . Fail += $r . Fail
}
2026-07-05 20:19:34 -05:00
}
2026-07-05 20:05:31 -05:00
2026-07-05 20:28:46 -05:00
$totalUploaded = $insFiles . Count + $ptStmts . Count - $ignored
if ( $insResult . Fail -eq 0 -and $totalUploaded -gt 0 -and $ignored -eq 0 ) {
2026-07-05 20:19:34 -05:00
$archive = Get-ArchiveDir $practice . FullName "Archive sent to FTP"
Move-Item -Path $dateDir -Destination ( Join-Path $archive $today ) -Force
Write-Host " $today \ archived." -ForegroundColor Green
Write-Info "ARCHIVE entity= $entityName practice= $pracName folder= $today "
} elseif ( $insResult . Fail -gt 0 ) {
Write-Host " $( $insResult . Fail ) failed - $today \ left for retry." -ForegroundColor Yellow
2026-07-05 20:28:46 -05:00
} elseif ( $totalUploaded -gt 0 -and $ignored -gt 0 ) {
Write-Host " $today \ NOT archived - $ignored file(s) were ignored (unconfigured path)." -ForegroundColor Yellow
Write-Info "NOARCHIVE entity= $entityName practice= $pracName reason= $ignored ignored files"
} elseif ( $insFiles . Count + $ptStmts . Count -eq 0 ) {
2026-07-05 20:19:34 -05:00
Write-Host " No . $insuranceExt files in $today \." -ForegroundColor Gray
Write-Info "SKIP entity= $entityName practice= $pracName reason=no . $insuranceExt files in date folder"
}
2026-06-29 20:34:33 -05:00
2026-07-05 20:19:34 -05:00
$totalOk += $insResult . Ok ; $totalFail += $insResult . Fail
2026-06-29 20:34:33 -05:00
}
}
2026-07-05 19:18:26 -05:00
# -- Patient files -> SFTP -------------------------------------------------
2026-06-29 20:34:33 -05:00
if ( $workflow -eq "insurance+patient" ) {
$patientExport = Join-Path $practice . FullName "Patient\Export"
if ( -not ( Test-Path $patientExport )) {
2026-07-05 19:18:26 -05:00
Write-Host " No Patient\Export folder - skipping patient." -ForegroundColor Gray
2026-06-29 20:43:12 -05:00
Write-Info "SKIP entity= $entityName practice= $pracName reason=no Patient\Export"
2026-06-29 20:34:33 -05:00
} else {
$ptFolder = Get-ChildItem -Path $patientExport -Directory |
Where-Object { $_ . Name -imatch "PT STMT_ $today " } |
Select-Object -First 1
if ( -not $ptFolder ) {
2026-07-05 19:18:26 -05:00
Write-Host " No PT STMT_ $today folder - skipping patient." -ForegroundColor Gray
2026-06-29 20:43:12 -05:00
Write-Info "SKIP entity= $entityName practice= $pracName reason=no PT STMT_ $today folder"
2026-06-29 20:34:33 -05:00
} else {
$patFiles = Get-ChildItem -Path $ptFolder . FullName -File |
Where-Object { $_ . Extension -imatch '\.(txt|tif|tiff)$' }
if ( $patFiles . Count -eq 0 ) {
2026-07-05 19:18:26 -05:00
Write-Host " No txt/tif files in $( $ptFolder . Name ) \ - skipping." -ForegroundColor Gray
2026-06-29 20:43:12 -05:00
Write-Info "SKIP entity= $entityName practice= $pracName reason=no txt/tif files"
2026-07-05 20:05:31 -05:00
} elseif (( $sftpMissing = Get-MissingFields $config . sftp "sftp" @ ( "host" , "username" , "password" , "patient_path" )). Count -gt 0 ) {
2026-07-05 20:19:34 -05:00
Write-Host " Patient upload not configured ( $( $sftpMissing -join ', ' ) is null/blank) - $( $patFiles . Count ) file(s) ignored, left in place." -ForegroundColor Yellow
Write-Info "IGNORE entity= $entityName practice= $pracName reason=unconfigured fields= $( $sftpMissing -join ',' ) "
2026-06-29 20:34:33 -05:00
} else {
$patPath = Expand-Path $config . sftp . patient_path $pracName
2026-07-05 19:18:26 -05:00
Write-Host " Patient ( $( $patFiles . Count ) ) -> $patPath " -ForegroundColor White
2026-06-29 20:43:12 -05:00
$r = Invoke-SFTPUpload $config . sftp $patFiles $patPath $entityName $pracName
2026-06-29 20:34:33 -05:00
if ( $r . Fail -eq 0 ) {
$patArchive = Get-ArchiveDir $patientExport "Archive Sent to FTP"
Move-Item -Path $ptFolder . FullName -Destination ( Join-Path $patArchive $ptFolder . Name ) -Force
Write-Host " $( $ptFolder . Name ) \ archived." -ForegroundColor Green
2026-06-29 20:43:12 -05:00
Write-Info "ARCHIVE entity= $entityName practice= $pracName folder= $( $ptFolder . Name ) "
2026-06-29 20:34:33 -05:00
} else {
2026-07-05 19:18:26 -05:00
Write-Host " $( $r . Fail ) failed - left for retry." -ForegroundColor Yellow
2026-06-29 20:34:33 -05:00
}
$totalOk += $r . Ok ; $totalFail += $r . Fail
}
}
}
}
}
Write-Host ""
Write-Host ( "=" * 60 )
2026-06-29 20:43:12 -05:00
Write-Info "=== END entity= $entityName ok= $totalOk fail= $totalFail ==="
2026-06-29 20:34:33 -05:00
if ( $totalFail -eq 0 ) {
Write-Host "Done: $totalOk file(s) uploaded for $entityName ." -ForegroundColor Green
} else {
2026-06-29 20:43:12 -05:00
Write-Host "Done: $totalOk uploaded, $totalFail failed. See Logs\ $today .log" -ForegroundColor Yellow
2026-06-29 20:34:33 -05:00
}
exit $ ( if ( $totalFail -gt 0 ) { 1 } else { 0 })