flowmarkup:
  title: Rollback and Transaction Groups Demo
  version: 1
  description: Demonstrates rollback handlers and transaction groups for saga patterns, variable safety, and error handling.
  requires:
    SERVICES: [payment, inventory, shipping, hotel, airline, cloud, email, crm]

  # Shared retry policy — referenced via = in rollback handlers
  const:
    ROLLBACK_RETRY: {maxAttempts: 3, delay: 2s, backoff: EXPONENTIAL}

  vars:
    order_status: new

  input:
    order:
      $kind: MAP
      _notes_: Order details
    trip:
      $kind: MAP
      _notes_: Travel booking details
    vm_spec:
      $kind: MAP
      _notes_: VM provisioning specification
    user:
      $kind: MAP
      _notes_: User information
    welcome_body:
      $kind: STRING
      _notes_: Welcome email body content

  # Error hierarchy — $parent enables polymorphic catch ordering
  throws:
  - ProvisioningError:
      - $kind: ManualInterventionError
        data:
          failed_count: NUMBER

  do:
    # ──────────────────────────────────────────────────────────────
    # 1. Sequential saga — transaction group with rollback handlers
    #    On failure: rollback handlers fire in reverse, then variables roll back
    # ──────────────────────────────────────────────────────────────
  - group:
      _label_: Order Processing Saga
      transaction: true
      onRollbackError: CONTINUE
      do:
      - set:
          order_status: processing

      - call:
          _label_: Charge payment
          service: payment
          operation: charge
          params: {order_id: =order.id, amount: =order.total}
          result: {payment_tx: =RESULT.tx_id}
          rollback:
          - call:
              service: payment
              operation: refund
              params: {tx_id: =payment_tx}
              retry: =ROLLBACK_RETRY

      - call:
          _label_: Reserve inventory
          service: inventory
          operation: reserve
          params: {order_id: =order.id, items: =order.items}
          result: {inv_reservation: =RESULT.reservation_id}
          rollback:
          - call:
              service: inventory
              operation: cancel
              params: {reservation_id: =inv_reservation}
              retry: 3/1s/EXPONENTIAL
      - call:
          _label_: Schedule shipping
          service: shipping
          operation: create_label
          params: {order_id: =order.id, address: =order.address}
  - parallel:
      _label_: Travel Booking
      transaction: true
      failPolicy: COMPLETE
      book_hotel:
      - call:
          service: =SERVICES.hotel
          operation: reserve
          params: {destination: =trip.destination, dates: =trip.dates}
          result: {hotel_confirmation: =RESULT.confirmation}
          rollback:
          - call:
              service: =SERVICES.hotel
              operation: cancel
              params: {confirmation: =hotel_confirmation}
              retry: =ROLLBACK_RETRY
      book_flight:
      - call:
          service: =SERVICES.airline
          operation: reserve
          params: {origin: =trip.origin, destination: =trip.destination, date: =trip.date}
          result: {flight_ticket: =RESULT.ticket}
          rollback:
          - call:
              service: =SERVICES.airline
              operation: cancel
              params: {ticket: =flight_ticket}
              retry: =ROLLBACK_RETRY

    # ──────────────────────────────────────────────────────────────
    # 3. Pure transaction — variable safety without rollback handlers
    #    All variable writes auto-rollback on assertion failure
    # ──────────────────────────────────────────────────────────────
  - group:
      _label_: Calculate Order Total
      transaction: true
      do:
      - set:
          running_total: 0
          discount_applied: false
      - forEach:
          items: =order.line_items
          maxItems: 1000
          do:
          - set:
              running_total: =running_total + item.price * item.quantity
      - if:
          condition: =order.coupon != null
          then:
          - set:
              running_total: =running_total * 0.9
              discount_applied: true
      - assert: =running_total >= 0
      - set:
          order_total: =running_total

    # ──────────────────────────────────────────────────────────────
    # 4. Error handling — distinguishing RolledBackError vs RollbackFailedError
    # ──────────────────────────────────────────────────────────────
  - try:
      do:
      - group:
          _label_: Provisioning
          transaction: true
          onRollbackError: CONTINUE
          do:
          - call:
              service: cloud
              operation: create_vm
              params: {spec: =vm_spec}
              result: {vm_id: =RESULT.instance_id}
              rollback:
              - call:
                  service: cloud
                  operation: terminate_vm
                  params: {instance_id: =vm_id}
                  retry: 5/5s/EXPONENTIAL
          - call:
              service: cloud
              operation: attach_storage
              params: {instance_id: =vm_id, size_gb: 100}
              result: {vol_id: =RESULT.volume_id}
              rollback:
              - call:
                  service: cloud
                  operation: detach_storage
                  params: {volume_id: =vol_id}
                  retry: =ROLLBACK_RETRY
          - call:
              service: cloud
              operation: configure_network
              params: {instance_id: =vm_id}
      catch:
        RolledBackError:
        - log: "Provisioning rolled back cleanly: {{ERROR.CAUSE.MESSAGE}}"
        RollbackFailedError:
        - log: "Rollback failed — manual cleanup required for: {{ERROR.DATA.failed.size()}} handlers"
        - throw:
            error: ManualInterventionError
            message: Provisioning rollback incomplete
            data:
              failed_count: =ERROR.DATA.failed.size()

    # ──────────────────────────────────────────────────────────────
    # 5. rollback without transaction — external compensation only
    #    Variables are NOT rolled back (no scope forking)
    # ──────────────────────────────────────────────────────────────
  - try:
      do:
      - call:
          service: =SERVICES.email
          operation: send
          params: {to: =user.email, subject: Welcome, body: =welcome_body}
          result: {email_id: =RESULT.message_id}
          rollback:
          - call:
              service: =SERVICES.email
              operation: recall
              params: {message_id: =email_id}
              retry: =ROLLBACK_RETRY
      - call:
          service: =SERVICES.crm
          operation: create_contact
          params: {email: =user.email}
      catch:
        default:
        - log: Error after email sent — rollback will recall email if registered


  - set:
      order_status: completed

  - log: Order processing complete
