public/Set-VSATenantModuleLicense.ps1

function Set-VSATenantModuleLicense {
    <#
    .Synopsis
       Updates a selected license within a specified tenant.
    .DESCRIPTION
       Updates a selected license within a specified tenant. Typically, only the Limit field is updated.
       Takes either Tenant or non-Tenant connection information.
 
       This cmdlet has two parameter sets, which differ in BOTH how the tenant is identified and how
       the license is selected:
 
         ById : -TenantId + -LicenseName + -zzVal + -Limit + -LicenseType
                  Looks the named license up on the tenant and updates its Limit in place.
         ByName : -TenantName + -zzValId + -Limit + -LicenseType
                  Resolves the tenant name to its Id, then writes a license record built from the
                  supplied values.
 
       NAMING NOTE: 'zzValId' and 'zzVal' are not placeholder names. They are the VSA REST API's own
       request-body field names for this endpoint and are transmitted verbatim, so they are kept as-is
       to stay greppable against the Kaseya API documentation. The friendlier aliases -LicenseValueId
       and -LicenseValue are accepted for readability.
    .PARAMETER VSAConnection
        Specifies existing non-Tenant VSAConnection.
    .PARAMETER URISuffix
        Specifies URI suffix if it differs from the default.
    .PARAMETER TenantId
        Specifies Tenant Id. Belongs to the ById parameter set.
    .PARAMETER TenantName
        Specifies Tenant Name. Belongs to the ByName parameter set; resolved to a Tenant Id at runtime.
    .PARAMETER DataType
        Specifies the data type for the limit.
    .PARAMETER Limit
        Specifies License Limit. Required by both parameter sets.
    .PARAMETER Name
        Specifies module name.
    .PARAMETER zzValId
        Specifies License Id (the API's 'zzValId' body field). Required by the ByName parameter set.
        Also accepted as -LicenseValueId.
    .PARAMETER zzVal
        Specifies the license value (the API's 'zzVal' body field). Required by the ById parameter set.
        When omitted on the ByName path it defaults to "zzvals<zzValId>". Also accepted as -LicenseValue.
    .PARAMETER LicenseType
        Specifies the license type id. Required by both parameter sets.
    .PARAMETER LicenseName
        Specifies the name of the existing license to update. Required by the ById parameter set, and
        tab-completes from the licenses already present on the given -TenantId.
    .PARAMETER StringValue
        Specifies String Value.
    .PARAMETER DateValue
        Specifies Date.
    .EXAMPLE
       Set-VSATenantModuleLicense -TenantId 10001 -Name 'MalwareBytes Anti-Malware' -Limit 1000000000000
    .EXAMPLE
       Set-VSATenantModuleLicense -TenantName 'Your Tenant' -zzValId 1600000000000 -LicenseType 900000000000 -Limit 2000000000000
    .INPUTS
       Accepts piped non-Tenant VSAConnection.
    .OUTPUTS
       Array of module licenses.
    #>


    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ById')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is invoked centrally by Invoke-VSAWriteRequest, which receives this cmdlet''s $PSCmdlet via -Caller (module-wide pattern).')]
    param (
        [parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNull()]
        [VSAConnection] $VSAConnection,

        [parameter(DontShow, Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string] $URISuffix = 'api/v1.0/tenantmanagement/tenant/modules/licenses/{0}',

        [parameter(Mandatory = $true, ParameterSetName = 'ById', ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if( $_ -notmatch "^\d+$" ) {
                throw "Non-numeric Id"
            }
            return $true
        })]
        [string] $TenantId,

        [parameter(Mandatory = $true, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            try {
                $CompleterParams = @{}
                if ($fakeBoundParameters['VSAConnection']) { $CompleterParams['VSAConnection'] = $fakeBoundParameters['VSAConnection'] }
                Get-VSATenant @CompleterParams -ErrorAction Stop |
                    Select-Object -ExpandProperty Ref |
                    Where-Object { $_ -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
            } catch { Write-Debug "Argument completer suppressed error: $_" }
        })]
        [ValidateNotNullOrEmpty()]
        [string] $TenantName,

        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if( $_ -notmatch "^\d+$" ) {
                throw "Non-numeric value"
            }
            return $true
        })]
        [string] $DataType,

        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        # 'zzValId' / 'zzVal' mirror the VSA API's own request-body field names (they are sent verbatim
        # in Process), so they are kept as the primary names to stay greppable against the Kaseya API
        # docs. The aliases below give callers a readable alternative without diverging from the API.
        [parameter(Mandatory = $true, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName = $true)]
        [Alias('LicenseValueId')]
        [ValidateScript({
            if( $_ -notmatch "^\d+$" ) {
                throw "Non-numeric value"
            }
            return $true
        })]
        [string] $zzValId,

        [parameter(Mandatory = $true, ParameterSetName = 'ById', ValueFromPipelineByPropertyName = $true)]
        [Alias('LicenseValue')]
        [ValidateNotNullOrEmpty()]
        [string] $zzVal,

        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if( $_ -notmatch "^\d+$" ) {
                throw "Non-numeric value"
            }
            return $true
        })]
        [string] $Limit,

        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if( $_ -notmatch "^\d+$" ) {
                throw "Non-numeric value"
            }
            return $true
        })]
        [string] $LicenseType,

        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $StringValue,

        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $DateValue,

        # ById selector for an existing license. This was a DynamicParam that eagerly called
        # Get-VSATenantModuleLicense on every tab-completion / Get-Command (F-44 / A-2). It is now a
        # static parameter with a lazy completer that uses the already-typed -TenantId; the actual
        # license fetch happens in Begin.
        [parameter(Mandatory = $true, ParameterSetName = 'ById', ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            try {
                $CompleterParams = @{}
                if ($fakeBoundParameters['VSAConnection']) { $CompleterParams['VSAConnection'] = $fakeBoundParameters['VSAConnection'] }
                if ($fakeBoundParameters['TenantId'])      { $CompleterParams['TenantId']      = $fakeBoundParameters['TenantId'] }
                Get-VSATenantModuleLicense @CompleterParams -ErrorAction Stop |
                    Select-Object -ExpandProperty ModuleLicenses |
                    Select-Object -ExpandProperty Name |
                    Where-Object { $_ -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
            } catch { Write-Debug "Argument completer suppressed error: $_" }
        })]
        [ValidateNotNullOrEmpty()]
        [string] $LicenseName
    )

    Begin {
        # Name<->Id resolution moved out of the former DynamicParam so no REST call fires during
        # parameter/command discovery (F-44 / A-2). Process is unchanged and still reads
        # $Licenses (populated here only for the ById set).
        [hashtable] $AuxParameters = @{ErrorAction = 'Stop'}
        if($VSAConnection) {$AuxParameters.Add('VSAConnection', $VSAConnection)}

        [array] $Licenses = @()

        if ( $PSCmdlet.ParameterSetName -eq 'ByName' ) {
            $Tenants  = Get-VSATenant @AuxParameters | Select-Object Id, Ref
            # Resolve into a LOCAL first: assigning an unresolved (empty) value straight to $TenantId
            # re-triggers that parameter's ValidateScript, which throws its own "Non-numeric Id" and
            # masks the accurate "No tenant found" message below (F-4).
            $ResolvedTenantId = $Tenants | Where-Object { $_.Ref -eq $TenantName } | Select-Object -First 1 -ExpandProperty Id
            if ( [string]::IsNullOrEmpty($ResolvedTenantId) ) {
                throw "Set-VSATenantModuleLicense: No tenant found with name '$TenantName'."
            }
            $TenantId = $ResolvedTenantId
        } else {
            $AuxParameters['TenantId'] = $TenantId
            [array] $Licenses = Get-VSATenantModuleLicense @AuxParameters |
                Select-Object -ExpandProperty ModuleLicenses |
                Where-Object { $_.Name -eq $LicenseName }
        }
    } # Begin
     Process {
        $URISuffix = $URISuffix -f $TenantId

        if ( 0 -lt $Licenses.Count ) {
            $Licenses[0].Limit = $Limit
            [string] $Body = $Licenses | ConvertTo-Json
        } else {

            if ( [string]::IsNullOrEmpty($zzVal))    { $zzVal = "zzvals$zzValId" }

            [string] $Body = @( [PSCustomObject]@{

                zzValId     = $zzValId
                LicenseType = $LicenseType
                Limit       = $Limit
                zzVal       = $zzVal
                StringValue = $StringValue
                DateValue   = $DateValue
                DataType    = $DataType
                Name        = $Name
            } | Where-Object { $_.Value } ) | ConvertTo-Json
        }

        Write-Debug "Set-VSATenantModuleLicense. $($Body | Out-String)"

        return Invoke-VSAWriteRequest -Body ($Body) -Method 'PUT' -URISuffix ($URISuffix) -VSAConnection $VSAConnection -Caller $PSCmdlet
    }#Process
}
New-Alias -Name Set-VSATenantModuleLicenses -Value Set-VSATenantModuleLicense
Export-ModuleMember -Function Set-VSATenantModuleLicense -Alias Set-VSATenantModuleLicenses