Public/Schedules/Add-JIMScheduleStep.ps1

# Copyright (c) Tetron Limited. All rights reserved.
# Licensed under the Tetron Commercial License. See LICENSE file in the project root.

function Add-JIMScheduleStep {
    <#
    .SYNOPSIS
        Adds a step to a Schedule in JIM.
 
    .DESCRIPTION
        Adds a new step to an existing Schedule. Steps define the tasks that
        execute when the schedule runs. Steps can run sequentially or in parallel.
 
    .PARAMETER ScheduleId
        The unique identifier (GUID) of the Schedule to add the step to.
 
    .PARAMETER StepType
        The type of step. RunProfile, which runs a Connected System's Run
        Profile, is the only type.
 
    .PARAMETER ConnectedSystemId
        For a RunProfile step, the id of the Connected System.
 
    .PARAMETER ConnectedSystemName
        For a RunProfile step, the name of the Connected System.
 
    .PARAMETER RunProfileId
        For a RunProfile step, the id of the Run Profile to run.
 
    .PARAMETER RunProfileName
        For a RunProfile step, the name of the Run Profile to run.
 
    .PARAMETER Parallel
        Runs this step at the same time as the step before it, in one step
        group. Without it, the step waits for the step before it to finish.
 
    .PARAMETER OnFailure
        What the Schedule does when this step fails. FollowSchedule (the
        default) does whatever the Schedule is set to do (its OnStepFailure).
        Stop stops the Schedule, even one set to continue. Continue carries on
        with the remaining steps, even when the Schedule is set to stop.
        Cannot be used with -ContinueOnFailure.
 
    .PARAMETER ContinueOnFailure
        Shorthand for -OnFailure Continue: the Schedule carries on if this
        step fails, whatever the Schedule is set to do. Cannot be used with
        -OnFailure; giving both is an error, and nothing is changed.
 
    .PARAMETER ChangeReason
        An optional reason for the change, recorded against this Schedule's change history.
 
    .PARAMETER PassThru
        Returns the updated Schedule.
 
    .OUTPUTS
        With -PassThru, the Schedule in full, with the new step; otherwise nothing. Adding a step changes no other
        step: the existing steps are sent back with their failure settings exactly as they were.
 
    .EXAMPLE
        Add-JIMScheduleStep -ScheduleId "12345678-..." -StepType RunProfile -ConnectedSystemId 1 -RunProfileId 1
 
        Adds a Run Profile step to the schedule.
 
    .EXAMPLE
        Add-JIMScheduleStep -ScheduleId "12345678-..." -StepType RunProfile -ConnectedSystemName "HR System" -RunProfileName "Delta Import" -PassThru
 
        Adds a step using names instead of IDs.
 
    .EXAMPLE
        $schedule = Get-JIMSchedule -Name "Delta Sync"
        Add-JIMScheduleStep -ScheduleId $schedule.id -StepType RunProfile -ConnectedSystemName "HR" -RunProfileName "Import"
        Add-JIMScheduleStep -ScheduleId $schedule.id -StepType RunProfile -ConnectedSystemName "Badge" -RunProfileName "Import" -Parallel
 
        Adds two import steps that run in parallel.
 
    .EXAMPLE
        Add-JIMScheduleStep -ScheduleId "12345678-..." -StepType RunProfile -ConnectedSystemName "Active Directory" -RunProfileName "Export" -OnFailure Stop
 
        Adds an export step that stops the Schedule if it fails, even when the Schedule is set to continue.
 
    .LINK
        Get-JIMSchedule
        New-JIMSchedule
        Remove-JIMScheduleStep
        Set-JIMScheduleStep
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ById')]
    [OutputType('JIM.ScheduleDetailDto')]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias('Id')]
        [guid]$ScheduleId,

        [Parameter(Mandatory)]
        [ValidateSet('RunProfile')]
        [string]$StepType,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [int]$ConnectedSystemId,

        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string]$ConnectedSystemName,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [int]$RunProfileId,

        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string]$RunProfileName,

        [switch]$Parallel,

        [Parameter()]
        [ValidateSet('FollowSchedule', 'Stop', 'Continue')]
        [string]$OnFailure,

        [switch]$ContinueOnFailure,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ChangeReason,

        [switch]$PassThru
    )

    process {
        # -ContinueOnFailure is shorthand for -OnFailure Continue; both at once is ambiguous (which wins?), so refuse it
        # before anything is sent rather than silently preferring one.
        if ($PSBoundParameters.ContainsKey('OnFailure') -and $PSBoundParameters.ContainsKey('ContinueOnFailure')) {
            $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
                [System.ArgumentException]::new('Specify either -OnFailure or -ContinueOnFailure, not both. -ContinueOnFailure is shorthand for -OnFailure Continue.'),
                'ConflictingFailureParameters',
                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                $null))
        }

        $newStepOnFailure = if ($PSBoundParameters.ContainsKey('OnFailure')) {
            $OnFailure
        } elseif ($ContinueOnFailure) {
            'Continue'
        } else {
            'FollowSchedule'
        }

        # Check connection first
        if (-not $script:JIMConnection) {
            Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again."
            return
        }

        # Resolve names to IDs if using ByName parameter set
        if ($PSCmdlet.ParameterSetName -eq 'ByName') {
            try {
                $cs = Resolve-JIMConnectedSystem -Name $ConnectedSystemName
                $ConnectedSystemId = $cs.id

                $runProfiles = Invoke-JIMApi -Endpoint "/api/v1/synchronisation/connected-systems/$ConnectedSystemId/run-profiles"
                $runProfile = $runProfiles | Where-Object { $_.name -eq $RunProfileName }

                if (-not $runProfile) {
                    Write-Error "Run Profile '$RunProfileName' not found for Connected System '$ConnectedSystemName'"
                    return
                }

                $RunProfileId = $runProfile.id
            }
            catch {
                Write-Error "Failed to resolve names: $_"
                return
            }
        }

        if ($PSCmdlet.ShouldProcess($ScheduleId, "Add Schedule Step")) {
            Write-Verbose "Adding step to Schedule: $ScheduleId"

            try {
                # Get existing schedule with steps
                $schedule = Invoke-JIMApi -Endpoint "/api/v1/schedules/$ScheduleId"

                if (-not $schedule) {
                    Write-Error "Schedule not found: $ScheduleId"
                    return
                }

                # Determine step index
                $existingSteps = if ($schedule.steps) { $schedule.steps } else { @() }
                $maxStepIndex = if ($existingSteps.Count -gt 0) {
                    ($existingSteps | Measure-Object -Property stepIndex -Maximum).Maximum
                } else { -1 }

                # If parallel, use same step index as previous; otherwise increment
                $newStepIndex = if ($Parallel -and $maxStepIndex -ge 0) {
                    $maxStepIndex
                } else {
                    $maxStepIndex + 1
                }

                # Build the new step. Enum values must be sent as names, not numbers: the API
                # rejects numeric enum values on request DTOs (#1060). The failure setting goes as
                # onFailure, never continueOnFailure, so no legacy mapping applies (#1787).
                $newStep = @{
                    stepIndex = [int]$newStepIndex
                    stepType = $StepType
                    executionMode = if ($Parallel) { 'ParallelWithPrevious' } else { 'Sequential' }
                    onFailure = $newStepOnFailure
                    connectedSystemId = [int]$ConnectedSystemId
                    runProfileId = [int]$RunProfileId
                }

                # Send the existing steps back as they are, their ids and failure settings included
                # (ConvertTo-JIMScheduleStepRequest), so adding a step changes no other step.
                $convertedSteps = @()
                foreach ($step in $existingSteps) {
                    $convertedSteps += ConvertTo-JIMScheduleStepRequest -Step $step
                }

                # Build update body with existing steps plus new one
                $allSteps = $convertedSteps + @($newStep)

                $body = @{
                    name = $schedule.name
                    description = $schedule.description
                    triggerType = $schedule.triggerType
                    patternType = $schedule.patternType
                    isEnabled = $schedule.isEnabled
                    daysOfWeek = $schedule.daysOfWeek
                    runTimes = $schedule.runTimes
                    intervalValue = $schedule.intervalValue
                    intervalUnit = $schedule.intervalUnit
                    intervalWindowStart = $schedule.intervalWindowStart
                    intervalWindowEnd = $schedule.intervalWindowEnd
                    cronExpression = $schedule.cronExpression
                    steps = $allSteps
                }

                if ($PSBoundParameters.ContainsKey('ChangeReason')) {
                    $body.changeReason = $ChangeReason
                }

                $result = Invoke-JIMApi -Endpoint "/api/v1/schedules/$ScheduleId" -Method 'PUT' -Body $body

                Write-Verbose "Added step to Schedule: $ScheduleId"

                if ($PassThru) {
                    Invoke-JIMApi -Endpoint "/api/v1/schedules/$ScheduleId"
                }
            }
            catch {
                Write-Error "Failed to add Schedule step: $_"
            }
        }
    }
}