openapi: 3.0.3
info:
  title: Fortza Bulk API
  description: >-
    Batch fraud-detection API. Submit customer records, track processing progress,
    and retrieve per-record fraud scores with layer-level breakdowns.
    Authenticate with the **X-Fortza-Key** header on every request.

  version: 1.0.0
  contact:
    name: Fortza Support
    email: support@tdfortza.com

servers:
  - url: https://api.tdfortza.com
    description: Production

security:
  - X-Fortza-Key: []

tags:
  - name: Jobs
    description: |
      Submit batch jobs, track progress, and retrieve fraud-detection results.
  - name: Health
    description: |
      Service health probe. Used by monitoring and load balancers.

paths:
  # ═══════════════════════════════════════════════════════════════════════════
  # HEALTH
  # ═══════════════════════════════════════════════════════════════════════════
  /health:
    get:
      operationId: healthCheck
      tags: [Health]
      summary: Health check
      description: Returns service health status. Used by ALB target group health checks.
      security:
        - X-Fortza-Key: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                status: healthy
                service: ingestion_controller

  # ═══════════════════════════════════════════════════════════════════════════
  # SUBMIT JOB
  # ═══════════════════════════════════════════════════════════════════════════
  /jobs:
    post:
      operationId: createJob
      tags: [Jobs]
      summary: Submit a new batch job
      description: >-
        Creates a batch job and queues each record for asynchronous fraud-detection
        processing. Each record is validated for required fields (customer_name, email)
        and correct types. The build_token resolves the scoring layer configuration,
        caller identity, and organization. Subscription status, expiry, and monthly
        transaction quota are verified before acceptance. Accepts 1 to 1,000 records
        per request. Records exceeding the monthly quota are rejected with a 403
        response and full usage breakdown.
      security:
        - X-Fortza-Key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateJobRequest"
            examples:
              minimal:
                summary: Minimal — just required fields
                value:
                  build_token: "<your-build-token>"
                  records:
                    - customer_name: "John Smith"
                      email: "john@example.com"
              full:
                summary: Full — all fields, metadata, tenant
                value:
                  build_token: "<your-build-token>"
                  tenant_id: "test-tenant-001"
                  job_metadata:
                    source: "crm_nightly_export"
                    batch_ref: "BATCH-2026-02-09-001"
                  records:
                    - origin_system_record_id: "CRM-10001"
                      customer_name: "Jane Smith"
                      email: "jane@example.com"
                      zip_code: "90210"
                      order_date: "2026-01-15"
                      salesperson_name: "Bob Sales"
                      cust_street: "123 Main St"
                      cust_state: "CA"
                      cust_city: "Beverly Hills"
                      dealer_name: "West Coast Wireless"
                      wireless_package: "Premium Plus"
                      Tntype: "NEW"
                      Autopay: "YES"
                      devicename: "iPhone 15 Pro"
                      devicetype: "Smartphone"
                      store_number: 42
                      gender: "F"
                      ipAddress: "192.168.1.100"
                    - origin_system_record_id: "CRM-10002"
                      customer_name: "Bob Johnson"
                      email: "bob@example.com"
                      zip_code: "10001"
      responses:
        "201":
          description: Job created and records queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateJobResponse"
              example:
                job_id: "7eddfb8d-78ea-4b03-902d-8bc53ff5109f"
                accepted_count: 20
                queued_count: 20
                duplicate_count: 0
                build_name: "Full 14-Layer Build"
                layers_count: 13
        "400":
          description: Record validation failed. Response includes per-record error details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationErrorResponse"
              example:
                detail:
                  error: "Record validation failed"
                  error_code: "VALIDATION_ERROR"
                  record_errors:
                    - record_index: 2
                      origin_system_record_id: "ABC-123"
                      errors:
                        - "Missing required field: email"
                  valid_count: 9
                  invalid_count: 1
        "403":
          description: >-
            Entitlement or subscription error. Possible error codes:
            SUBSCRIPTION_NOT_FOUND, SUBSCRIPTION_INACTIVE,
            SUBSCRIPTION_EXPIRED, TRANSACTION_LIMIT_EXCEEDED.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntitlementErrorResponse"
              examples:
                transaction_limit:
                  summary: Transaction limit exceeded
                  value:
                    detail:
                      limit: 3500
                      completed: 3500
                      in_flight: 0
                      total_reserved: 3500
                      requested: 2
                      remaining: 0
                      can_proceed: false
                      scope: "organization"
                      error: "Transaction limit would be exceeded"
                      error_code: "TRANSACTION_LIMIT_EXCEEDED"
                      would_exceed_by: 2
                      message: "Cannot process 2 records. Limit: 3500, Reserved: 3500 (3500 completed, 0 in-flight), Remaining: 0"
                subscription_expired:
                  summary: Subscription expired
                  value:
                    detail:
                      error: "Subscription expired on 2026-01-01. Please renew your subscription to continue."
                      error_code: "SUBSCRIPTION_EXPIRED"
                      build_name: "Full 14-Layer Build"
                      expired_on: "2026-01-01"
                subscription_inactive:
                  summary: Subscription inactive
                  value:
                    detail:
                      error: "Subscription is inactive. Please renew your subscription to continue."
                      error_code: "SUBSCRIPTION_INACTIVE"
                      build_name: "Full 14-Layer Build"
        "404":
          description: Build token not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                detail:
                  error: "Build token not found: <your-build-token>"
                  error_code: "BUILD_NOT_FOUND"
        "422":
          description: Request body failed schema validation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HTTPValidationError"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # ═══════════════════════════════════════════════════════════════════════════
  # JOB STATUS
  # ═══════════════════════════════════════════════════════════════════════════
  /jobs/{job_id}:
    get:
      operationId: getJobStatus
      tags: [Jobs]
      summary: Get job status and progress
      description: >-
        Returns the current status of a job, including the overall job status
        (PENDING, PROCESSING, COMPLETED, or FAILED), per-status item counts,
        and completion progress as a percentage. Poll this endpoint after
        submitting a job. When the status is COMPLETED or FAILED, proceed
        to fetch results.
      security:
        - X-Fortza-Key: []
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: Job status retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobStatusResponse"
              example:
                job_id: "7eddfb8d-78ea-4b03-902d-8bc53ff5109f"
                tenant_id: "test-tenant"
                status: "PROCESSING"
                metadata:
                  source: "client"
                created_at: "2026-01-29T10:00:00Z"
                updated_at: "2026-01-29T10:05:00Z"
                summary:
                  total: 20
                  queued: 5
                  running: 3
                  succeeded: 10
                  failed: 2
                  retryable_failed: 0
                progress:
                  completed: 12
                  total: 20
                  percent: 60.0
        "400":
          description: Invalid job_id format (not a UUID)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                detail: "Invalid job_id format"
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                detail: "Job not found"

  # ═══════════════════════════════════════════════════════════════════════════
  # JOB RESULTS
  # ═══════════════════════════════════════════════════════════════════════════
  /jobs/{job_id}/results:
    get:
      operationId: getJobResults
      tags: [Jobs]
      summary: Get processed results
      description: >-
        Returns paginated fraud-detection results for a completed or failed job.
        Results are only available after the job reaches COMPLETED or FAILED status;
        calling this on an in-progress job returns 409 Conflict. On first fetch, a
        timestamp is recorded and results are auto-deleted 24 hours later. Each
        result item includes the overall fraud analysis summary, numeric risk score,
        per-layer breakdown, layer execution summary, confidence metrics, granular
        status, and error details if applicable. Use limit and offset to page
        through large result sets. Default page size is 1000.
      security:
        - X-Fortza-Key: []
      parameters:
        - $ref: "#/components/parameters/JobId"
        - name: limit
          in: query
          required: false
          description: Page size (1–1000)
          schema:
            type: integer
            default: 1000
            minimum: 1
            maximum: 1000
        - name: offset
          in: query
          required: false
          description: Number of items to skip
          schema:
            type: integer
            default: 0
            minimum: 0
        - name: status_filter
          in: query
          required: false
          description: Filter by item status. Omit to get both SUCCEEDED and FAILED.
          schema:
            type: string
            enum: [SUCCEEDED, FAILED]
        - name: fetched_by
          in: query
          required: false
          description: Identifier for who or what is fetching results (e.g. crm_sync_worker). Recorded for audit.
          schema:
            type: string
      responses:
        "200":
          description: Results retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobResultsResponse"
              example:
                job_id: "7eddfb8d-78ea-4b03-902d-8bc53ff5109f"
                tenant_id: "test-tenant"
                status: "COMPLETED"
                results:
                  - item_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    origin_system_record_id: "AWS-TEST-00001"
                    status: "SUCCEEDED"
                    result: "The overall fraud risk score is 0.42, indicating a low level of concern..."
                    status_detail: "complete_success"
                    results:
                      FBI:
                        layer: "FBI"
                        score: 0.1
                        reason: "No match found"
                        Summary: "Clear"
                        blocked: false
                      Anomaly:
                        layer: "Anomaly"
                        score: 0.07
                        reason: "No anomalies detected"
                        Summary: "Normal"
                        blocked: false
                    summary:
                      errors: 0
                      failed: 0
                      blocked: 0
                      successful: 13
                      total_layers: 13
                    confidence:
                      message: "High confidence: 13/13 layers executed successfully"
                      coverage: 1.0
                      confidence_level: "high"
                      confidence_percentage: 100.0
                    total_score: 0.42
                    error: null
                    completed_at: "2026-01-29T10:06:12Z"
                fetch_metadata:
                  fetched_at: "2026-01-29T10:07:00Z"
                  fetched_by: "client_system"
                  items_returned: 1
                  first_fetch: true
                  auto_delete_after: "2026-01-30T10:07:00Z"
                pagination:
                  limit: 1000
                  offset: 0
                  count: 1
        "400":
          description: Invalid job_id or status_filter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Job not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Job still in progress
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                detail: "Job is still PROCESSING. Results can only be fetched after job is COMPLETED or FAILED."
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"


