private/localization/Get-CtManagedService.ps1

<#
.SYNOPSIS
    Resolve, para um achado REPROVADO, o texto de "Serviços Gerenciados Cloud Target".
 
.DESCRIPTION
    Combina duas fontes (carregadas dos assets do módulo):
      - assets/managed-service-ct.json : texto 'ct' por TestId (como a Cloud Target
        apoia aquele achado específico).
      - assets/offer-rules.json + assets/offers.json : mapeia o pilar/categoria do
        teste para uma oferta (projeto/serviço gerenciado) e traz o pitch dela.
 
    Só produz conteúdo para achados com status reprovado (offer-rules.mappedStatuses),
    pois a recomendação comercial só faz sentido quando o controle falhou.
    Retorna markdown pronto para um card, ou $null quando não há oferta aplicável.
#>


$script:CtOffers      = $null
$script:CtOfferRules  = $null
$script:CtServiceText = $null
$script:CtOffersLoaded = $false

function Initialize-CtManagedService {
    [CmdletBinding()]
    param()
    $assets = Join-Path $script:ModuleRoot 'assets'
    $offersPath = Join-Path $assets 'offers.json'
    $rulesPath  = Join-Path $assets 'offer-rules.json'
    $ctPath     = Join-Path $assets 'managed-service-ct.json'

    $script:CtOffers = @{}
    if (Test-Path $offersPath) {
        foreach ($o in (Get-Content $offersPath -Raw -Encoding UTF8 | ConvertFrom-Json).offers) {
            $script:CtOffers[$o.id] = $o
        }
    }
    $script:CtOfferRules  = if (Test-Path $rulesPath) { Get-Content $rulesPath -Raw -Encoding UTF8 | ConvertFrom-Json } else { $null }
    $script:CtServiceText = if (Test-Path $ctPath)    { Get-Content $ctPath   -Raw -Encoding UTF8 | ConvertFrom-Json } else { $null }
    $script:CtOffersLoaded = $true
}

function Get-CtManagedService {
    [CmdletBinding()]
    param(
        [string]$TestId,
        [string]$Pillar,
        [string]$Category,
        [string]$Status
    )
    if (-not $script:CtOffersLoaded) { Initialize-CtManagedService }
    if (-not $script:CtOfferRules) { return $null }

    # Apenas achados reprovados (mappedStatuses = 'fail').
    if ([string]::IsNullOrWhiteSpace($Status) -or $Status -notmatch '(?i)fail') { return $null }
    if ([string]::IsNullOrWhiteSpace($Pillar)) { return $null }

    $pillarRule = $script:CtOfferRules.byPillar.$Pillar
    if (-not $pillarRule) { return $null }

    $offerId = $pillarRule.primary

    # categoryOverrides refinam a oferta por categoria dentro do pilar.
    if ($Category -and $script:CtOfferRules.categoryOverrides) {
        foreach ($ov in $script:CtOfferRules.categoryOverrides) {
            if ($ov.pillar -eq $Pillar -and $ov.categoryMatch -and $Category -like "*$($ov.categoryMatch)*" -and $ov.primary) {
                $offerId = $ov.primary
                break
            }
        }
    }

    $offer = $script:CtOffers[$offerId]

    # Texto especifico do achado (por TestId), quando existir.
    $ctText = $null
    if ($TestId -and $script:CtServiceText) {
        $prop = $script:CtServiceText.PSObject.Properties[$TestId]
        if ($prop) { $ctText = $prop.Value }
    }

    if (-not $offer -and -not $ctText) { return $null }

    $sb = [System.Text.StringBuilder]::new()
    if ($ctText) {
        [void]$sb.AppendLine($ctText)
        [void]$sb.AppendLine()
    }
    if ($offer) {
        $tipo = if ($offer.type) { " ($($offer.type))" } else { '' }
        [void]$sb.AppendLine("**Oferta recomendada: $($offer.name)$tipo**")
        [void]$sb.AppendLine()
        if ($offer.pitch) { [void]$sb.AppendLine($offer.pitch) }
    }
    return $sb.ToString().TrimEnd()
}