Modules/businessdev.ALbuild.Marketplace/Public/Get-BcMarketplaceProduct.ps1

function Get-BcMarketplaceProduct {
    <#
    .SYNOPSIS
        Retrieves Business Central products from the Microsoft product-ingestion API.
 
    .DESCRIPTION
        Mirrors the proven Publish-AppSourceApp / Get-AppSourceProduct implementation: the product name
        lives in the product's 'alias' (NOT 'name'), the product type is 'dynamics365BusinessCentral'
        (camelCase - the hyphenated form returns 400), and the working schema version is
        '2022-03-01-preview3'. Results are paged via '@nextLink'.
 
    .PARAMETER AuthContext
        An auth context from New-BcMarketplaceAuthContext (Graph scope:
        'https://graph.microsoft.com/.default').
 
    .PARAMETER Name
        Optional product-name (alias) filter: exact match first, then wildcard/substring.
 
    .PARAMETER ProductType
        Product type. Default 'dynamics365BusinessCentral'.
 
    .PARAMETER ApiVersion
        Product ingestion API version. Default '2022-03-01-preview3'.
 
    .OUTPUTS
        The product object(s) (each with id, alias, identity.externalId, ...).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [PSCustomObject] $AuthContext,
        [string] $Name,
        [string] $ProductType = 'dynamics365BusinessCentral',
        [string] $ApiVersion = '2022-03-01-preview3'
    )

    # Product-ingestion responses are dynamic JSON whose OPTIONAL properties (e.g. '@nextLink' on the
    # last page) are simply absent. The module runs under Set-StrictMode -Version Latest, which throws
    # on a missing property; relax it here (as the proven Publish-AppSourceApp script runs) so an absent
    # property reads as $null.
    Set-StrictMode -Off

    $headers = @{ Authorization = "Bearer $($AuthContext.AccessToken)"; 'Content-Type' = 'application/json' }

    # List every product of the type, following @nextLink. '$version' must be a literal ($ escaped).
    $url = "https://graph.microsoft.com/rp/product-ingestion/product?type=$ProductType&`$version=$ApiVersion"
    $products = [System.Collections.Generic.List[object]]::new()
    while ($url) {
        try { $response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get -ErrorAction Stop }
        catch {
            $detail = Get-BcMarketplaceErrorDetail -ErrorRecord $_
            throw "Product-ingestion product query failed: $($_.Exception.Message)$(if ($detail) { " - $detail" })"
        }
        foreach ($p in @($response.value)) { $products.Add($p) }
        $url = $response.'@nextLink'
    }

    $all = @($products.ToArray())
    if ($Name) {
        # The product NAME is the 'alias'. Prefer an exact match, then fall back to wildcard/substring.
        $exact = @($all | Where-Object { "$($_.alias)" -eq $Name })
        if ($exact.Count -gt 0) { return $exact }
        return @($all | Where-Object { "$($_.alias)" -like "*$Name*" })
    }
    return $all
}