{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://unitytrip.com/specs/leased-travel-policy.schema.json",
  "title": "UnityTrip Leased Travel Policy",
  "description": "The machine-readable form of a leased travel policy: the document produced by the UnityTrip Leased Travel Policy Builder (leasedpolicybuilder.unitytrip.com) and consumed by a deterministic booking engine. It expresses an operational ontology — passenger types, trip reasons, transport modes, priorities, quotas, booking windows, penalties, and fairness principles — for travel where the seat has no fare and policy, not price, rations demand. The document is self-identifying (documentType and schemaVersion) so an import pipeline can recognise it among mixed uploads, and it declares both the live runtime inputs a static policy cannot contain and the ordered evaluation contract the engine runs.",
  "type": "object",
  "required": ["documentType", "schemaVersion", "meta", "transportModes", "passengerTypes", "tripReasons", "bookingPolicies", "penalties", "fairness", "requiredRuntimeInputs", "evaluationContract"],
  "properties": {
    "documentType": {
      "const": "unitytrip.leased-travel-policy",
      "description": "Self-identifying envelope. Lets an import pipeline deterministically recognise this file among mixed document uploads and route it to the policy-engine importer for enforcement as well as the knowledge base for explanation."
    },
    "schemaVersion": {
      "type": "string",
      "description": "Version of this schema the document conforms to. Versioning is what makes past booking decisions reproducible: a decision can always be traced to the policy version that governed it.",
      "examples": ["1.0"]
    },
    "meta": {
      "type": "object",
      "description": "Provenance and review cycle for the policy document.",
      "properties": {
        "company": { "type": ["string", "null"], "description": "Organisation the policy belongs to." },
        "industry": { "type": ["string", "null"] },
        "operatingContext": {
          "type": "string",
          "enum": ["offshore-compound", "remote-site", "office-based", "mixed"],
          "description": "The operating environment the policy governs. Offshore compounds and remote sites are where leased and chartered transport dominates."
        },
        "primaryLocations": { "type": ["string", "null"] },
        "approxMonthlyTravellers": { "type": ["string", "null"] },
        "effectiveDate": { "type": "string", "format": "date" },
        "reviewDate": { "type": "string", "format": "date", "description": "Policies should be reviewed at least annually; quotas typically reset on 1 January." },
        "generatedAt": { "type": "string", "format": "date-time" }
      }
    },
    "fundingTypesInUse": {
      "type": "object",
      "description": "Which funding models the operation runs. Business travel is company-funded under budget approval; private travel is the traveller's own money on the same transport; subsidised travel sits between.",
      "properties": {
        "business": { "type": "boolean" },
        "private": { "type": "boolean" },
        "subsidised": { "type": "boolean" }
      }
    },
    "transportModes": {
      "type": "array",
      "description": "The transport modes the policy governs. Leased aircraft, chartered vessels, and ground fleets share one policy model.",
      "items": { "type": "string", "enum": ["air", "maritime", "vehicle"] }
    },
    "offices": {
      "type": "array",
      "description": "The offices or locations whose travellers the policy covers.",
      "items": { "type": "string" }
    },
    "seatAllocations": {
      "type": "array",
      "description": "Named seat-allocation pools on the operation's transport — capacity carve-outs that rules can reserve.",
      "items": { "type": "string" }
    },
    "passengerTypes": {
      "type": "array",
      "description": "The classes of traveller the policy recognises — staff, contractors, dependents, guests. Priority, quotas, and entitlements attach to passenger types, not to individuals.",
      "items": {
        "type": "object",
        "properties": { "id": { "type": "string" }, "label": { "type": "string" } },
        "required": ["id", "label"]
      }
    },
    "tripReasons": {
      "type": "array",
      "description": "Why a journey is taken. Reasons carry their own funding model and quotas, so business, leave, medical, and compassionate travel are governed differently. Reason-aware policy is what lets penalties exempt legitimate disruptions.",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "label": { "type": "string" },
          "funding": { "type": ["string", "null"], "description": "The funding model this reason implies." }
        },
        "required": ["id", "label"]
      }
    },
    "bookingPolicies": {
      "type": "array",
      "description": "The priority matrix: ordered rules mapping passenger types, trip reasons, transport modes, and offices to a priority, with each rule carrying its own quotas and booking windows. When several rules match a request, the lowest priority number governs. Because the seat has no fare, this matrix — not price — is what rations demand.",
      "items": {
        "type": "object",
        "required": ["id", "priority", "paxTypes", "tripReasons", "transport"],
        "properties": {
          "id": { "type": "string" },
          "description": { "type": "string" },
          "priority": { "type": "integer", "description": "Rank among competing requests for the same seat; lower numbers outrank higher. Displacement at overbooking offloads the highest priority numbers first." },
          "source": { "type": ["string", "null"], "enum": ["cytric", "duffel", null], "description": "The booking-content provider this rule may be fulfilled by, when restricted to one." },
          "sourceLabel": { "type": ["string", "null"] },
          "seatAllocation": { "type": ["string", "null"], "description": "The named seat pool this rule draws from, if restricted." },
          "multiSectorAllowed": { "type": "boolean", "description": "Whether the rule may book across multiple sectors of a sailing or rotation. Multi-sector bookings outrank single-sector ones at displacement, because a single-leg booking on a multi-leg route can strand an empty seat on the remaining legs." },
          "paxTypes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" } } } },
          "tripReasons": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" } } } },
          "transport": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" } } } },
          "offices": { "type": "array", "items": { "type": "string" } },
          "quota": {
            "type": "object",
            "description": "The seat caps this rule enforces. Quotas are per rule — not global and not by salary group — and infants are excluded from counting.",
            "properties": {
              "scope": { "type": "string", "enum": ["each", "all"], "description": "Whether each passenger type in the rule gets its own quota, or all share one pool." },
              "scopeLabel": { "type": "string" },
              "infantsExcluded": { "type": "boolean" },
              "limits": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "max": { "type": ["integer", "null"], "description": "Maximum seats within the timeframe; null means uncapped." },
                    "timeframe": { "type": "string", "enum": ["perYear", "perWeek", "perSector", "perPeakSail", "perDay"], "description": "The window the cap applies over — including per peak sailing, because peak-period capacity is the scarcest resource in a rotational operation." },
                    "timeframeLabel": { "type": "string" }
                  }
                }
              }
            }
          },
          "bookingWindows": {
            "type": "array",
            "description": "How early a matching request may book, staged by sail period, so early bookers cannot crowd out higher-priority travel in peak periods.",
            "items": {
              "type": "object",
              "properties": {
                "daysBeforeDeparture": { "type": ["integer", "null"] },
                "sailPeriod": { "type": "string", "enum": ["peak", "offpeak"] },
                "sailPeriodLabel": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "salaryGroups": {
      "type": ["object", "null"],
      "description": "Salary-group access entitlements — cabin, transport mode, or guesthouse category granted by grade or band. Entitlements govern access only, never quotas: a senior grade may unlock a better cabin, but seats are still rationed by the priority matrix.",
      "properties": {
        "source": { "type": ["string", "null"], "description": "Where the grade system is mastered, such as the HR system." },
        "governs": { "type": "string" },
        "tiers": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "levels": { "type": "string" },
              "transportModes": { "type": "array", "items": { "type": "string" } },
              "guesthouseCategory": { "type": ["string", "null"] },
              "notes": { "type": ["string", "null"] }
            }
          }
        }
      }
    },
    "quotaApportionment": {
      "type": "object",
      "properties": {
        "note": { "type": "string" },
        "peakPeriodsDefined": { "type": "boolean" },
        "peakPeriodDefinition": { "type": ["string", "null"], "description": "What counts as a peak period for this operation — school holidays, crew-change weeks, festival seasons." }
      }
    },
    "sectorRestrictions": {
      "type": ["boolean", "string"],
      "description": "Controls on movement over specific legs or routes, so essential personnel can travel without being blocked. False when none; free text or true when defined."
    },
    "penalties": {
      "type": "object",
      "description": "The behavioural levers for no-shows and late cancellations. Points scale with proximity to departure — a last-minute no-show costs more than an early cancellation — and accumulated points trigger a temporary ban that still allows a colleague to book on the offender's behalf, so essential travel continues.",
      "properties": {
        "tiers": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "description": { "type": "string" },
              "minHours": { "type": ["number", "null"], "description": "Lower bound of hours before departure for this tier." },
              "maxHours": { "type": ["number", "null"], "description": "Upper bound of hours before departure for this tier." },
              "points": { "type": ["number", "null"], "description": "Penalty points this tier assigns." }
            }
          }
        },
        "banThresholdPoints": { "type": ["number", "null"], "description": "Accumulated points at which a booking ban triggers." },
        "banDurationDays": { "type": ["number", "null"] },
        "exemptTripReasons": { "type": "array", "items": { "type": "string" }, "description": "Trip reasons that bypass penalties — medical travel, for example, is typically exempt." },
        "sameDayRescheduleExempt": { "type": "boolean", "description": "Whether rescheduling to another sailing the same day avoids penalty — the traveller still travelled; the seat was not wasted." }
      }
    },
    "fairness": {
      "type": "object",
      "description": "Cross-cutting fairness principles that override the matrix where they apply.",
      "properties": {
        "medicalAlwaysHighest": { "type": "boolean", "description": "Medical travel outranks everything." },
        "businessOutranksPrivate": { "type": "boolean", "description": "Company-funded travel outranks private travel on the same transport." },
        "preventKeyPersonsTogether": { "type": "boolean", "description": "Key-person separation: designated critical personnel may not travel on the same sailing, so a single incident cannot remove them all." },
        "keyPersonNotes": { "type": ["string", "null"] },
        "allowDelegatedBookings": { "type": "boolean", "description": "Whether a colleague may book on another traveller's behalf — the mechanism that keeps essential travel moving during a booking ban." },
        "delegationNotes": { "type": ["string", "null"] }
      }
    },
    "subsidisedTravel": {
      "type": ["object", "null"],
      "properties": { "details": { "type": ["string", "null"] } }
    },
    "additionalNotes": { "type": ["string", "null"] },
    "requiredRuntimeInputs": {
      "type": "array",
      "description": "The live state the booking engine must supply at decision time — state a static policy document can never hold: the traveller's profile and group memberships, quota already consumed, current penalty points and ban status, the booking context (dates, route, sectors, reason, mode, source), and the manifest state of the target sailing (capacity, seats taken, reserved-seat consumption, passengers aboard).",
      "items": {
        "type": "object",
        "properties": {
          "key": { "type": "string", "enum": ["travellerProfile", "quotaUsage", "sanctionStatus", "bookingContext", "manifestState"] },
          "description": { "type": "string" },
          "usedFor": { "type": "string" }
        }
      }
    },
    "evaluationContract": {
      "type": "array",
      "description": "The ordered decision procedure the engine runs: MATCH the governing rule (lowest priority number wins); check SOURCE restriction; check the booking WINDOW for the sail period; check SANCTION status (unless the reason is penalty-exempt); check QUOTA remaining; check CAPACITY against the manifest with reserved-seat carve-outs; check KEY-PERSON separation; and at overbooking only, apply DISPLACEMENT — highest priority numbers offloaded first, single-sector before multi-sector among equals, final tie-break by booking timestamp. Deterministic: the same request against the same state always returns the same decision.",
      "items": { "type": "string" }
    }
  }
}
