Public/Update-M365PTaskSmart.ps1

function Update-M365PTaskSmart {
    <#
    .SYNOPSIS
        Updates a Planner task with automatic ETag handling and concurrency control.
 
    .DESCRIPTION
        This function provides a safe way to update Planner tasks by automatically handling
        ETag concurrency control. It retrieves the current ETag before updating and implements
        retry logic for 412 Precondition Failed errors caused by concurrent modifications.
 
    .PARAMETER TaskId
        The ID of the task to update.
 
    .PARAMETER Title
        Optional. New title for the task.
 
    .PARAMETER BucketId
        Optional. Move the task to a different bucket by specifying the new bucket ID.
 
    .PARAMETER PercentComplete
        Optional. Update the completion percentage (0-100).
 
    .PARAMETER StartDateTime
        Optional. Set or update the task start date.
 
    .PARAMETER DueDateTime
        Optional. Set or update the task due date.
 
    .PARAMETER Priority
        Optional. Set task priority (0-10, where 0 is urgent and 10 is low priority).
 
    .PARAMETER MaxRetries
        Optional. Maximum number of retry attempts for 412 errors. Default is 1.
 
    .EXAMPLE
        Update-M365PTaskSmart -TaskId "task123" -Title "Updated Task Title"
         
        Updates only the task title with automatic ETag handling.
 
    .EXAMPLE
        Update-M365PTaskSmart -TaskId "task123" -PercentComplete 50 -Priority 5
         
        Updates multiple properties of the task.
 
    .EXAMPLE
        Get-MgPlannerPlanTask -PlannerPlanId "plan123" | Where-Object {$_.Title -like "*Review*"} |
            ForEach-Object { Update-M365PTaskSmart -TaskId $_.Id -PercentComplete 100 }
         
        Marks all tasks containing "Review" as complete using pipeline input.
 
    .EXAMPLE
        Update-M365PTaskSmart -TaskId "task123" -DueDateTime "2026-03-01" -MaxRetries 3
         
        Updates the due date with up to 3 retry attempts on concurrency conflicts.
 
    .NOTES
        Requires Microsoft.Graph.Planner module and appropriate permissions:
        - Tasks.ReadWrite
         
        This function implements automatic retry logic for ETag conflicts (HTTP 412).
        The ETag is automatically retrieved and passed to the update operation.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]$TaskId,

        [Parameter(Mandatory = $false)]
        [string]$Title,

        [Parameter(Mandatory = $false)]
        [string]$BucketId,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 100)]
        [int]$PercentComplete,

        [Parameter(Mandatory = $false)]
        [datetime]$StartDateTime,

        [Parameter(Mandatory = $false)]
        [datetime]$DueDateTime,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 10)]
        [int]$Priority,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 5)]
        [int]$MaxRetries = 1
    )

    begin {
        Write-Verbose "Starting Update-M365PTaskSmart"
        
        # Function to perform the actual update with ETag
        function Invoke-TaskUpdate {
            param(
                [string]$TaskId,
                [hashtable]$UpdateParams,
                [int]$AttemptNumber
            )

            try {
                # Get current task to retrieve ETag
                Write-Verbose "Attempt $AttemptNumber : Retrieving current task state for ETag..."
                $currentTask = Get-MgPlannerTask -PlannerTaskId $TaskId
                
                if (-not $currentTask) {
                    throw "Task not found: $TaskId"
                }

                $etag = $currentTask.AdditionalProperties['@odata.etag']
                Write-Verbose "Current ETag: $etag"

                # Add ETag to update parameters
                $UpdateParams['IfMatch'] = $etag
                $UpdateParams['PlannerTaskId'] = $TaskId

                # Perform the update
                Write-Verbose "Executing update operation..."
                $updatedTask = Update-MgPlannerTask @UpdateParams
                
                Write-Verbose "Task updated successfully"
                return @{
                    Success = $true
                    Task = $updatedTask
                    Attempts = $AttemptNumber
                }
            }
            catch {
                # Check if this is a 412 Precondition Failed error
                if ($_.Exception.Message -match '412|Precondition Failed|etag') {
                    Write-Verbose "ETag conflict detected (412 Precondition Failed)"
                    return @{
                        Success = $false
                        Error = $_
                        IsETagConflict = $true
                        Attempts = $AttemptNumber
                    }
                }
                else {
                    # Different error, don't retry
                    Write-Verbose "Non-ETag error occurred: $($_.Exception.Message)"
                    return @{
                        Success = $false
                        Error = $_
                        IsETagConflict = $false
                        Attempts = $AttemptNumber
                    }
                }
            }
        }
    }

    process {
        try {
            Write-Verbose "Processing task: $TaskId"

            # Build update parameters hashtable
            $updateParams = @{}

            if ($PSBoundParameters.ContainsKey('Title')) {
                $updateParams['Title'] = $Title
            }

            if ($PSBoundParameters.ContainsKey('BucketId')) {
                $updateParams['BucketId'] = $BucketId
            }

            if ($PSBoundParameters.ContainsKey('PercentComplete')) {
                $updateParams['PercentComplete'] = $PercentComplete
            }

            if ($PSBoundParameters.ContainsKey('StartDateTime')) {
                $updateParams['StartDateTime'] = $StartDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ')
            }

            if ($PSBoundParameters.ContainsKey('DueDateTime')) {
                $updateParams['DueDateTime'] = $DueDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ')
            }

            if ($PSBoundParameters.ContainsKey('Priority')) {
                $updateParams['Priority'] = $Priority
            }

            # Check if there are any parameters to update
            if ($updateParams.Count -eq 0) {
                Write-Warning "No update parameters specified for task: $TaskId"
                return
            }

            Write-Verbose "Update parameters: $($updateParams.Keys -join ', ')"

            # WhatIf support
            if ($PSCmdlet.ShouldProcess("Task $TaskId", "Update with parameters: $($updateParams.Keys -join ', ')")) {
                
                # Attempt update with retry logic
                $attemptNumber = 1
                $result = $null

                do {
                    Write-Verbose "Update attempt $attemptNumber of $($MaxRetries + 1)"
                    
                    $result = Invoke-TaskUpdate -TaskId $TaskId -UpdateParams $updateParams -AttemptNumber $attemptNumber

                    if ($result.Success) {
                        # Success!
                        Write-Verbose "Task updated successfully on attempt $($result.Attempts)"
                        
                        return [PSCustomObject]@{
                            TaskId = $TaskId
                            Status = 'Success'
                            Attempts = $result.Attempts
                            UpdatedProperties = $updateParams.Keys
                        }
                    }
                    elseif ($result.IsETagConflict -and $attemptNumber -le $MaxRetries) {
                        # ETag conflict and we have retries left
                        Write-Warning "ETag conflict on attempt $attemptNumber. Retrying..."
                        Start-Sleep -Milliseconds (500 * $attemptNumber)  # Exponential backoff
                        $attemptNumber++
                    }
                    else {
                        # Either not an ETag conflict, or we're out of retries
                        if ($result.IsETagConflict) {
                            $errorMessage = "Failed to update task after $($result.Attempts) attempts due to repeated ETag conflicts. The task is being modified by another process."
                        }
                        else {
                            $errorMessage = "Failed to update task: $($result.Error.Exception.Message)"
                        }
                        
                        Write-Error $errorMessage
                        
                        return [PSCustomObject]@{
                            TaskId = $TaskId
                            Status = 'Failed'
                            Attempts = $result.Attempts
                            Error = $errorMessage
                        }
                    }

                } while ($attemptNumber -le ($MaxRetries + 1))
            }
        }
        catch {
            Write-Error "Unexpected error updating task $TaskId : $_"
            throw
        }
    }

    end {
        Write-Verbose "Update-M365PTaskSmart completed"
    }
}