Public/New-CloudPCProvisioningPolicy.ps1

function New-CloudPCProvisioningPolicy {
    <#
    .SYNOPSIS
        Creates a Windows 365 Cloud PC provisioning policy from an export.

    .DESCRIPTION
        Creates a new Cloud PC provisioning policy by POSTing the exported CreateBody
        to /beta/deviceManagement/virtualEndpoint/provisioningPolicies.

        Use Export-CloudPCProvisioningPolicy to produce the JSON. Assignment targets
        are included in the export, but are only applied when -Assign is specified.

    .PARAMETER Path
        Path to a JSON file created by Export-CloudPCProvisioningPolicy.

    .PARAMETER InputObject
        Export object created by Export-CloudPCProvisioningPolicy.

    .PARAMETER DisplayName
        Optional replacement display name for the new policy.

    .PARAMETER Description
        Optional replacement description for the new policy.

    .PARAMETER Assign
        Recreate exported assignment targets on the newly created policy.

    .PARAMETER RegionName
        Optional supported region name to use for Microsoft Entra joined policies.
        This overrides exported automatic target geography values.

    .PARAMETER IncludeAutopilotConfiguration
        Include the exported Autopilot configuration in the create request.
        By default, this is omitted because Graph can reject copied device
        preparation profile IDs even when they were returned on the source policy.

    .PARAMETER AllotmentLicensesCount
        Override the exported allotment count for shared by Entra group assignment
        targets. Use this when copying a Flex Shared policy and the source count
        exceeds remaining capacity.

    .PARAMETER Force
        Suppress confirmation prompts. Equivalent to -Confirm:$false.

    .EXAMPLE
        New-CloudPCProvisioningPolicy -Path .\policy.json -DisplayName 'Copied Policy' -WhatIf

    .EXAMPLE
        Export-CloudPCProvisioningPolicy -Id '<policy-id>' |
            New-CloudPCProvisioningPolicy -DisplayName 'Copied Policy' -Assign -Force
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'FromPath')]
    [OutputType('WindowsCloudPC.ProvisioningPolicyCreateResult')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'FromPath')]
        [string]$Path,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromObject')]
        [object]$InputObject,

        [string]$DisplayName,

        [string]$Description,

        [string]$RegionName,

        [switch]$IncludeAutopilotConfiguration,

        [ValidateRange(1, [int]::MaxValue)]
        [int]$AllotmentLicensesCount,

        [switch]$Assign,

        [switch]$Force
    )

    begin {
        if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) {
            $ConfirmPreference = 'None'
        }

        Connect-CloudPC -AdditionalScopes 'CloudPC.ReadWrite.All' | Out-Null
    }

    process {
        $export = if ($PSCmdlet.ParameterSetName -eq 'FromPath') {
            $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
            if (-not (Test-Path -Path $resolvedPath)) {
                Write-Error "New-CloudPCProvisioningPolicy: export file '$resolvedPath' was not found."
                return
            }
            Get-Content -Path $resolvedPath -Raw | ConvertFrom-Json
        }
        else {
            $InputObject
        }

        if (-not $export.PSObject.Properties['CreateBody']) {
            Write-Error 'New-CloudPCProvisioningPolicy: input must be an export object with a CreateBody property.'
            return
        }

        $body = $export.CreateBody | ConvertTo-Json -Depth 50 | ConvertFrom-Json -AsHashtable
        if ($PSBoundParameters.ContainsKey('DisplayName')) {
            $body['displayName'] = $DisplayName
        }
        if ($PSBoundParameters.ContainsKey('Description')) {
            $body['description'] = $Description
        }
        elseif (-not $body.ContainsKey('description') -or $null -eq $body['description']) {
            $body['description'] = ''
        }

        if ($body.ContainsKey('domainJoinConfigurations')) {
            $body['domainJoinConfigurations'] = @(
                foreach ($configuration in @($body['domainJoinConfigurations'])) {
                    if ($configuration -isnot [System.Collections.IDictionary]) {
                        $configuration = $configuration | ConvertTo-Json -Depth 20 | ConvertFrom-Json -AsHashtable
                    }

                    $joinType = if ($configuration.ContainsKey('domainJoinType')) {
                        $configuration['domainJoinType']
                    }
                    elseif ($configuration.ContainsKey('type')) {
                        $configuration['type']
                    }
                    else {
                        $null
                    }

                    $normalizedConfiguration = [ordered]@{}
                    if ($configuration.ContainsKey('@odata.type')) {
                        $normalizedConfiguration['@odata.type'] = $configuration['@odata.type']
                    }
                    if ($joinType) {
                        $normalizedConfiguration['domainJoinType'] = $joinType
                    }
                    if ($configuration.ContainsKey('onPremisesConnectionId') -and $configuration['onPremisesConnectionId']) {
                        $normalizedConfiguration['onPremisesConnectionId'] = $configuration['onPremisesConnectionId']
                    }

                    $effectiveRegionName = if ($PSBoundParameters.ContainsKey('RegionName')) {
                        $RegionName
                    }
                    elseif ($configuration.ContainsKey('regionName') -and $configuration['regionName'] -and $configuration['regionName'] -ne 'automatic') {
                        $configuration['regionName']
                    }
                    else {
                        $null
                    }

                    if ($effectiveRegionName) {
                        $normalizedConfiguration['regionName'] = $effectiveRegionName
                    }

                    [pscustomobject]$normalizedConfiguration
                }
            )
        }

        if (-not $IncludeAutopilotConfiguration -and $body.ContainsKey('autopilotConfiguration')) {
            $body.Remove('autopilotConfiguration')
        }

        if (-not $body.ContainsKey('displayName') -or [string]::IsNullOrWhiteSpace($body['displayName'])) {
            Write-Error 'New-CloudPCProvisioningPolicy: CreateBody.displayName is required. Use -DisplayName to provide one.'
            return
        }

        $missingRequiredProperties = @(
            foreach ($requiredProperty in @('domainJoinConfigurations','imageDisplayName','imageId','imageType','provisioningType','windowsSetting')) {
                if (-not $body.ContainsKey($requiredProperty) -or $null -eq $body[$requiredProperty]) {
                    $requiredProperty
                }
            }
        )

        if ($missingRequiredProperties.Count -gt 0) {
            Write-Error "New-CloudPCProvisioningPolicy: CreateBody is missing required Graph create field(s): $($missingRequiredProperties -join ', '). Re-export the policy with Export-CloudPCProvisioningPolicy and try again."
            return
        }

        $target = "Cloud PC provisioning policy '$($body['displayName'])'"
        $created = $null
        $status = 'WhatIf'
        $assignStatus = if ($Assign) { 'WhatIf' } else { 'Skipped' }
        $assignmentsApplied = 0
        $errorMessage = $null

        if ($PSCmdlet.ShouldProcess($target, 'Create provisioning policy')) {
            try {
                $uri = 'https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/provisioningPolicies'
                $created = Invoke-MgGraphRequest -Method POST -Uri $uri -ContentType 'application/json' -Body ($body | ConvertTo-Json -Depth 50 -Compress)
                $status = 'Created'
            }
            catch {
                $status = 'Failed'
                $errorMessage = if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                    $_.ErrorDetails.Message
                }
                else {
                    $_.Exception.Message
                }
                Write-Error -Message "New-CloudPCProvisioningPolicy: create failed for $target. $errorMessage" -Exception $_.Exception
            }
        }

        if ($created -and $Assign) {
            $createdId = $created.id
            $assignmentExports = @($export.Assignments)
            if (-not $createdId) {
                $assignStatus = 'Failed'
                $errorMessage = 'Graph create response did not include an id, so assignments could not be applied.'
                Write-Error "New-CloudPCProvisioningPolicy: $errorMessage"
            }
            elseif ($assignmentExports.Count -eq 0) {
                $assignStatus = 'Skipped'
            }
            else {
                $hasAllotmentLicensesCountOverride = $PSBoundParameters.ContainsKey('AllotmentLicensesCount')
                $buildAssignments = {
                    param(
                        [bool]$IncludeAssignmentId
                    )

                    foreach ($assignment in $assignmentExports) {
                        if (-not $assignment.GroupId) { continue }
                        $targetType = if ($assignment.TargetType) { [string]$assignment.TargetType } else { 'microsoft.graph.cloudPcManagementGroupAssignmentTarget' }
                        $targetType = $targetType.TrimStart('#')

                        $assignmentBody = [ordered]@{
                            target = [ordered]@{
                                '@odata.type' = $targetType
                                groupId       = $assignment.GroupId
                            }
                        }

                        if ($assignment.PSObject.Properties['ServicePlanId'] -and $assignment.ServicePlanId) {
                            $assignmentBody.target['servicePlanId'] = $assignment.ServicePlanId
                        }
                        $effectiveAllotmentLicensesCount = if ($hasAllotmentLicensesCountOverride) {
                            $AllotmentLicensesCount
                        }
                        elseif ($assignment.PSObject.Properties['AllotmentLicensesCount'] -and $null -ne $assignment.AllotmentLicensesCount) {
                            $assignment.AllotmentLicensesCount
                        }
                        else {
                            $null
                        }

                        if ($null -ne $effectiveAllotmentLicensesCount) {
                            $assignmentBody.target['allotmentLicensesCount'] = $effectiveAllotmentLicensesCount
                        }
                        if ($assignment.PSObject.Properties['AllotmentDisplayName'] -and $assignment.AllotmentDisplayName) {
                            $assignmentBody.target['allotmentDisplayName'] = $assignment.AllotmentDisplayName
                        }

                        if ($IncludeAssignmentId) {
                            $assignmentBody['id'] = "$createdId`_$($assignment.GroupId)"
                        }

                        $assignmentBody
                    }
                }

                $includeAssignmentId = $body['provisioningType'] -eq 'dedicated'
                $assignments = @(& $buildAssignments -IncludeAssignmentId $includeAssignmentId)

                if ($body['provisioningType'] -ne 'dedicated') {
                    $missingServicePlanAssignments = @($assignments | Where-Object { -not $_.target.Contains('servicePlanId') -or -not $_.target['servicePlanId'] })
                    if ($missingServicePlanAssignments.Count -gt 0) {
                        $assignStatus = 'Failed'
                        $errorMessage = "Shared provisioning policy assignments require servicePlanId. Re-export the source policy with Export-CloudPCProvisioningPolicy and try again."
                        Write-Error "New-CloudPCProvisioningPolicy: $errorMessage"
                        $assignments = @()
                    }
                }
                if ($body['provisioningType'] -eq 'sharedByEntraGroup') {
                    $missingAllotmentAssignments = @($assignments | Where-Object { -not $_.target.Contains('allotmentLicensesCount') -or $null -eq $_.target['allotmentLicensesCount'] })
                    if ($missingAllotmentAssignments.Count -gt 0) {
                        $assignStatus = 'Failed'
                        $errorMessage = "Shared by Entra group provisioning policy assignments require allotmentLicensesCount. Re-export the source policy with Export-CloudPCProvisioningPolicy and try again."
                        Write-Error "New-CloudPCProvisioningPolicy: $errorMessage"
                        $assignments = @()
                    }
                }

                if ($assignments.Count -gt 0) {
                    $assignTarget = "Assignments for Cloud PC provisioning policy '$($body['displayName'])'"
                    if ($PSCmdlet.ShouldProcess($assignTarget, 'Assign provisioning policy to exported groups')) {
                        try {
                            $escapedId = [uri]::EscapeDataString($createdId)
                            $assignUri = "https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/provisioningPolicies/$escapedId/assign"
                            Invoke-MgGraphRequest -Method POST -Uri $assignUri -ContentType 'application/json' -Body (@{ assignments = @($assignments) } | ConvertTo-Json -Depth 20 -Compress) | Out-Null
                            $assignStatus = 'Assigned'
                            $assignmentsApplied = $assignments.Count
                        }
                        catch {
                            $assignStatus = 'Failed'
                            $errorMessage = if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                                $_.ErrorDetails.Message
                            }
                            else {
                                $_.Exception.Message
                            }

                            if ($includeAssignmentId -and $errorMessage -match 'Invalid properties:\s*Id') {
                                try {
                                    $assignments = @(& $buildAssignments -IncludeAssignmentId $false)
                                    Invoke-MgGraphRequest -Method POST -Uri $assignUri -ContentType 'application/json' -Body (@{ assignments = @($assignments) } | ConvertTo-Json -Depth 20 -Compress) | Out-Null
                                    $assignStatus = 'Assigned'
                                    $assignmentsApplied = $assignments.Count
                                    $errorMessage = $null
                                }
                                catch {
                                    $errorMessage = if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                                        $_.ErrorDetails.Message
                                    }
                                    else {
                                        $_.Exception.Message
                                    }
                                    Write-Error -Message "New-CloudPCProvisioningPolicy: assignment failed for $target. $errorMessage" -Exception $_.Exception
                                }
                            }
                            else {
                                Write-Error -Message "New-CloudPCProvisioningPolicy: assignment failed for $target. $errorMessage" -Exception $_.Exception
                            }
                        }
                    }
                }
            }
        }

        [pscustomobject]@{
            PSTypeName          = 'WindowsCloudPC.ProvisioningPolicyCreateResult'
            SourceId            = $export.SourceId
            Id                  = if ($created) { $created.id } else { $null }
            DisplayName         = $body['displayName']
            Status              = $status
            AssignmentStatus    = $assignStatus
            AssignmentsApplied  = $assignmentsApplied
            ErrorMessage        = $errorMessage
            Raw                 = $created
        }
    }

    end { }
}