# ═══════════════════════════════════════════════════════════════
# FlowMarkup Showcase — Order Fulfillment Pipeline
# ═══════════════════════════════════════════════════════════════
# A realistic e-commerce flow that showcases how FlowMarkup turns
# painful distributed-systems problems into readable YAML:
#
#   - Typed contracts catch bugs before runtime
#   - User-defined functions eliminate copy-paste
#   - waitFor pauses for human approval (no polling!)
#   - Saga transactions auto-rollback on failure
#   - Parallel branches race shipping carriers
#   - Circuit breakers prevent cascading failures
# ═══════════════════════════════════════════════════════════════

flowmarkup:
  title: Order Fulfillment Pipeline
  version: 3
  documentation: |
    Processes e-commerce orders end-to-end: validates the order, applies
    discounts, routes by shipping priority, pauses for manager approval
    on large orders, runs a fraud check, charges payment and reserves
    inventory (with automatic rollback on failure), selects the cheapest
    carrier, and sends a confirmation email.

  requires:
    # Security: MAIL is restricted to specific recipient domains. Using
    # MAIL: true (unrestricted) would allow the flow to send email to any
    # address, risking spam or data exfiltration. Always use domain allowlists.
    MAIL: ["@example.com", "@ops.example.com"]
    SERVICES: [payment, inventory, shipping, fraud]
    SECRET: [fraud_api_key]
    SUBFLOWS: true

  # ── Types — catch bad data at load time, not 3 AM ──────────
  types:
    Order:
      type: object
      properties:
        id: {type: string, pattern: "^ORD-\\d{6}$"}
        customer: {$ref: "#/$defs/Customer"}
        items:
          type: array
          items: {$ref: "#/$defs/LineItem"}
          minItems: 1
        coupon: {type: string}
      required: [id, customer, items]

    Customer:
      type: object
      properties:
        name: {type: string, minLength: 1}
        email: {type: string, format: email}
        tier: {type: string, enum: [standard, premium, enterprise]}
      required: [name, email]

    LineItem:
      type: object
      properties:
        sku: {type: string, pattern: "^[A-Z]{2}-\\d{4}$"}
        name: {type: string}
        quantity: {type: integer, minimum: 1}
        price: {type: number, minimum: 0}
      required: [sku, quantity, price]

    FulfillmentResult:
      type: object
      properties:
        order_id: {type: string}
        status: {type: string, enum: [fulfilled, partial, failed]}
        tracking_id: {type: string}
        total: {type: number}
        discount: {type: number}
      required: [order_id, status, total]

  # ── Contract — callers know exactly what to send and expect ─
  input:
    order: {$type: application/json, $schema: Order}
    priority: {$enum: [standard, express, overnight], $default: standard}
    dry_run: {$default: false}
    coupon_override: {$kind: STRING, $nullable: true, _notes_: Optional coupon override}

  output:
    result: {$type: application/json, $schema: FulfillmentResult}

  yields:
    $kind: TEXT
    _notes_: Real-time progress updates for UI display.

  throws:
  - PaymentDeclinedError
  - OutOfStockError
  - ApprovalDeniedError
  - FraudDetectedError

  events:
    order_approval:
      _notes_: Manager approval or denial for large orders
      data:
        order_id: STRING
        decision: STRING
        approver: STRING
    order_fulfilled:
      _notes_: Emitted when order fulfillment completes successfully
      data:
        order_id: STRING
        tracking_id: STRING
        carrier: STRING
        total: NUMBER

  examples:
    express_order:
      _notes_: Happy path — express order for a premium customer with coupon
      input:
        order:
          id: ORD-001337
          customer:
            name: Alice Johnson
            email: alice@example.com
            tier: premium
          items:
          - {sku: EL-2001, name: Wireless Keyboard, quantity: 1, price: 79.99}
          - {sku: EL-2002, name: USB-C Hub, quantity: 2, price: 45.00}
          coupon: SAVE10
        priority: express
      output:
        result:
          order_id: ORD-001337
          status: fulfilled
          total: 152.99
          discount: 17.00
    dry_run:
      _notes_: Validates pricing without side effects
      input:
        order:
          id: ORD-000001
          customer: {name: Test User, email: test@example.com}
          items:
          - {sku: BK-1001, name: Notebook, quantity: 1, price: 12.99}
        dry_run: true
      output:
        result:
          order_id: ORD-000001
          status: fulfilled
          total: 12.99
          discount: 0

  # ── Reusable functions — no more copy-pasted CEL ────────────
  functions:
    subtotal:
      params:
        items: {$kind: ARRAY, _notes_: Line items with .price and .quantity}
      body: =items.map(i, i.price * i.quantity).sum()
    applyDiscount:
      params:
        code: {$kind: STRING, _notes_: Coupon code}
        total: {$kind: NUMBER, _notes_: Pre-discount total}
      body: "=code == 'SAVE10' ? total * 0.10 : (code == 'SAVE20' ? total * 0.20 : 0)"

  # ── Constants — operator-tunable, immutable at runtime ──────
  const:
    approval_threshold: 500
    max_retries: 3
    payment_timeout: 30s

  # ── Variables ───────────────────────────────────────────────
  vars:
    order_total: 0
    discount: 0
    payment_tx_id:
    tracking_id:

  # ══════════════════════════════════════════════════════════════
  # Flow body
  # ══════════════════════════════════════════════════════════════
  do:

    # ── 1. Validate & price ──────────────────────────────────
    # Types enforce structure; assertions enforce business rules.
  - assert:
      condition: =order.items.size() > 0
      message: Order must have at least one item

  - set:
      order_total: =subtotal(order.items)
      discount: "=has(order.coupon) ? applyDiscount(order.coupon, subtotal(order.items)) : 0"

  - set:
      order_total: =order_total - discount

  - log: "Order {{order.id}}: ${{order_total}} ({{order.items.size()}} items)"

    # ── 2. Dry-run gate ──────────────────────────────────────
    # Lets callers validate pricing without triggering side effects.
  - return:
      condition: =dry_run
      result:
        order_id: =order.id
        status: fulfilled
        total: =order_total
        discount: =discount

    # ── 3. Priority routing ──────────────────────────────────
    # switch is cleaner than if/elseIf chains for discrete values.
  - switch:
      value: =priority
      match:
        overnight:
        - logWarn: "Overnight order {{order.id}} — expedited pipeline"
        express:
        - log: "Express processing for {{order.id}}"
      default:
      - log: "Standard processing for {{order.id}}"

    # ── 4. Human approval for large orders ───────────────────
    # THIS is the part that's painful in plain code. You'd need
    # a database, a webhook endpoint, a state machine, and a
    # scheduled job to check for timeouts. Here: ~15 lines.
  - if:
      condition: =order_total > approval_threshold
      then:
      - yield: "Awaiting approval for ${{order_total}}..."
      - try:
          do:
          - waitFor:
              _label_: Wait for manager decision
              event: order_approval
              scope: GLOBAL
              source: "services/approval-service.flowmarkup.yaml"
              condition: "=EVENT.DATA.order_id == order.id"
              timeout: 24h
              capture:
                approval_decision: =EVENT.DATA.decision
                approver_name: =EVENT.DATA.approver
          catch:
            TimeoutError:
            - throw:
                error: ApprovalDeniedError
                message: "Approval timed out for {{order.id}}"
      - throw:
          condition: "=approval_decision != 'approved'"
          error: ApprovalDeniedError
          message: "Denied by {{approver_name}}"
  - run:
      _label_: Fraud check
      flow: checks/fraud-score.flowmarkup.yaml
      cap:
        SECRET: [fraud_api_key]
        SERVICES: [fraud]
      params:
        customer_email: =order.customer.email
        order_total: =order_total
        item_count: =order.items.size()
      result:
        fraud_score: =RESULT.score
      timeout: 10s

  - throw:
      condition: =fraud_score > 0.8
      error: FraudDetectedError
      message: "Fraud score {{fraud_score}} exceeds threshold"
  - yield: "Charging ${{order_total}}..."

  - try:
      do:
      - group:
          _label_: Payment and Inventory
          transaction: true
          onRollbackError: CONTINUE
          do:
          - call:
              _label_: Charge payment
              service: payment
              operation: charge
              params:
                order_id: =order.id
                amount: =order_total
                currency: USD
              circuitBreaker:
                name: fulfillment.payment
                threshold: 5
                window: 1m
                resetTimeout: 30s
                scope: GLOBAL
              retry:
                maxAttempts: =max_retries
                delay: 2s
                backoff: EXPONENTIAL
                nonRetryable: [PaymentDeclinedError, CircuitOpenError]
              timeout: =payment_timeout
              result:
                payment_tx_id: =RESULT.tx_id
              rollback:
              - call:
                  service: payment
                  operation: refund
                  params: {tx_id: =payment_tx_id}
                  retry: 5/3s/EXPONENTIAL
          - forEach:
              _label_: Reserve inventory
              items: =order.items
              maxItems: 1000
              concurrent: true
              do:
              - call:
                  service: inventory
                  operation: reserve
                  params:
                    sku: =item.sku
                    quantity: =item.quantity
                    order_id: =order.id
                  retry: 2/1s
                  rollback:
                  - call:
                      service: inventory
                      operation: release
                      params: {sku: =item.sku, order_id: =order.id}
      catch:
        PaymentDeclinedError:
        - throw:
            error: PaymentDeclinedError
            message: =ERROR.MESSAGE
        RolledBackError:
        - log: "Saga rolled back cleanly: {{ERROR.CAUSE.MESSAGE}}"
        - throw:
            error: OutOfStockError
            message: =ERROR.CAUSE.MESSAGE

    # ── 7. Select cheapest carrier ───────────────────────────
    # Parallel branches race two carriers. failPolicy: COMPLETE
    # waits for both so we can compare quotes.
  - parallel:
      _label_: Carrier quotes
      failPolicy: COMPLETE
      fedex:
      - call:
          service: shipping
          operation: quote
          params: {carrier: FedEx, priority: =priority}
          timeout: 5s
          result:
            fedex_rate: =RESULT.rate
      ups:
      - call:
          service: shipping
          operation: quote
          params: {carrier: UPS, priority: =priority}
          timeout: 5s
          result:
            ups_rate: =RESULT.rate

  - set:
      best_carrier: "=fedex_rate <= ups_rate ? 'FedEx' : 'UPS'"

  - call:
      _label_: Book shipment
      service: shipping
      operation: book
      params:
        carrier: =best_carrier
        order_id: =order.id
        priority: =priority
      result:
        tracking_id:                  # null-value → =RESULT.tracking_id
  - mail:
      _label_: Confirmation email
      to: =order.customer.email
      subject: "Order {{order.id}} confirmed!"
      body:
        # Security: All user-controlled or variable data in HTML bodies MUST use
        # htmlEscape() to prevent HTML/script injection.
        html: "<h2>Thank you, {{ htmlEscape(order.customer.name) }}!</h2><p>Shipped via {{ htmlEscape(best_carrier) }}. Tracking: <code>{{ htmlEscape(tracking_id) }}</code></p>"
        text: "Order {{order.id}} shipped via {{best_carrier}}. Tracking: {{tracking_id}}"

  - emit:
      event: order_fulfilled
      data:
        order_id: =order.id
        tracking_id: =tracking_id
        carrier: =best_carrier
        total: =order_total

    # ── Done ─────────────────────────────────────────────────
  - return:
      result:
        order_id: =order.id
        status: fulfilled
        tracking_id: =tracking_id
        total: =order_total
        discount: =discount
