Initial build: engine, run scripts, WinSCP setup, example configs

This commit is contained in:
2026-06-29 20:34:33 -05:00
parent 241acd40d6
commit ac4c6d823c
9 changed files with 416 additions and 2 deletions
+236
View File
@@ -0,0 +1,236 @@
# Core upload engine — called by run.ps1 with an entity directory path
param(
[Parameter(Mandatory=$true)]
[string]$EntityDir
)
$scriptRoot = Split-Path -Parent $PSCommandPath
$winscpDll = Join-Path $scriptRoot "WinSCP\WinSCPnet.dll"
$today = Get-Date -Format "yyyyMMdd"
if (-not (Test-Path $winscpDll)) {
Write-Host ""
Write-Host "ERROR: WinSCP not found. Run setup.bat first." -ForegroundColor Red
exit 1
}
Add-Type -Path $winscpDll
$entityConfigFile = Join-Path $EntityDir "entity.json"
if (-not (Test-Path $entityConfigFile)) {
Write-Host "ERROR: entity.json not found in $EntityDir" -ForegroundColor Red
exit 1
}
$entityConfig = Get-Content $entityConfigFile -Raw | ConvertFrom-Json
$workflow = $entityConfig.workflow
$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
exit 1
}
Write-Host ""
Write-Host "Entity: $entityName | Workflow: $workflow | Date: $today" -ForegroundColor Cyan
Write-Host ("=" * 60)
# Merge a practice-level override on top of the entity config
function Merge-Config($base, $override) {
if (-not $override) { return $base }
$json = $base | ConvertTo-Json -Depth 10
$merged = $json | ConvertFrom-Json
foreach ($section in $override.PSObject.Properties) {
if ($null -ne $merged.PSObject.Properties[$section.Name]) {
foreach ($prop in $section.Value.PSObject.Properties) {
$merged.$($section.Name).$($prop.Name) = $prop.Value
}
}
}
return $merged
}
# Replace {practice} token in a remote path template
function Expand-Path($template, $practice) {
return $template -replace '\{practice\}', $practice
}
# Find archive subfolder by name (case-insensitive), create if missing
function Get-ArchiveDir($parent, $name) {
$found = Get-ChildItem -Path $parent -Directory |
Where-Object { $_.Name -ieq $name } |
Select-Object -First 1
if ($found) { return $found.FullName }
$path = Join-Path $parent $name
New-Item -ItemType Directory -Path $path | Out-Null
return $path
}
# Upload a list of files to a remote path via FTPS/FTP using WinSCP
function Invoke-FTPSUpload($ftpConfig, $files, $remotePath) {
$opts = New-Object WinSCP.SessionOptions
$opts.Protocol = [WinSCP.Protocol]::Ftp
$opts.FtpSecure = if ($ftpConfig.tls) { [WinSCP.FtpSecure]::Explicit } else { [WinSCP.FtpSecure]::None }
$opts.HostName = $ftpConfig.host
$opts.PortNumber = if ($ftpConfig.port) { [int]$ftpConfig.port } else { 21 }
$opts.UserName = $ftpConfig.username
$opts.Password = $ftpConfig.password
$opts.GiveUpSecurityAndAcceptAnyTlsHostCertificate = $true
$session = New-Object WinSCP.Session
$ok = 0; $fail = 0
try {
$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
$ok++
} catch {
Write-Host " $($file.Name) ... FAILED: $($_.Exception.Message)" -ForegroundColor Red
$fail++
}
}
} finally {
$session.Dispose()
}
return @{ Ok = $ok; Fail = $fail }
}
# Upload a list of files to a remote path via SFTP using WinSCP
function Invoke-SFTPUpload($sftpConfig, $files, $remotePath) {
$opts = New-Object WinSCP.SessionOptions
$opts.Protocol = [WinSCP.Protocol]::Sftp
$opts.HostName = $sftpConfig.host
$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
$ok = 0; $fail = 0
try {
$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
$ok++
} catch {
Write-Host " $($file.Name) ... FAILED: $($_.Exception.Message)" -ForegroundColor Red
$fail++
}
}
} finally {
$session.Dispose()
}
return @{ Ok = $ok; Fail = $fail }
}
$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
exit 0
}
foreach ($practice in $practices) {
$pracName = $practice.Name
Write-Host ""
Write-Host " [ $pracName ]" -ForegroundColor Cyan
# Load optional practice-level credential override
$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
# ── INSURANCE + PT STMT → FTPS ──────────────────────────────────────────
$dateDir = Join-Path $practice.FullName $today
if (-not (Test-Path $dateDir)) {
Write-Host " No $today\ folder — skipping insurance upload." -ForegroundColor Gray
} else {
$allPdfs = Get-ChildItem -Path $dateDir -Filter "*.pdf" -File
$ptStmts = $allPdfs | Where-Object { $_.Name -imatch 'pt[\s_]*stmt' }
$insFiles = $allPdfs | Where-Object { $_.Name -notmatch 'pt[\s_]*stmt' }
$insPath = Expand-Path $config.ftps.insurance_path $pracName
$pdfPath = Expand-Path $config.ftps.pdf_path $pracName
$insResult = @{ Ok = 0; Fail = 0 }
if ($insFiles.Count -gt 0) {
Write-Host " Insurance ($($insFiles.Count)) → $insPath" -ForegroundColor White
$r = Invoke-FTPSUpload $config.ftps $insFiles $insPath
$insResult.Ok += $r.Ok; $insResult.Fail += $r.Fail
}
if ($ptStmts.Count -gt 0) {
Write-Host " PT STMT PDF ($($ptStmts.Count)) → $pdfPath" -ForegroundColor White
$r = Invoke-FTPSUpload $config.ftps $ptStmts $pdfPath
$insResult.Ok += $r.Ok; $insResult.Fail += $r.Fail
}
$totalUploaded = $insFiles.Count + $ptStmts.Count
if ($insResult.Fail -eq 0 -and $totalUploaded -gt 0) {
$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
} elseif ($insResult.Fail -gt 0) {
Write-Host " $($insResult.Fail) failed — $today\ left in place for retry." -ForegroundColor Yellow
} elseif ($totalUploaded -eq 0) {
Write-Host " No PDFs found in $today\." -ForegroundColor Gray
}
$totalOk += $insResult.Ok; $totalFail += $insResult.Fail
}
# ── PATIENT FILES → SFTP (only if workflow = insurance+patient) ──────────
if ($workflow -eq "insurance+patient") {
$patientExport = Join-Path $practice.FullName "Patient\Export"
if (-not (Test-Path $patientExport)) {
Write-Host " No Patient\Export folder — skipping patient upload." -ForegroundColor Gray
} else {
$ptFolder = Get-ChildItem -Path $patientExport -Directory |
Where-Object { $_.Name -imatch "PT STMT_$today" } |
Select-Object -First 1
if (-not $ptFolder) {
Write-Host " No PT STMT_$today folder — skipping patient upload." -ForegroundColor Gray
} else {
$patFiles = Get-ChildItem -Path $ptFolder.FullName -File |
Where-Object { $_.Extension -imatch '\.(txt|tif|tiff)$' }
if ($patFiles.Count -eq 0) {
Write-Host " No txt/tif files in $($ptFolder.Name)\ — skipping." -ForegroundColor Gray
} else {
$patPath = Expand-Path $config.sftp.patient_path $pracName
Write-Host " Patient ($($patFiles.Count)) → $patPath" -ForegroundColor White
$r = Invoke-SFTPUpload $config.sftp $patFiles $patPath
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
} else {
Write-Host " $($r.Fail) failed — left in place for retry." -ForegroundColor Yellow
}
$totalOk += $r.Ok; $totalFail += $r.Fail
}
}
}
}
}
Write-Host ""
Write-Host ("=" * 60)
if ($totalFail -eq 0) {
Write-Host "Done: $totalOk file(s) uploaded for $entityName." -ForegroundColor Green
} else {
Write-Host "Done: $totalOk uploaded, $totalFail failed." -ForegroundColor Yellow
}
exit $(if ($totalFail -gt 0) { 1 } else { 0 })