{
  "rulesetId": "rs-evaluate-all-checks",
  "name": "Independent eligibility checks (evaluate-all)",
  "description": "Three parallel checks: passport validity, funds coverage and purpose; every check fires its own outcome and contributes to a score.",
  "contractVersion": "app-contract@v2",
  "strategy": "evaluate-all",
  "nodes": [
    {
      "type": "start",
      "id": "start",
      "inputs": ["passport.expiryDate", "trip.*", "finance.bankBalance"],
      "next": ["c-passport", "c-funds", "c-purpose"]
    },
    {
      "type": "condition",
      "id": "c-passport",
      "expr": { "op": ">=", "left": { "fn": "days_until", "args": [{ "field": "passport.expiryDate" }] }, "right": { "lit": 180 } },
      "onTrue": "o-passport-ok",
      "onFalse": "o-passport-short"
    },
    {
      "type": "outcome",
      "id": "o-passport-ok",
      "outcome": "PASS",
      "reasonCode": "PASSPORT_OK",
      "reasonText": "Passport valid for {days_until(passport.expiryDate)} more days.",
      "severity": "info"
    },
    {
      "type": "outcome",
      "id": "o-passport-short",
      "outcome": "FAIL",
      "hard": true,
      "reasonCode": "PASSPORT_VALIDITY",
      "reasonText": "Passport expires in {days_until(passport.expiryDate)} days; minimum 180 required.",
      "severity": "critical"
    },
    {
      "type": "condition",
      "id": "c-funds",
      "expr": { "op": "is_null", "arg": { "field": "finance.bankBalance" } },
      "onTrue": "s-funds-unknown",
      "onFalse": "c-funds-ratio"
    },
    {
      "type": "score",
      "id": "s-funds-unknown",
      "accumulator": "risk",
      "delta": { "lit": 30 },
      "next": "o-funds-unknown"
    },
    {
      "type": "outcome",
      "id": "o-funds-unknown",
      "outcome": "FLAG",
      "reasonCode": "FUNDS_UNKNOWN",
      "reasonText": "No bank balance supplied; risk score now {accumulators.risk}.",
      "severity": "warning"
    },
    {
      "type": "condition",
      "id": "c-funds-ratio",
      "expr": { "op": ">=", "left": { "field": "finance.bankBalance" }, "right": { "op": "mul", "left": { "field": "trip.cost" }, "right": { "lit": 2 } } },
      "onTrue": "o-funds-ok",
      "onFalse": "s-funds-low"
    },
    {
      "type": "outcome",
      "id": "o-funds-ok",
      "outcome": "PASS",
      "reasonCode": "FUNDS_OK",
      "reasonText": "Balance {finance.bankBalance} covers twice the trip cost {trip.cost}.",
      "severity": "info"
    },
    {
      "type": "score",
      "id": "s-funds-low",
      "accumulator": "risk",
      "delta": { "op": "sub", "left": { "op": "mul", "left": { "field": "trip.cost" }, "right": { "lit": 2 } }, "right": { "field": "finance.bankBalance" } },
      "next": "o-funds-low"
    },
    {
      "type": "outcome",
      "id": "o-funds-low",
      "outcome": "FLAG",
      "reasonCode": "FUNDS_LOW",
      "reasonText": "Balance {finance.bankBalance} is below twice the trip cost; shortfall added to risk ({accumulators.risk}).",
      "severity": "warning"
    },
    {
      "type": "switch",
      "id": "c-purpose",
      "field": "trip.purpose",
      "cases": { "tourism": "o-purpose-ok", "family_visit": "o-purpose-ok", "business": ["rh-business", "s-business"] },
      "default": "o-purpose-unknown"
    },
    {
      "type": "routeHint",
      "id": "rh-business",
      "hint": "business-desk",
      "next": "o-purpose-ok"
    },
    {
      "type": "score",
      "id": "s-business",
      "accumulator": "risk",
      "delta": { "lit": -5 },
      "next": "o-purpose-business"
    },
    {
      "type": "outcome",
      "id": "o-purpose-business",
      "outcome": "PASS",
      "reasonCode": "PURPOSE_BUSINESS",
      "reasonText": "Business travel; risk adjusted to {accumulators.risk}.",
      "severity": "info"
    },
    {
      "type": "outcome",
      "id": "o-purpose-ok",
      "outcome": "PASS",
      "reasonCode": "PURPOSE_OK",
      "reasonText": "Purpose '{trip.purpose}' is accepted.",
      "severity": "info"
    },
    {
      "type": "outcome",
      "id": "o-purpose-unknown",
      "outcome": "FLAG",
      "reasonCode": "UNKNOWN_PURPOSE",
      "reasonText": "Purpose '{trip.purpose}' is not recognised.",
      "severity": "warning"
    }
  ]
}
