Private/FSvc.Tickets.ps1
|
# Ticket querying and triage helpers. # Named saved views. Defined once so the "self-assigned unresolved" and # "unassigned unresolved" query_hash strings cannot drift between commands. $script:FSvcViews = [ordered]@{ SelfAssigned = '[{"condition":"status","operator":"is_in","value":["0"],"type":"default"},{"condition":"responder_id","operator":"is_in","value":["0"],"type":"default"}]' Unassigned = '[{"condition":"status","operator":"is_in","value":["0"],"type":"default"},{"condition":"responder_id","operator":"is_in","value":["-1"],"type":"default"}]' } # The query_hash for a named view. function Get-FSvcViewQueryHash { param([Parameter(Mandatory)][ValidateSet('SelfAssigned', 'Unassigned')][string]$View) return $script:FSvcViews[$View] } # Tickets for a named view, paginated. function Get-FSvcViewTickets { param( [Parameter(Mandatory)][ValidateSet('SelfAssigned', 'Unassigned')][string]$View, [int]$PerPage = 100, [int]$MaxPages = 1000, [hashtable]$Config ) return Invoke-FSvcPagedQuery -Path "tickets" -ArrayKey "tickets" -MaxPages $MaxPages -Config $Config -BaseQuery @{ "order_by" = "created_at" "order_type" = "asc" "per_page" = $PerPage "query_hash" = (Get-FSvcViewQueryHash -View $View) } } # Paginates a tickets query, returning every ticket as an object. function Get-FSvcTickets { param( [string]$QueryHash, [int]$PerPage = 100, [int]$MaxPages = 1000, [hashtable]$Config ) return Invoke-FSvcPagedQuery -Path "tickets" -ArrayKey "tickets" -MaxPages $MaxPages -Config $Config -BaseQuery @{ "order_by" = "created_at" "order_type" = "asc" "per_page" = $PerPage "query_hash" = $QueryHash } } # Most recent conversation for a ticket - any kind, private note or public # reply - or $null when it has none. function Get-FSvcLatestConversation { param([int64]$TicketId, [hashtable]$Config) $query = @{ "order_by" = "created_at" "order_type" = "desc" "per_page" = 1 "page" = 1 } $data = (Invoke-FSvcGet -Path ("tickets/{0}/conversations" -f $TicketId) -Query $query -Config $Config) | ConvertFrom-FSvcJson $c = @($data.conversations) | Select-Object -First 1 if ($null -eq $c) { return $null } return ConvertTo-FSvcConversationView $c } # Normalises a raw API conversation (or the latest-conversation response) into # the single shape consumers use: Id, Author (nested user name else user_id), # UserId, Direction, At, Body (body_text preferred over body). function ConvertTo-FSvcConversationView { param([AllowNull()]$Conversation) if ($null -eq $Conversation) { return $null } $author = if ($Conversation.user -and $Conversation.user.name) { [string]$Conversation.user.name } else { [string]$Conversation.user_id } $direction = if ($Conversation.incoming) { 'incoming' } else { 'outgoing' } $body = if ($Conversation.body_text) { [string]$Conversation.body_text } elseif ($Conversation.body) { [string]$Conversation.body } else { '' } return [pscustomobject]@{ Id = $Conversation.id Author = $author UserId = [int64]$Conversation.user_id Direction = $direction At = ConvertTo-FSDateTimeOffset $Conversation.created_at Body = $body } } # Triage bucket for a self-assigned ticket: unassigned / awaiting_agent / # waiting / none. Mirrors the standalone overview logic. function Get-FSvcTicketCategory { param( [AllowNull()]$ResponderID, [AllowNull()]$LastMessage, [int64]$LastUserID, [datetimeoffset]$CreatedAt, [double]$OlderThanDays, [datetimeoffset]$Now ) if ($null -eq $ResponderID -or [int64]$ResponderID -lt 0) { return "unassigned" } $last = ConvertTo-FSDateTimeOffset $LastMessage if ($null -ne $last -and $LastUserID -ne [int64]$ResponderID) { return "awaiting_agent" } $ref = $CreatedAt if ($null -ne $last) { $ref = $last } if ((Get-FSvcBusinessDaysBetween -From $ref -To $Now) -gt $OlderThanDays) { return "waiting" } return "none" } # The UTC offset evidenced by the tickets' own dates (planned_end_date # preferred, created_at fallback), or the fallback's offset. function Get-FSvcAccountOffset { param($Tickets, [datetimeoffset]$Fallback) foreach ($t in @($Tickets)) { $d = ConvertTo-FSDateTimeOffset $t.planned_end_date if ($null -ne $d) { return $d.Offset } } foreach ($t in @($Tickets)) { $d = ConvertTo-FSDateTimeOffset $t.created_at if ($null -ne $d) { return $d.Offset } } return $Fallback.Offset } # Renders a business-days number with one decimal, invariant culture. function Format-FSvcDays { param([double]$Days) $rounded = [math]::Round($Days, 1, [System.MidpointRounding]::AwayFromZero) return $rounded.ToString("0.0", [System.Globalization.CultureInfo]::InvariantCulture) } # Computes the planned_end_date a ticket should have, or $null when it should be # left alone. The base is the latest conversation timestamp (any kind) when # present, else created_at. The target is base + N business days at the target # hour in the target zone/offset, clamped so it is never in the past. Returns # $null when there is no base date, or when the ticket already holds that exact # instant (so re-running is a no-op). function Get-FSvcPlannedEndDate { param( [Parameter(Mandatory)]$Ticket, [AllowNull()]$LatestConversationAt, [datetimeoffset]$Now, [int]$BusinessDays = 3, [int]$TargetHour = 17, [AllowNull()]$Offset ) $base = ConvertTo-FSDateTimeOffset $LatestConversationAt if ($null -eq $base) { $base = ConvertTo-FSDateTimeOffset $Ticket.created_at } if ($null -eq $base) { return $null } $target = Get-FSvcTargetEndDate -Base $base -Days $BusinessDays -Hour $TargetHour -Offset $Offset -Now $Now if (-not (Test-FSvcEndDateNeedsUpdate -PlannedEndDate $Ticket.planned_end_date -Target $target)) { return $null } return $target } |