packages feed

keiro-dsl-0.17.0.0: test/conformance-process-state-authority/Generated/IncidentResponse/IncidentEscalation/Process.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
-- @generated by keiro-dsl 0.17.0.0 (language keiro-dsl 6) from process reaction IncidentEscalation; do not edit.
module Generated.IncidentResponse.IncidentEscalation.Process
  ( IncidentEscalationInput (..)
  , incidentEscalationProcessName
  , incidentEscalationCategory
  , incidentEscalationProcessWorkerOptions
  , incidentEscalationReactionVersion
  , incidentEscalationReactionFingerprint
  , incidentEscalationReact
  , incidentEscalationProcessManager
  , incidentEscalationRunProcessWorker
  , EscalationPayload (..)
  , incidentEscalationEscalationTimerRequest
  , incidentEscalationTimerWorkerOptions
  , incidentEscalationFireTimer
  ) where

import Data.Text (Text)
import Data.Aeson (FromJSON, ToJSON)
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.Text qualified as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time (UTCTime, addUTCTime)
import Data.UUID (UUID)
import Data.UUID.V5 qualified as UUID.V5
import GHC.Generics (Generic)
import Numeric.Natural (Natural)
import Generated.IncidentResponse.IncidentEscalation.Input (IncidentEscalationInput (..))
import IncidentResponse.IncidentEscalation.ProcessHoles (decodeIncidentEscalationInput)
import Generated.IncidentResponse.Escalation.Domain qualified as Saga
import Generated.IncidentResponse.Escalation.EventStream (escalationEventStream, EscalationEventStreamDef)
import Generated.IncidentResponse.Incident.Domain qualified as Target
import Generated.IncidentResponse.Incident.EventStream (incidentCategory, incidentCommandCategory, incidentEventStream)
import Generated.IncidentResponse.Nominals qualified as N
import Keiro.Command (DomainCommandHandler (..), SilentDomainDecision (..))
import Keiro.Command (CommandError (..), RunCommandOptions (..), runCommand)
import Keiro.ProcessManager (PMCommand (..), confirmBenignDuplicate)
import Keiro.ProcessManager.Reaction qualified as Reaction
import Keiro.Stream qualified as Stream
import Keiro.DeterministicId (identitySeedBytes)
import Keiro.Timer (TimerId (..), TimerRequest (..), TimerWorkerOptions (..))
import Kiroku.Store.Types (EventId (..))
import Keiro.ProcessManager (PoisonPolicy (..), RejectedCommandPolicy (..), WorkerOptions (..))
import Shibuya.Core.Ack (RetryDelay (..))

data EscalationPayload = EscalationPayload
  { kind :: !Text
  , incidentId :: !N.IncidentId
  , severity :: !N.Severity
  }
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)

incidentEscalationEscalationTimerRequest :: Text -> UTCTime -> N.IncidentId -> N.Severity -> TimerRequest
incidentEscalationEscalationTimerRequest correlationId fireAtTime payloadIncidentId payloadSeverity =
  TimerRequest
    { timerId = TimerId (reactionIdentity "incident-escalation-timer:" correlationId)
    , processManagerName = incidentEscalationProcessName
    , correlationId = correlationId
    , fireAt = fireAtTime
    , payload = Aeson.toJSON (EscalationPayload { kind = "escalation", incidentId = payloadIncidentId, severity = payloadSeverity })
    }

incidentEscalationProcessName :: Text
incidentEscalationProcessName = "incident-escalation"

incidentEscalationCategory :: Stream.StreamCategory EscalationEventStreamDef
incidentEscalationCategory = Stream.categoryUnsafe "escalation"

incidentEscalationProcessWorkerOptions :: WorkerOptions es msg
incidentEscalationProcessWorkerOptions =
  WorkerOptions
    { poisonPolicy = PoisonHalt,
      rejectedCommandPolicy = RejectedHalt,
      transientRetryDelay = RetryDelay 5, -- matches defaultWorkerOptions; runtime tuning
      metrics = Nothing -- runtime configuration; install at call site
    }

incidentEscalationTimerWorkerOptions :: TimerWorkerOptions
incidentEscalationTimerWorkerOptions =
  TimerWorkerOptions
    { maxAttempts = Just 5
    , requeueStuckAfter = Just 300
    }
-- Operator dead-letter guidance: "escalation timer exceeded ceiling"

incidentEscalationReactionVersion :: Natural
incidentEscalationReactionVersion = 1

incidentEscalationReactionFingerprint :: Text
incidentEscalationReactionFingerprint = "74ccfa3e3e78a8d747546629dce80be851ec3dd2babbd7ddea34b0746f19c7c7"

incidentEscalationCorrelate :: IncidentEscalationInput -> Text
incidentEscalationCorrelate input = case input of
  IncidentReported { incidentId } -> (N.incidentIdText incidentId)
  ResponderAcked { incidentId } -> (N.incidentIdText incidentId)
  ResponderIgnored { incidentId } -> (N.incidentIdText incidentId)
  IncidentNoted { incidentId } -> (N.incidentIdText incidentId)

