# Idempotency Key — deduplication of payment charges
# Demonstrates: idempotencyKey (§4.4), composite key construction (R-IDEMP-2),
# and DuplicateInvocationError propagation for duplicate requests.

flowmarkup:
  title: Payment Charge (Idempotent)
  version: 1
  description: >
    Charges a customer for an order. Uses a composite idempotencyKey
    to prevent duplicate charges when the same request is retried.
    On duplicate invocation, the engine raises DuplicateInvocationError
    before do: executes — callers must handle this error.
  requires:
    CONTEXT: { read: [tenant_id] }
    SERVICES: [payments, ledger]

  # Composite key: tenant + order + amount ensures same charge is never duplicated
  # even across retries, while allowing different amounts for the same order
  idempotencyKey: "='charge:' + CONTEXT.tenant_id + ':' + order_id + ':' + amount"

  input:
    order_id: STRING
    amount: NUMBER
    currency:
      $kind: STRING
      $default: USD

  output:
    charge_id: STRING
    status: STRING

  # DuplicateInvocationError is raised by the engine BEFORE do: executes (§4.4).
  # It cannot be caught inside the flow body. Callers receive it directly and
  # can inspect ERROR.DATA.status ("IN_PROGRESS" or "COMPLETED") to decide
  # whether to wait or return the cached result.
  throws:
  - PaymentDeclinedError
  - DuplicateInvocationError

  # The entire do: list is one transaction — if ledger recording fails after
  # the charge succeeds, the charge is rolled back automatically.
  transaction: true
  onRollbackError: CONTINUE

  examples:
    successful_charge:
      _notes_: Normal payment processed successfully
      input:
        order_id: ORD-7890
        amount: 149.99
        currency: USD
      output:
        charge_id: ch_abc123
        status: charged

  vars:
    charge_id: ""

  do:
  - call:
      _label_: Charge payment
      service: payments
      operation: charge
      timeout: 30s
      params:
        order_id: =order_id
        amount: =amount
        currency: =currency
      result:
        charge_id: =RESULT.charge_id
      retry:
        maxAttempts: 3
        delay: 2s
        backoff: EXPONENTIAL
        onErrors: [TimeoutError, ConnectionError]
      errors: [PaymentDeclinedError, TimeoutError, ConnectionError]
      rollback:
      - call:
          service: payments
          operation: refund
          params: {charge_id: =charge_id}
          retry: 3/2s/EXPONENTIAL

  - call:
      _label_: Record in ledger
      service: ledger
      operation: record
      timeout: 30s
      params:
        charge_id: =charge_id
        order_id: =order_id
        amount: =amount

  - return: {charge_id: =charge_id, status: charged}