# ═════════════════════════════════════════════════════════════════════════════
# COMPONENTS
# ═════════════════════════════════════════════════════════════════════════════
components:
  securitySchemes:
    X-Fortza-Key:
      type: apiKey
      in: header
      name: X-Fortza-Key
      description: |
        API key provided by Fortza. Required on every request.
        Contact support@tdfortza.com if you need a key.

  parameters:
    JobId:
      name: job_id
      in: path
      required: true
      description: Job UUID returned by POST /jobs
      schema:
        type: string
        format: uuid
      example: "7eddfb8d-78ea-4b03-902d-8bc53ff5109f"

  schemas:
    # ═════════════════════════════════════════════════════════════════════════
    # REQUEST
    # ═════════════════════════════════════════════════════════════════════════
    CreateJobRequest:
      type: object
      required: [records, build_token]
      properties:
        records:
          type: array
          description: 1–1,000 customer records to analyze
          minItems: 1
          maxItems: 1000
          items:
            $ref: "#/components/schemas/RecordInput"
        build_token:
          type: string
          description: |
            Build token that selects the scoring layer configuration.
            Provided by Fortza during onboarding.
          example: "<your-build-token>"
        tenant_id:
          type: string
          description: Optional tenant identifier for multi-tenant tracking
          example: "test-tenant-001"
        job_metadata:
          type: object
          description: |
            Arbitrary metadata stored with the job. Returned in status queries.
            Useful for batch references, source system tags, etc.
          additionalProperties: true
          example:
            source: "crm_nightly_export"
            batch_ref: "BATCH-2026-02-09-001"

    RecordInput:
      type: object
      required: [customer_name, email]
      description: |
        A single customer record for fraud analysis.

        Required fields are customer_name and email.

        Recommended: include origin_system_record_id for matching results back to your records.

        All other fields are optional but improve analysis accuracy.
        Fields not listed here are accepted and passed through to the pipeline.
      properties:
        origin_system_record_id:
          type: string
          description: Your record ID — returned as-is in results for matching
          example: "CRM-10001"
        customer_name:
          type: string
          description: Full name of the customer
          example: "Jane Smith"
        email:
          type: string
          format: email
          description: Customer email address
          example: "jane@example.com"
        zip_code:
          type: string
          description: Customer ZIP/postal code
          example: "90210"
        order_date:
          type: string
          description: Date of the order/transaction
          example: "2026-01-15"
        salesperson_name:
          type: string
          description: Name of the salesperson
          example: "Bob Sales"
        cust_street:
          type: string
          description: Customer street address
          example: "123 Main St"
        cust_state:
          type: string
          description: Customer state/province
          example: "CA"
        cust_city:
          type: string
          description: Customer city
          example: "Beverly Hills"
        dealer_name:
          type: string
          description: Dealer or store name
          example: "West Coast Wireless"
        wireless_package:
          type: string
          description: Wireless service package name
          example: "Premium Plus"
        Tntype:
          type: string
          description: Transaction type (e.g., NEW, UPGRADE, PORT)
          example: "NEW"
        Autopay:
          type: string
          description: Auto-pay enrollment status
          example: "YES"
        devicename:
          type: string
          description: Device name
          example: "iPhone 15 Pro"
        devicetype:
          type: string
          description: Device category
          example: "Smartphone"
        store_number:
          description: Store/location number (accepts integer or string)
          example: 42
        gender:
          type: string
          description: Customer gender
          example: "F"
        ipAddress:
          type: string
          description: IP address of the transaction
          example: "192.168.1.100"
      additionalProperties: true

    # ═════════════════════════════════════════════════════════════════════════
    # RESPONSES
    # ═════════════════════════════════════════════════════════════════════════
    CreateJobResponse:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
          description: Unique job identifier — use for all subsequent queries
        accepted_count:
          type: integer
          description: Total records received
        queued_count:
          type: integer
          description: Records successfully queued for processing
        duplicate_count:
          type: integer
          description: Records skipped (idempotency dedup)
        build_name:
          type: string
          description: Name of the resolved build configuration
        layers_count:
          type: integer
          description: Number of scoring layers in the build

    JobStatusResponse:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        tenant_id:
          type: string
        status:
          $ref: "#/components/schemas/JobStatus"
        metadata:
          type: object
          description: Job metadata provided at submission
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        summary:
          $ref: "#/components/schemas/JobSummary"
        progress:
          $ref: "#/components/schemas/JobProgress"

    JobSummary:
      type: object
      description: Item counts by processing status
      properties:
        total:
          type: integer
          description: Total items in the job
        queued:
          type: integer
          description: Waiting for a worker
        running:
          type: integer
          description: Currently being processed
        succeeded:
          type: integer
          description: Completed successfully
        failed:
          type: integer
          description: Permanently failed
        retryable_failed:
          type: integer
          description: Failed but eligible for retry

    JobProgress:
      type: object
      description: Completion progress
      properties:
        completed:
          type: integer
          description: Items finished (succeeded + failed)
        total:
          type: integer
          description: Total items
        percent:
          type: number
          format: float
          description: Completion percentage (0.00–100.00)

    JobResultsResponse:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        tenant_id:
          type: string
        status:
          $ref: "#/components/schemas/JobStatus"
        results:
          type: array
          items:
            $ref: "#/components/schemas/ResultItem"
        fetch_metadata:
          $ref: "#/components/schemas/FetchMetadata"
        pagination:
          $ref: "#/components/schemas/PaginationInfo"

    ResultItem:
      type: object
      description: Fraud analysis result for a single record
      properties:
        item_id:
          type: string
          format: uuid
          description: Internal item identifier
        origin_system_record_id:
          type: string
          description: Your record ID (from submission) for matching
        status:
          $ref: "#/components/schemas/JobItemStatus"
        result:
          type: string
          description: Overall fraud analysis summary text
        status_detail:
          type: string
          description: Granular status (e.g. complete_success)
        results:
          type: object
          description: |
            Per-layer results keyed by layer name. Each layer contains
            layer, score, reason, Summary, blocked, and layer-specific fields.
          additionalProperties: true
        summary:
          type: object
          description: |
            Layer execution summary with counts for errors, failed, blocked, successful, and total_layers.
          properties:
            errors:
              type: integer
            failed:
              type: integer
            blocked:
              type: integer
            successful:
              type: integer
            total_layers:
              type: integer
        confidence:
          type: object
          description: |
            Confidence metrics for the analysis
          properties:
            message:
              type: string
            coverage:
              type: number
            confidence_level:
              type: string
              enum: [high, medium, low]
            confidence_percentage:
              type: number
        total_score:
          type: number
          format: float
          description: Overall weighted fraud risk score
        error:
          type: object
          nullable: true
          description: Error details (null if succeeded)
          additionalProperties: true
        completed_at:
          type: string
          format: date-time
          description: When processing completed

    FetchMetadata:
      type: object
      description: Metadata about this fetch operation
      properties:
        fetched_at:
          type: string
          format: date-time
          description: Timestamp of this fetch
        fetched_by:
          type: string
          description: Identifier of the fetcher (from query param)
        items_returned:
          type: integer
          description: Items in this response page
        first_fetch:
          type: boolean
          description: |
            True if this is the first time these results are fetched.
            Starts the 24-hour auto-deletion countdown.
        auto_delete_after:
          type: string
          format: date-time
          description: Results will be auto-deleted after this timestamp

    PaginationInfo:
      type: object
      properties:
        limit:
          type: integer
          description: Requested page size
        offset:
          type: integer
          description: Requested offset
        count:
          type: integer
          description: Items returned in this page

    HealthResponse:
      type: object
      properties:
        status:
          type: string
          enum: [healthy]
        service:
          type: string
          enum: [ingestion_controller]

    # ═════════════════════════════════════════════════════════════════════════
    # ENUMS
    # ═════════════════════════════════════════════════════════════════════════
    JobStatus:
      type: string
      enum: [PENDING, PROCESSING, COMPLETED, FAILED]
      description: |
        PENDING (job created, items being queued), PROCESSING (at least one item running),
        COMPLETED (all items finished), FAILED (job-level failure).

    JobItemStatus:
      type: string
      enum: [QUEUED, RUNNING, SUCCEEDED, FAILED, RETRYABLE_FAILED]
      description: |
        QUEUED (waiting for a worker), RUNNING (currently being processed),
        SUCCEEDED (completed with results), FAILED (permanently failed),
        RETRYABLE_FAILED (failed, eligible for retry).

    # ═════════════════════════════════════════════════════════════════════════
    # ERRORS
    # ═════════════════════════════════════════════════════════════════════════
    ErrorResponse:
      type: object
      properties:
        detail:
          description: Error detail (string or object depending on endpoint)

    ValidationErrorResponse:
      type: object
      description: Returned when one or more records fail field validation
      properties:
        error:
          type: string
          example: "Record validation failed"
        error_code:
          type: string
          example: "VALIDATION_ERROR"
        record_errors:
          type: array
          items:
            $ref: "#/components/schemas/RecordValidationError"
        valid_count:
          type: integer
          description: Records that passed validation
        invalid_count:
          type: integer
          description: Records that failed validation

    RecordValidationError:
      type: object
      properties:
        record_index:
          type: integer
          description: Zero-based index in the records array
        origin_system_record_id:
          type: string
          description: Your record ID (if present)
        errors:
          type: array
          items:
            type: string
          description: List of validation errors for this record

    EntitlementErrorResponse:
      type: object
      description: |
        Returned when subscription or quota checks fail.
        The detail object contains the specific error code and usage breakdown.
      properties:
        detail:
          type: object
          properties:
            error:
              type: string
            error_code:
              type: string
              enum:
                - SUBSCRIPTION_NOT_FOUND
                - SUBSCRIPTION_INACTIVE
                - SUBSCRIPTION_EXPIRED
                - TRANSACTION_LIMIT_EXCEEDED
            build_name:
              type: string
            limit:
              type: integer
              description: Monthly transaction limit
            completed:
              type: integer
              description: Completed this month
            in_flight:
              type: integer
              description: Currently in-flight (QUEUED/RUNNING)
            total_reserved:
              type: integer
              description: completed + in_flight
            requested:
              type: integer
              description: Records in this request
            remaining:
              type: integer
              description: Remaining quota
            can_proceed:
              type: boolean
            would_exceed_by:
              type: integer
            scope:
              type: string
              enum: [user, organization]
            message:
              type: string
              description: Human-readable summary
            expired_on:
              type: string
              description: Expiry date (for SUBSCRIPTION_EXPIRED)

    HTTPValidationError:
      type: object
      description: Request body schema validation error (Pydantic)
      properties:
        detail:
          type: array
          items:
            type: object
            properties:
              loc:
                type: array
                items:
                  type: string
                description: Error location (e.g., ["body", "records", "0"])
              msg:
                type: string
                description: Error message
              type:
                type: string
                description: Error type