incidentEscalationReact :: IncidentEscalationInput -> Reaction.ReactionPlan Saga.EscalationCommand Target.IncidentCommand
incidentEscalationReact input = case input of
  IncidentReported { incidentId, severity, raisedAt }
    | (severity == N.Sev1) -> Reaction.AdvanceReaction { command = Saga.NoteRaised (Saga.NoteRaisedData { Saga.incidentId = incidentId }), followUps = [Reaction.FollowSchedule Reaction.Rearm (incidentEscalationEscalationTimerRequest (N.incidentIdText incidentId) (addUTCTime 300 raisedAt) incidentId severity)], onAccepted = [] }
    | otherwise -> Reaction.AdvanceReaction { command = Saga.NoteRaised (Saga.NoteRaisedData { Saga.incidentId = incidentId }), followUps = [Reaction.FollowSchedule Reaction.Rearm (incidentEscalationEscalationTimerRequest (N.incidentIdText incidentId) (addUTCTime 3600 raisedAt) incidentId severity)], onAccepted = [] }
  ResponderAcked { incidentId, ackedAt = _ackedAt }
    -> Reaction.AdvanceReaction { command = Saga.NoteAcknowledged (Saga.NoteAcknowledgedData { Saga.incidentId = incidentId }), followUps = [Reaction.FollowCancel (TimerId (reactionIdentity "incident-escalation-timer:" (N.incidentIdText incidentId)))], onAccepted = [Reaction.FollowDispatch (PMCommand (Stream.entityStream incidentCommandCategory (N.incidentIdText incidentId)) (Target.AcknowledgeIncident (Target.AcknowledgeIncidentData { Target.incidentId = incidentId })))] }
  ResponderIgnored { incidentId }
    -> Reaction.AdvanceReaction { command = Saga.NoteIgnored (Saga.NoteIgnoredData { Saga.incidentId = incidentId }), followUps = [Reaction.FollowCancel (TimerId (reactionIdentity "incident-escalation-timer:" (N.incidentIdText incidentId)))], onAccepted = [Reaction.FollowDispatch (PMCommand (Stream.entityStream incidentCommandCategory (N.incidentIdText incidentId)) (Target.AcknowledgeIncident (Target.AcknowledgeIncidentData { Target.incidentId = incidentId })))] }
  IncidentNoted {}
    -> Reaction.NoAdvance []

incidentEscalationProcessManager =
  Reaction.ReactiveProcessManager
    { name = incidentEscalationProcessName
    , correlate = incidentEscalationCorrelate
    , sagaHandler = DomainCommandHandler { eventStream = escalationEventStream, classifySilent = \_ -> SilentNoOp () }
    , streamFor = Stream.entityStream incidentEscalationCategory
    , targetEventStream = incidentEventStream
    , targetProjections = const []
    , react = incidentEscalationReact
    }

incidentEscalationRunProcessWorker options adapter =
  Reaction.runReactiveProcessManagerWorkerWith
    incidentEscalationProcessWorkerOptions
    options
    incidentEscalationProcessManager
    adapter
    (\event -> case decodeIncidentEscalationInput event of Nothing -> Nothing; Just input -> Just (event, input))

incidentEscalationFireTimer options timer
  | timer.timerId == TimerId (reactionIdentity "incident-escalation-timer:" timer.correlationId) = incidentEscalationEscalationFire options timer
  | otherwise = pure Nothing

incidentEscalationEscalationFire options timer
  | timer.processManagerName /= incidentEscalationProcessName = pure Nothing
  | otherwise =
      case (Aeson.fromJSON timer.payload :: Aeson.Result EscalationPayload) of
        Aeson.Error _ -> pure Nothing
        Aeson.Success decoded -> do
          let firedId = EventId (reactionIdentity "incident-escalation-fired:" timer.correlationId)
              target = Stream.entityStream incidentCategory timer.correlationId
          result <-
            runCommand
              (options { eventIds = [firedId] })
              incidentEventStream
              target
              (Target.EscalateIncident (Target.EscalateIncidentData { Target.incidentId = decoded.incidentId }))
          case result of
            Right {} -> pure (Just firedId)
            Left err -> do
              benign <- confirmBenignDuplicate (Stream.streamName target) firedId err
              pure $ if benign then Just firedId else case err of
                CommandRejected -> Just firedId
                CommandAmbiguous _ -> Nothing
                _ -> Nothing

reactionIdentity :: Text -> Text -> UUID
reactionIdentity prefix correlation =
  UUID.V5.generateNamed UUID.V5.namespaceURL (identitySeedBytes (T.concat (map field [prefix, correlation])))
  where
    field value = T.pack (show (BS.length (encodeUtf8 value))) <> ":" <> value