swagger: "2.0"

info:
  title: OpenDQV Cloud Validate API
  description: |
    Write-time data quality validation for OpenDQV Cloud. Customer-facing surface
    for registering as a Salesforce External Service, Postman collection import,
    or any OpenAPI 2.0 consumer.

    **Salesforce External Service setup:**
    1. Setup → External Services → New External Service
    2. Choose "From API Specification" and paste this YAML (or host it at a URL and reference it)
    3. Link to a Named Credential that points at your cell URL
       (e.g. `https://cells.opendqv.com/t/{tenant_id}`)
    4. The Named Credential's External Credential carries the Authorization
       Custom Header: `Bearer <your PAT>`
    5. Salesforce auto-generates invocable actions `Validate` and `ValidateBatch`
       — drop them into a Screen Flow before a Create Record element to
       implement write-time data-quality enforcement declaratively (no Apex).

    **About the "record" field:** it is an arbitrary JSON object whose keys are
    the field names the contract validates. From Flow Builder, populate it via
    an Apex-Defined variable or a JSON-string assembled from your record's
    fields.

  version: "1.0.0"
  contact:
    name: OpenDQV Cloud Support
    email: support@opendqv.com
    url: https://opendqv.com

# Customers override host + basePath via their Named Credential's URL.
# Defaults shown here are placeholders for spec validators.
host: cells.opendqv.com
basePath: /
schemes:
  - https

consumes:
  - application/json
produces:
  - application/json

securityDefinitions:
  BearerAuth:
    type: apiKey
    in: header
    name: Authorization
    description: |
      Personal Access Token. Mint via OpenDQV Cloud dashboard → API Tokens.
      Salesforce External Credentials inject this header automatically when
      you configure a Custom Header with name `Authorization` and value
      `Bearer <PAT>`.

security:
  - BearerAuth: []

paths:

  /api/v1/validate:
    post:
      operationId: Validate
      summary: Validate a single record against a contract
      description: |
        Synchronous write-time validation. Returns valid=true/false plus the
        list of rule failures. In observation_only mode, `valid` is always
        true and `would_have_failed` carries the enforcement counterfactual.

        In a Salesforce Screen Flow: invoke this action before a Create Record
        element, then add a Decision element branching on the response's
        `valid` field (or `would_have_failed` for observation mode). If
        invalid, route to an error Screen and skip Create Record — this is
        the declarative bouncer-at-the-door pattern.
      parameters:
        - in: body
          name: body
          required: true
          schema:
            $ref: "#/definitions/ValidateRequest"
      responses:
        "200":
          description: Validation completed (record was evaluated; check `valid` for outcome)
          schema:
            $ref: "#/definitions/ValidateResponse"
        "400":
          description: Malformed request body or unknown contract version hash
          schema:
            $ref: "#/definitions/ErrorResponse"
        "401":
          description: Missing or invalid Authorization header
          schema:
            $ref: "#/definitions/ErrorResponse"
        "402":
          description: Tenant ODU budget exhausted
          schema:
            $ref: "#/definitions/ErrorResponse"
        "404":
          description: Contract name not found on this tenant
          schema:
            $ref: "#/definitions/ErrorResponse"
        "422":
          description: Request shape valid but semantics invalid (missing contract, missing record)
          schema:
            $ref: "#/definitions/ErrorResponse"
        "503":
          description: Cell warming up or audit queue saturated — retry after short backoff
          schema:
            $ref: "#/definitions/ErrorResponse"

  /api/v1/validate/batch:
    post:
      operationId: ValidateBatch
      summary: Validate a batch of records against a contract
      description: |
        Multi-record validation in one round-trip. Returns a per-record
        results array preserving input order. Uses one HTTP callout against
        Salesforce's 100-callouts-per-transaction governor regardless of
        batch size (up to platform limits). Use for Salesforce Data Loader
        post-import sweeps, scheduled batch validation, or any bulk write
        path.
      parameters:
        - in: body
          name: body
          required: true
          schema:
            $ref: "#/definitions/BatchValidateRequest"
      responses:
        "200":
          description: Batch evaluated (every record validated; per-record results in array)
          schema:
            $ref: "#/definitions/BatchValidateResponse"
        "400":
          description: Malformed request body or unknown contract version hash
          schema:
            $ref: "#/definitions/ErrorResponse"
        "401":
          description: Missing or invalid Authorization header
          schema:
            $ref: "#/definitions/ErrorResponse"
        "402":
          description: Tenant ODU budget exhausted
          schema:
            $ref: "#/definitions/ErrorResponse"
        "404":
          description: Contract name not found on this tenant
          schema:
            $ref: "#/definitions/ErrorResponse"
        "422":
          description: Request shape valid but semantics invalid (empty records, missing contract)
          schema:
            $ref: "#/definitions/ErrorResponse"
        "503":
          description: Cell warming up or audit queue saturated — retry after short backoff
          schema:
            $ref: "#/definitions/ErrorResponse"

