diff --git a/ServiceNow/Private/Test-ServiceNowSysId.ps1 b/ServiceNow/Private/Test-ServiceNowSysId.ps1 new file mode 100644 index 0000000..f9246ae --- /dev/null +++ b/ServiceNow/Private/Test-ServiceNowSysId.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + Tests if a string is a valid ServiceNow sys_id format + +.DESCRIPTION + Validates that a string matches the ServiceNow sys_id format of a 32-character alphanumeric string + +.PARAMETER InputObject + The string value to test + +.EXAMPLE + Test-ServiceNowSysId -InputObject '9d385017c611228701d22104cc95c371' + Returns $true + +.EXAMPLE + Test-ServiceNowSysId -InputObject 'INC0010001' + Returns $false + +.OUTPUTS + System.Boolean +#> +function Test-ServiceNowSysId { + [CmdletBinding()] + [OutputType([System.Boolean])] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [AllowEmptyString()] + [string] $InputObject + ) + + process { + if ([string]::IsNullOrEmpty($InputObject)) { + return $false + } + + return $InputObject -match '^[a-zA-Z0-9]{32}$' + } +} diff --git a/ServiceNow/Public/Get-ServiceNowCart.ps1 b/ServiceNow/Public/Get-ServiceNowCart.ps1 new file mode 100644 index 0000000..a99584a --- /dev/null +++ b/ServiceNow/Public/Get-ServiceNowCart.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS + Get the cart of the current user +.DESCRIPTION + Get the cart of the current user +.PARAMETER ServiceNowSession + ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession. +.EXAMPLE + Get-ServiceNowCart + Get the cart of the current user +.INPUTS + None +.OUTPUTS + PSCustomObject representing the cart +#> +function Get-ServiceNowCart { + param + ( + [Parameter()] + [hashtable]$ServiceNowSession = $script:ServiceNowSession + ) + + $params = @{ + Method = 'Get' + UriLeaf = "/servicecatalog/cart" + Namespace = 'sn_sc' + ServiceNowSession = $ServiceNowSession + } + Invoke-ServiceNowRestMethod @params | Select-Object @{n = 'sys_id'; e = { $_.cart_id } }, * -ExcludeProperty cart_id +} \ No newline at end of file diff --git a/ServiceNow/Public/New-ServiceNowCartItem.ps1 b/ServiceNow/Public/New-ServiceNowCartItem.ps1 new file mode 100644 index 0000000..25a883f --- /dev/null +++ b/ServiceNow/Public/New-ServiceNowCartItem.ps1 @@ -0,0 +1,171 @@ +<# +.SYNOPSIS + Add item to the cart of the current user + +.DESCRIPTION + Adds a catalog item to the cart of the current user and optionally checks out the cart to create the order. + +.PARAMETER CatalogItem + Name or ID of the catalog item that will be created. + Use Tab or Menu Completion to see available catalog items. + +.PARAMETER Quantity + Quantity of the catalog item to request. Default is 1. + +.PARAMETER ItemValues + Key/value pairs of variable names and their values + +.PARAMETER Checkout + Checkout the cart after adding the item to create the order. + If not checking out, you can use Submit-ServiceNowCatalogOrder to checkout later. + +.PARAMETER PassThru + If provided, the new record, either cart addition or checked out order, will be returned + +.PARAMETER ServiceNowSession + ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession. + +.EXAMPLE + New-ServiceNowCartItem -CatalogItem "Standard Laptop" + + Add 1 of the catalog item to the cart without checking out + +.EXAMPLE + New-ServiceNowCartItem -CatalogItem "Standard Laptop" -Quantity 3 + + Add 3 of the catalog item to the cart without checking out + +.EXAMPLE + New-ServiceNowCartItem -CatalogItem "Standard Laptop" -Quantity 3 -Checkout + + Add 3 of the catalog item to the cart and checkout to create the order + +.EXAMPLE + 'Standard Laptop', 'Adobe Acrobat Pro' | New-ServiceNowCartItem + + Add multiple catalog items to the cart + +.EXAMPLE + New-ServiceNowCartItem -CatalogItem "Standard Laptop" -PassThru + + Add a catalog item to the cart and return the cart details + +.EXAMPLE + New-ServiceNowCartItem -CatalogItem "Standard Laptop" -PassThru -Checkout + + Add a catalog item to the cart, checkout to create the order, and return the order details + +.EXAMPLE + 'Packaging and Shipping' | New-ServiceNowCartItem -ItemValues @{'type'='Inter-office';'parcel_details'='fragile'} + + Add a catalog item to the cart with mandatory values + +.EXAMPLE + New-ServiceNowCartItem -CatalogItem 'ce40793b53d6ba10295d38e0a0490e86' + + Add a catalog item to the cart using its sys_id + +.INPUTS + CatalogItem + +.OUTPUTS + PSCustomObject if PassThru provided +#> +function New-ServiceNowCartItem { + [CmdletBinding(SupportsShouldProcess)] + [Alias('Add-ServiceNowCartItem')] + + param + ( + [Parameter(Mandatory, ValueFromPipeline)] + [string] $CatalogItem, + + [Parameter()] + [ValidateRange(1, [int32]::MaxValue)] + [int32] $Quantity = 1, + + [Parameter()] + [hashtable]$ItemValues, + + [Parameter()] + [switch]$Checkout, + + [Parameter()] + [switch]$PassThru, + + [Parameter()] + [hashtable]$ServiceNowSession = $script:ServiceNowSession + ) + + process { + if ($CatalogItem | Test-ServiceNowSysId ) { + #Verify the sys_id of the Catalog Item + $catalogItemID = Get-ServiceNowRecord -Table sc_cat_item -AsValue -ID $CatalogItem -Property sys_id + } + else { + #Lookup the sys_id of the Catalog Item + $catalogItemID = Get-ServiceNowRecord -Table sc_cat_item -AsValue -Filter @('name', '-eq', $CatalogItem ) -Property sys_id + } + + if ([string]::IsNullOrEmpty($catalogItemID)) { + throw "Unable to find catalog item '$CatalogItem'" + } + else { + Write-Verbose "Found $catalogItemID via lookup from '$CatalogItem'" + } + + $addItemToCart = @{ + Method = 'Post' + UriLeaf = "/servicecatalog/items/{0}/add_to_cart" -f $catalogItemID + Values = @{ + 'sysparm_quantity' = $Quantity + } + Namespace = 'sn_sc' + ServiceNowSession = $ServiceNowSession + } + + if ( $ItemValues ) { + $addItemToCart.Values.variables = $ItemValues + } + + if ( $PSCmdlet.ShouldProcess($catalogItemID, 'Create new catalog item request') ) { + + try { + $addItemCartResponse = Invoke-ServiceNowRestMethod @addItemToCart + } + catch { + if ( ($_.ErrorDetails.Message | ConvertFrom-Json | Select-Object -ExpandProperty error | Select-Object -ExpandProperty message) -match 'Mandatory Variables are required' ) { + $catItem = Invoke-ServiceNowRestMethod -UriLeaf "/servicecatalog/items/$catalogItemID" -Namespace 'sn_sc' + $mandatoryVars = $catItem.variables | Where-Object { $_.mandatory -eq $true } | Select-Object -ExpandProperty name + throw ('Failed to add item to cart. The following mandatory variables must be provided: {0}' -f ($mandatoryVars -join ', ')) + } + else { + throw $_ + } + } + + if ( -not $addItemCartResponse.cart_id ) { + throw ('Failed to add item to cart. Response: {0}' -f ($addItemCartResponse | ConvertTo-Json)) + } + else { + Write-Verbose "Added item to cart with Cart ID: $($addItemCartResponse.cart_id)" + $out = $addItemCartResponse + } + + Write-Verbose ("Current cart items:`n{0}" -f ($addItemCartResponse.items | Select-Object item_name, quantity, price | Out-String)) + Write-Verbose ('Cart Total: {0}' -f $addItemCartResponse.subtotal) + } + } + + end { + if ( $Checkout ) { + $submitResponse = Submit-ServiceNowCart -ServiceNowSession $ServiceNowSession -PassThru + Write-Verbose 'Order submitted successfully.' + $out = $submitResponse + } + + if ( $PassThru ) { + $out + } + } +} \ No newline at end of file diff --git a/ServiceNow/Public/New-ServiceNowCatalogItem.ps1 b/ServiceNow/Public/New-ServiceNowCatalogItem.ps1 deleted file mode 100644 index 94c90c8..0000000 --- a/ServiceNow/Public/New-ServiceNowCatalogItem.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -<# -.SYNOPSIS - Submit a catalog request using Service Catalog API - -.DESCRIPTION - Create a new catalog item request using Service Catalog API. Reference: https://www.servicenow.com/community/itsm-articles/submit-catalog-request-using-service-catalog-api/ta-p/2305836 - -.PARAMETER CatalogItem - Name or ID of the catalog item that will be created - -.PARAMETER Variables - Key/value pairs of variable names and their values - -.PARAMETER CheckoutImmediately - If provided, a second Post for cart checkout to submit_order API Endpoint will be sent - -.PARAMETER PassThru - If provided, the new record will be returned - -.PARAMETER Connection - Azure Automation Connection object containing username, password, and URL for the ServiceNow instance - -.PARAMETER ServiceNowSession - ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession. - -.EXAMPLE - New-ServiceNowCatalogItem -CatalogItem "Standard Laptop" -Variables @{'acrobat' = 'true'; 'photoshop' = 'true'; ' Additional_software_requirements' = 'Testing Service catalog API' } - - Raise a new catalog request using Item Name - -.EXAMPLE - New-ServiceNowCatalogItem -CatalogItem "04b7e94b4f7b42000086eeed18110c7fd" -Variables @{'acrobat' = 'true'; 'photoshop' = 'true'; ' Additional_software_requirements' = 'Testing Service catalog API' } - - Raise a new catalog request using Item ID - -.INPUTS - InputData - -.OUTPUTS - PSCustomObject if PassThru provided -#> -function New-ServiceNowCatalogItem { - [CmdletBinding(SupportsShouldProcess)] - param - ( - [Parameter(Mandatory)] - [string]$CatalogItem, - [Parameter(Mandatory)] - [Alias('Variables')] - [hashtable]$InputData, - [Parameter()][Hashtable]$Connection, - [Parameter()][hashtable]$ServiceNowSession = $script:ServiceNowSession, - [Parameter()][switch]$CheckoutImmediately, - [Parameter()][switch]$PassThru - ) - - begin { - if (-not $PSBoundParameters.ContainsKey('CheckoutImmediately')) { - $CheckoutImmediately = $false - } - if ($CatalogItem -match '^[a-zA-Z0-9]{32}$') { - #Verify the sys_id of the Catalog Item - $CatalogItemID = Get-ServiceNowRecord -Table sc_cat_item -AsValue -ID $CatalogItem -Property sys_id - if ([string]::IsNullOrEmpty($CatalogItemID)) { throw "Unable to find catalog item by ID '$($CatalogItem)'" } else { Write-Verbose "Found $($catalogitemid) via lookup from '$($CatalogItem)'" } - } else { - #Lookup the sys_id of the Catalog Item - $CatalogItemID = Get-ServiceNowRecord -Table sc_cat_item -AsValue -Filter @('name', '-eq', $CatalogItem ) -Property sys_id - if ([string]::IsNullOrEmpty($CatalogItemID)) { throw "Unable to find catalog item by name '$($CatalogItem)'" } else { Write-Verbose "Found $($catalogitemid) via lookup from '$($CatalogItem)'" } - } - } - process { - - $AddItemToCart = @{ - Method = 'Post' - UriLeaf = "/servicecatalog/items/{0}/add_to_cart" -f $CatalogItemID - Values = @{'sysparm_quantity' = 1; 'variables' = $InputData } - Namespace = 'sn_sc' - Connection = $Connection - ServiceNowSession = $ServiceNowSession - } - - if ( $PSCmdlet.ShouldProcess($CatalogItemID, 'Create new catalog item request') ) { - - $AddItemCartResponse = Invoke-ServiceNowRestMethod @AddItemToCart - - if ($AddItemCartResponse.cart_id -and $CheckoutImmediately) { - $SubmitOrder = @{ - Method = 'Post' - UriLeaf = "/servicecatalog/cart/submit_order" - Namespace = 'sn_sc' - Connection = $Connection - ServiceNowSession = $ServiceNowSession - } - - $SubmitOrderResponse = Invoke-ServiceNowRestMethod @SubmitOrder - - if ($PassThru) { - $SubmitOrderResponse | Select-Object @{'n' = 'number'; 'e' = { $_.request_number } }, request_id - } - } - } else { - $AddItemToCart | Out-String - } - } -} \ No newline at end of file diff --git a/ServiceNow/Public/Remove-ServiceNowCartItem.ps1 b/ServiceNow/Public/Remove-ServiceNowCartItem.ps1 new file mode 100644 index 0000000..91558a3 --- /dev/null +++ b/ServiceNow/Public/Remove-ServiceNowCartItem.ps1 @@ -0,0 +1,103 @@ +<# +.SYNOPSIS + Remove cart items + +.DESCRIPTION + Removes a specific catalog item from the cart or empties the entire cart. + +.PARAMETER CartItemId + SysId of the cart item to be removed from the current user. + Must be a 32 character alphanumeric string. + +.PARAMETER All + Remove all items from the cart (empty the cart). + +.PARAMETER CartId + SysId of the cart to be emptied. If not provided, the current user's cart will be looked up. + Must be a 32 character alphanumeric string. + +.PARAMETER ServiceNowSession + ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession. + +.EXAMPLE + Remove-ServiceNowCartItem -CartItemId "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" + + Remove a specific item from the cart + +.EXAMPLE + Remove-ServiceNowCartItem -All + + Remove all items from the current user's cart + +.EXAMPLE + Remove-ServiceNowCartItem -All -CartId "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" + + Remove all items from the specified cart + +.INPUTS + CartItemId + +.OUTPUTS + None +#> +function Remove-ServiceNowCartItem { + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByCatalogItemId')] + [ValidateScript( { + if ($_ | Test-ServiceNowSysId) { + $true + } + else { + throw 'CartItemId must be a SysId 32 character alphanumeric' + } + })] + [string] $CartItemId, + + [Parameter(Mandatory, ParameterSetName = 'All')] + [switch] $All, + + [Parameter(ParameterSetName = 'All')] + [ValidateScript( { + if ($_ | Test-ServiceNowSysId) { + $true + } + else { + throw 'CartId must be a SysId 32 character alphanumeric' + } + })] + [string] $CartId, + + [Parameter()] + [hashtable]$ServiceNowSession = $script:ServiceNowSession + ) + + process { + + $params = @{ + Method = 'Delete' + Namespace = 'sn_sc' + ServiceNowSession = $ServiceNowSession + } + + if ( $CartItemId ) { + $params.UriLeaf = "/servicecatalog/cart/{0}" -f $CartItemId + } + else { + # remove a cart and its items + # we need the cart id so if we don't have it, look up the current user's cart + if ( -not $CartId ) { + $cart = Get-ServiceNowCart -ServiceNowSession $ServiceNowSession + $cartId = $cart.sys_id + } + Write-Verbose "Emptying cart with sys_id $cartId" + $params.UriLeaf = "/servicecatalog/cart/$cartId/empty" + } + + $target = if ($CartItemId) { "Remove item $CartItemId from current cart" } else { "Remove cart $cartId completely" } + if ( $PSCmdlet.ShouldProcess($target) ) { + Invoke-ServiceNowRestMethod @params + } + } +} \ No newline at end of file diff --git a/ServiceNow/Public/Submit-ServiceNowCart.ps1 b/ServiceNow/Public/Submit-ServiceNowCart.ps1 new file mode 100644 index 0000000..46091e4 --- /dev/null +++ b/ServiceNow/Public/Submit-ServiceNowCart.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS + Submit the user cart for checkout + +.DESCRIPTION + Checks out the user cart, based on the current check-out type (one-step or two-step) + +.PARAMETER PassThru + If provided, the new record id will be returned + +.PARAMETER ServiceNowSession + ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession. + +.EXAMPLE + Submit-ServiceNowCart + + Checks out the user cart, based on the current check-out type (one-step or two-step). + +.EXAMPLE + Submit-ServiceNowCart -PassThru + + Checks out the user cart, based on the current check-out type (one-step or two-step) and returns the request number and request ID. + +.LINK + https://developer.servicenow.com/dev.do#!/reference/api/zurich/rest/c_ServiceCatalogAPI#servicecat-POST-cart-sub_order?navFilter=serv + +.OUTPUTS + PSCustomObject if PassThru provided +#> +function Submit-ServiceNowCart { + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter()] + [switch]$PassThru, + + [Parameter()] + [hashtable]$ServiceNowSession = $script:ServiceNowSession + ) + + process { + + if ( $PSCmdlet.ShouldProcess('Checkout current user cart') ) { + $submitOrder = @{ + Method = 'Post' + UriLeaf = "/servicecatalog/cart/submit_order" + Namespace = 'sn_sc' + ServiceNowSession = $ServiceNowSession + } + + $submitOrderResponse = Invoke-ServiceNowRestMethod @submitOrder + + if ($PassThru) { + if ( $submitOrderResponse.request_number ) { + $submitOrderResponse | Select-Object @{'n' = 'number'; 'e' = { $_.request_number } }, request_id + } + else { + $submitOrderResponse + } + } + + } + } +} \ No newline at end of file diff --git a/ServiceNow/Public/Submit-ServiceNowCatalogOrder.ps1 b/ServiceNow/Public/Submit-ServiceNowCatalogOrder.ps1 deleted file mode 100644 index bf45b33..0000000 --- a/ServiceNow/Public/Submit-ServiceNowCatalogOrder.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -<# -.SYNOPSIS - Submit a catalog request using Service Catalog API - -.DESCRIPTION - Checks out the user cart, based on the current check-out type (one-step or two-step). Reference: https://developer.servicenow.com/dev.do#!/reference/api/zurich/rest/c_ServiceCatalogAPI#servicecat-POST-cart-sub_order?navFilter=serv - -.PARAMETER PassThru - If provided, the new record will be returned - -.PARAMETER Connection - Azure Automation Connection object containing username, password, and URL for the ServiceNow instance - -.PARAMETER ServiceNowSession - ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession. - -.EXAMPLE - Submit-ServiceNowCatalogOrder - - Checks out the user cart, based on the current check-out type (one-step or two-step). - -.EXAMPLE - Submit-ServiceNowCatalogOrder -PassThru - - Checks out the user cart, based on the current check-out type (one-step or two-step) and returns the request numbers as an object. - -.INPUTS - InputData - -.OUTPUTS - PSCustomObject if PassThru provided -#> -function Submit-ServiceNowCatalogOrder { - [CmdletBinding(SupportsShouldProcess)] - param - ( - [Parameter()][Hashtable]$Connection, - [Parameter()][hashtable]$ServiceNowSession = $script:ServiceNowSession, - [Parameter()][switch]$PassThru - ) - - process { - - if ( $PSCmdlet.ShouldProcess('POST cart to Submit_Order API') ) { - $SubmitOrder = @{ - Method = 'Post' - UriLeaf = "/servicecatalog/cart/submit_order" - Namespace = 'sn_sc' - Connection = $Connection - ServiceNowSession = $ServiceNowSession - } - - $SubmitOrderResponse = Invoke-ServiceNowRestMethod @SubmitOrder - - if ($PassThru) { - $SubmitOrderResponse | Select-Object @{'n' = 'number'; 'e' = { $_.request_number } }, request_id - } - - } else { - Write-Output "Checks out the user cart, based on the current check-out type (one-step or two-step).`n`nIf one-step checkout, the method checks out (saves) the cart and returns the request number and the request order ID. If two-step checkout, the method returns the cart order status and all the information required for two-step checkout." - } - } -} \ No newline at end of file diff --git a/ServiceNow/ServiceNow.psd1 b/ServiceNow/ServiceNow.psd1 index afd8a89..5f8252a 100644 --- a/ServiceNow/ServiceNow.psd1 +++ b/ServiceNow/ServiceNow.psd1 @@ -76,7 +76,9 @@ FunctionsToExport = 'New-ServiceNowConfigurationItem', 'Get-ServiceNowRecord', 'New-ServiceNowQuery', 'New-ServiceNowRecord', 'Remove-ServiceNowAttachment', 'Remove-ServiceNowRecord', 'Update-ServiceNowRecord', 'Export-ServiceNowRecord', - 'Invoke-ServiceNowGraphQL', 'New-ServiceNowChangeTask' + 'Invoke-ServiceNowGraphQL', 'New-ServiceNowChangeTask', + 'Get-ServiceNowCart', 'New-ServiceNowCartItem', + 'Remove-ServiceNowCartItem', 'Submit-ServiceNowCart' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() @@ -85,7 +87,7 @@ CmdletsToExport = @() VariablesToExport = 'ServiceNowSession', 'ServiceNowOperator', 'ServiceNowTable' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = 'gsnr' +AliasesToExport = 'gsnr', 'Add-ServiceNowCartItem' # DSC resources to export from this module # DscResourcesToExport = @() diff --git a/ServiceNow/ServiceNow.psm1 b/ServiceNow/ServiceNow.psm1 index 97fe40b..fffb197 100644 --- a/ServiceNow/ServiceNow.psm1 +++ b/ServiceNow/ServiceNow.psm1 @@ -9,6 +9,36 @@ $Script:ServiceNowOperator = $config.FilterOperators Export-ModuleMember -Variable ServiceNowOperator, ServiceNowTable +$script:catalogItems = [System.Collections.Generic.List[object]]::new() + +$tableLookupArgCompleterSb = { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + + switch ($parameterName) { + 'CatalogItem' { + if ( $script:catalogItems.Count -eq 0 ) { + $allItems = Get-ServiceNowRecord -Table sc_cat_item -Property sys_id, name, short_description -IncludeTotalCount -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -ServiceNowSession $script:ServiceNowSession + $script:catalogItems.AddRange($allItems) + } + + $out = $script:catalogItems + if ( $wordToComplete ) { + $out = $script:catalogItems | Where-Object { + ($_.sys_id -like ('{0}*' -f $wordToComplete.Trim("'"))) -or + ($_.name -like ('{0}*' -f $wordToComplete.Trim("'"))) + } + } + $out | ForEach-Object { + $itemText = "'{0}'" -f $_.name + $itemDescription = if ($_.short_description) { $_.short_description } else { ' ' } + [System.Management.Automation.CompletionResult]::new($itemText, $_.name, 'ParameterValue', $itemDescription) + } + } + } +} + +Register-ArgumentCompleter -CommandName 'New-ServiceNowCartItem' -ParameterName 'CatalogItem' -ScriptBlock $tableLookupArgCompleterSb + $tableArgCompleterSb = { $ServiceNowTable | ForEach-Object { if ( $_.ClassName ) {