Functions/Merge-InstallerCacheReports.ps1

<#
.SYNOPSIS
    Merges per-host FixMissingMSI report CSVs into 1 consolidated report that is leveraged to build the shared cache of missing files.
 
.DESCRIPTION
    Intended to be run interactively after Merge-InstallerCacheReports has been ran across servers.
    This script reads all CSV files from:
        $FileSharePath\FixMissingMSI\Reports
    and produces consolidated outputs in the same folder.
 
    It performs the following:
      1. Imports all *.csv under the Reports folder.
      2. Deduplicates rows by key fields (ProductCode, {PackageCode, PatchCode}, PackageName, Publisher, ProductVersion)
      3. Produces MSI and MSP summary lists:
         - MSIProductCodes.csv (ProductCode, PackageCode, PackageName, Publisher, ProductVersion)
         - MSPPatchCodes.csv (ProductCode, PatchCode, PackageName, Publisher, ProductVersion)
 
    > Note: Step 1 adds Hostname or SourcePath columns for traceability. Those are preserved in the merged data
    > but are not part of the uniqueness key. Adjust the Select-Object properties if you prefer a different definition.
 
.PARAMETER FileSharePath
    UNC to the share root that contains the app tree. Example: \\FS01\Software
    The script expects reports at: $FileSharePath\FixMissingMSI\Reports
 
.EXAMPLE
PS> Merge-InstallerCacheReports -FileSharePath \\FS01\Software
 
    Merges all host reports from \\FS01\Software\FixMissingMSI\Reports and writes
    MSIProductCodes.csv and MSPPatchCodes.csv in that folder.
 
.NOTES
    Author: Joey Eckelbarger
 
    Requires:
        - PowerShell 5.1+
        - Read/Write access to \\<Server>\<Share>\FixMissingMSI\Reports
#>

function Merge-InstallerCacheReports {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FileSharePath
    )
    $ErrorActionPreference = 'Stop'

    # Compose paths; all activity occurs under the app folder's Reports directory.
    $ShareRoot     = $FileSharePath.TrimEnd('\')
    $AppFolder     = Join-Path $ShareRoot 'FixMissingMSI'
    $ReportsPath   = Join-Path $AppFolder 'Reports'
    $msiReportPath = Join-Path $ReportsPath "MSIProductCodes.csv"
    $mspReportPath = Join-Path $ReportsPath "MSPPatchCodes.csv"

    if (-not (Test-Path -LiteralPath $ReportsPath)) {
        throw "Reports folder not found: $ReportsPath"
    }

    # Discover CSVs first; fail gracefully if none are present.
    [array]$csvFiles = Get-ChildItem -Path $ReportsPath -Filter '*.csv' | Where-object {$_.Name -notin @("MSIProductCodes.csv","MSPPatchCodes.csv")} | Select-Object -ExpandProperty FullName

    if (-not $csvFiles -or $csvFiles.Count -eq 0) {
        Throw "No CSV reports found in $ReportsPath. Ensure Step 1 has completed across targets."
    }

    # Import all CSVs into 1 merged var
    [array]$merged = foreach ($csv in $csvFiles) {
        Import-Csv -LiteralPath $csv
    }

    # Build MSI summary (msi packages typically lack PatchCode; keep fields most relevant to sourcing).
   [array]$msi = $merged |
        Where-Object { $_.PackageName -like '*.msi' } |
        Select-Object ProductCode, PackageCode, PackageName, Publisher, ProductVersion |
        Sort-Object * -Unique

    # Build MSP summary (patches carry PatchCode).
   [array]$msp = $merged |
        Where-Object { $_.PackageName -like '*.msp' } |
        Select-Object ProductCode, PatchCode, PackageName, Publisher, ProductVersion |
        Sort-Object * -Unique
        
    if($msi){
        $msi | Export-Csv -LiteralPath $msiReportPath -NoTypeInformation -Force
    }

    if($msp){
        $msp | Export-Csv -LiteralPath $mspReportPath -NoTypeInformation -Force
    }

    # Simple summary
    [PSCustomObject]@{
        "CSV Files" = $($csvFiles.Count)
        "MSI"       = "$($msi.Count) -> MSIPatchCodes.csv"
        "MSP"       = "$($msp.Count) -> MSPProductCodes.csv"
    } | Format-List

}