definitions:

  ValidateRequest:
    type: object
    required:
      - contract
      - record
    properties:
      contract:
        type: string
        description: Contract name (case-sensitive). Use `list_contracts` to discover names available on your tenant.
        example: salesforce_contact
      record:
        type: object
        description: |
          Record fields as a JSON object. Keys are field names referenced by
          the contract's rules. The properties listed below are the union of
          standard Salesforce Contact / Lead / Account fields and are
          provided so that Salesforce External Services can generate a
          usable Apex-Defined Type with settable fields in Flow Builder.
          The engine accepts ANY additional fields beyond these — set
          additionalProperties: true.
        additionalProperties: true
        properties:
          FirstName:
            type: string
            description: Contact / Lead first name.
          LastName:
            type: string
            description: Contact / Lead last name.
          Email:
            type: string
            description: Email address.
          Phone:
            type: string
            description: Phone number.
          MobilePhone:
            type: string
            description: Mobile phone number.
          MailingStreet:
            type: string
            description: Street address for mailing.
          MailingCity:
            type: string
            description: City for mailing address.
          MailingState:
            type: string
            description: State or province for mailing address.
          MailingPostalCode:
            type: string
            description: Postal code for mailing address.
          MailingCountry:
            type: string
            description: Country for mailing address.
          Birthdate:
            type: string
            format: date
            description: Birthdate in YYYY-MM-DD format.
          Title:
            type: string
            description: Job title.
          Department:
            type: string
            description: Department.
          AccountName:
            type: string
            description: Related account name (for Contacts).
          Name:
            type: string
            description: Account name or full record name.
          Company:
            type: string
            description: Lead company name.
        example:
          FirstName: Sarah
          LastName: Acme
          Email: sarah@acme.com
      agent_id:
        type: string
        description: |
          Caller-asserted source-system identifier (e.g. "salesforce-prod",
          "kafka-orders"). Stamped on the audit row for per-source analytics
          and SLA reporting. Free-form text, spoofable — use for labelling,
          not for security.
        example: salesforce-prod
      record_id:
        type: string
        description: Optional caller-supplied business key (e.g. Salesforce record Id, Kafka offset). Echoed in response and stamped on the audit row.
        example: 003gL00000mNXu5QAG
      dry_run:
        type: boolean
        description: If true, validate without recording an audit row. Default false (record).
        default: false
      observe_only:
        type: boolean
        description: |
          Optional per-request override toward observation mode. Conservative
          direction only — if the contract is in enforcement mode, set true
          to evaluate this single call as observation_only. Forcing enforce
          on an observation contract is intentionally not supported.

  BatchValidateRequest:
    type: object
    required:
      - contract
      - records
    properties:
      contract:
        type: string
        description: Contract name (case-sensitive)
        example: salesforce_contact
      records:
        type: array
        description: Array of records to validate (preserves input order in response)
        items:
          $ref: "#/definitions/BatchRecordInput"
      agent_id:
        type: string
        description: Caller-asserted source-system identifier (applied to every record in the batch)
        example: salesforce-prod
      dry_run:
        type: boolean
        description: If true, evaluate without recording audit rows
        default: false
      observe_only:
        type: boolean
        description: Per-request conservative override toward observation mode (same semantics as the single-record endpoint)

  BatchRecordInput:
    type: object
    required:
      - record
    properties:
      record:
        type: object
        description: One record's field key-value pairs
        additionalProperties: true
      record_id:
        type: string
        description: Optional caller-supplied business key for this record

  ValidateResponse:
    type: object
    properties:
      valid:
        type: boolean
        description: |
          Validation outcome. In enforcement mode, true = passes all rules,
          false = at least one error rule failed. In observation_only mode,
          always true — check `would_have_failed` for the counterfactual.
      record_id:
        type: string
        description: Echoed from the request
      errors:
        type: array
        description: Per-field rule failures (severity=error)
        items:
          $ref: "#/definitions/FieldError"
      warnings:
        type: array
        description: Per-field rule failures (severity=warning) — never block
        items:
          $ref: "#/definitions/FieldError"
      mode:
        type: string
        description: "Which governance path produced this result: 'enforcement' or 'observation_only'"
        enum:
          - enforcement
          - observation_only
      would_have_failed:
        type: boolean
        description: |
          Enforcement counterfactual — always emitted regardless of mode.
          In observation_only mode where `valid` is forced true, this field
          carries the truth about whether enforcement would have blocked the
          record.
      would_have_failed_rules:
        type: array
        description: Rule names that triggered the would-have-failed signal
        items:
          type: string
      contract:
        type: string
        description: Contract name (echoed)
      version:
        type: string
        description: Contract version
      owner:
        type: string
        description: Contract owner (free-form)
      engine_version:
        type: string
        description: Engine binary that processed this validation (build-stamped at link time)
      contract_hash:
        type: string
        description: yaml_hash of the contract version used (pin for point-in-time replay)
      contract_status:
        type: string
        description: "Lifecycle status of the contract used: draft | review | active | archived"
        enum:
          - draft
          - review
          - active
          - archived
      persisted:
        type: boolean
        description: True if an audit row was written; false if dry_run suppressed the write
      caller_principal:
        type: string
        description: Server-derived authenticated principal UUID (the PAT id that authenticated this request). Unspoofable.
      validated_at:
        type: string
        format: date-time
        description: Server timestamp (UTC, RFC3339)
      latency_ms:
        type: number
        format: float
        description: Engine evaluation latency in milliseconds
      agent_id:
        type: string
        description: Echoed from request (caller-asserted)
      event_id:
        type: string
        description: Per-row UUIDv7 — pin to retrieve this audit row via `get_audit_event`. Empty on dry_run.
      entry_hash:
        type: string
        description: sha256-hex of canonical-JSON(record) plus record_id
      content_hash:
        type: string
        description: sha256-hex of canonical-JSON(record)
      effective_rule_hash:
        type: string
        description: sha256-hex of canonical-JSON(sorted rule names) — identifies the rule set evaluated
      batch_id:
        type: string
        description: Gateway-witnessed canonical batch correlator (populated when reached through the MCP gateway; absent on direct REST)

  BatchValidateResponse:
    type: object
    properties:
      results:
        type: array
        description: Per-record results, preserving the input order
        items:
          $ref: "#/definitions/BatchRecordResult"
      batch_warnings:
        type: array
        description: Request-level warnings that apply to the whole batch (always present, often empty)
        items:
          $ref: "#/definitions/FieldError"
      total:
        type: integer
        description: Total records in the batch
      valid_count:
        type: integer
        description: Records that passed all error-severity rules
      invalid_count:
        type: integer
        description: Records that failed at least one error-severity rule
      contract:
        type: string
        description: Contract name (echoed)
      version:
        type: string
      owner:
        type: string
      engine_version:
        type: string
      contract_hash:
        type: string
      contract_status:
        type: string
      persisted:
        type: boolean
        description: True if per-record audit rows were written; false if dry_run suppressed
      caller_principal:
        type: string
      validated_at:
        type: string
        format: date-time
      latency_ms:
        type: number
        format: float
      agent_id:
        type: string

  BatchRecordResult:
    type: object
    properties:
      record_index:
        type: integer
        description: Zero-based input position
      record_id:
        type: string
      valid:
        type: boolean
      errors:
        type: array
        items:
          $ref: "#/definitions/FieldError"
      warnings:
        type: array
        items:
          $ref: "#/definitions/FieldError"
      latency_ms:
        type: number
        format: float
      mode:
        type: string
        enum:
          - enforcement
          - observation_only
      would_have_failed:
        type: boolean
      would_have_failed_rules:
        type: array
        items:
          type: string
      event_id:
        type: string
      entry_hash:
        type: string
      content_hash:
        type: string
      effective_rule_hash:
        type: string
      batch_id:
        type: string

  FieldError:
    type: object
    properties:
      field:
        type: string
        description: Field name that failed
      rule:
        type: string
        description: Rule name that triggered
      message:
        type: string
        description: Human-readable failure message (drawn from the contract YAML)
      severity:
        type: string
        enum:
          - error
          - warning
      error_code:
        type: string
        description: Machine-readable code (e.g. OPENDQV_REGEX_EMAIL_FORMAT) for programmatic branching
      suggested_fix:
        type: string
        description: Hint for the producer to remediate the failure

  ErrorResponse:
    type: object
    properties:
      error:
        type: string
        description: Stable machine-readable error code (e.g. "contract_not_found")
      detail:
        type: string
        description: Human-readable error description
      suggested_fix:
        type: string
        description: Hint for the caller to remediate
