openapi: 3.0.0
info:
  title: Fatoora Organisation API
  version: "1.0"
  description: >
    REST API for organizations on a DigiGO, SEAL Max, or Fatoora Trust plan using their own
    API key (created from /app/api-keys) — no org_id to resolve, the key is already bound to
    your organization. Authenticate with OAuth2 client_credentials, then submit, validate, sign
    and track TEIF invoices, download generated artifacts (XML/PDF), and subscribe to webhooks.
    Adapted from fatooraLive/public/openapi/api-oauth.yaml, extended with the artifact-retrieval
    and webhook endpoints. See PARTNER_API_GUIDE.md for the separate integrator-on-behalf-of-clients
    product line.
  contact:
    name: Fatoora Support
    email: support@fatoora.tn
servers:
  - url: https://business.fatoora.tn
    description: Production

security:
  - orgOAuth: []

paths:
  /oauth/token:
    post:
      summary: Exchange client credentials for an access token
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type: { type: string, enum: [client_credentials] }
                client_id: { type: string }
                client_secret: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenResponse" }
        "401":
          description: invalid_client
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }

  /api/v2/invoices/submit:
    post:
      summary: Submit an invoice (JSON TEIF)
      security: [{ orgOAuth: [invoice:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TeifInvoiceJson" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InvoiceSummary" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v2/invoices/submit-xml:
    post:
      summary: Submit an invoice (raw TEIF XML)
      security: [{ orgOAuth: [invoice:write] }]
      requestBody:
        required: true
        content:
          application/xml:
            schema: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InvoiceSummary" }

  /api/v2/teif/validate:
    post:
      summary: Validate a TEIF XML against the XSD schema, without creating an invoice
      security: [{ orgOAuth: [invoice:read, invoice:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [xml]
              properties:
                xml: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TeifValidationResult" }

  /api/v2/invoices:
    get:
      summary: List invoices (paginated)
      security: [{ orgOAuth: [invoice:read] }]
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 0 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 20 }
        - name: direction
          in: query
          schema: { type: string, enum: [SENT, RECEIVED] }
        - name: status
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  content:
                    type: array
                    items: { $ref: "#/components/schemas/InvoiceSummary" }
                  page: { type: integer }
                  pageSize: { type: integer }
                  totalElements: { type: integer }
                  totalPages: { type: integer }

  /api/v2/invoices/{id}:
    get:
      summary: Full invoice detail (wrapped — includes teifJson/teifXml)
      security: [{ orgOAuth: [invoice:read] }]
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InvoiceDetailResponse" }
        "404": { description: Not found }

  /api/v2/invoices/{id}/pdf:
    get:
      summary: Download the invoice PDF
      description: Binary response, works regardless of the invoice's status (drafts included).
      security: [{ orgOAuth: [invoice:read] }]
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
        - name: pdfTemplate
          in: query
          required: false
          schema: { type: string }
        - name: lang
          in: query
          required: false
          schema: { type: string }
      responses:
        "200":
          description: application/pdf binary
          content:
            application/pdf:
              schema: { type: string, format: binary }

  /api/v2/invoices/{id}/generated-xml:
    get:
      summary: Get the generated TEIF XML
      description: >
        ⚠️ Past the SIGNED status, this regenerates a fresh UNSIGNED XML on the fly (drops the
        signature and the TTN QR code). For the actual final, signed/TTN-timestamped document,
        read the teifXml field from GET /api/v2/invoices/{id} instead — see §8 in the guide body.
      security: [{ orgOAuth: [invoice:read] }]
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "200":
          description: application/xml body
          content:
            application/xml:
              schema: { type: string }

  /api/v2/invoices/stats:
    get:
      summary: Dashboard-style aggregate stats
      security: [{ orgOAuth: [invoice:read] }]
      responses:
        "200":
          description: OK

  /api/v2/invoices/api-key-stats:
    get:
      summary: Usage stats broken down per API key
      security: [{ orgOAuth: [invoice:read] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  organizationId: { type: string }
                  totalInvoices: { type: integer }
                  stats:
                    type: array
                    items:
                      type: object
                      properties:
                        appId: { type: string }
                        invoicesCreated: { type: integer }
                        invoicesSigned: { type: integer }
                        invoicesSubmittedToTtn: { type: integer }

  /api/v2/organization:
    get:
      summary: Get your organization's identity, plan, credits and invoice stats
      description: >
        Returns your own organization's identity (including taxIdentifier — must match
        MessageSenderIdentifier in submitted TEIF XML, or submission fails with
        SENDER_MATRICULE_MISMATCH), plan/subscription summary, signature-pack credit
        balance, and the same stats as /invoices/stats and /invoices/api-key-stats,
        merged into one response.
      security: [{ orgOAuth: [invoice:read] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  orgId: { type: string, format: uuid }
                  orgName: { type: string }
                  taxIdentifier: { type: string, nullable: true }
                  status: { type: string, enum: [active, archived] }
                  isTestAccount: { type: boolean }
                  isSandboxAccount: { type: boolean }
                  partnerStatus: { type: string, nullable: true }
                  createdAt: { type: string, format: date-time }
                  plan:
                    type: object
                    properties:
                      slug: { type: string, nullable: true }
                      name: { type: string, nullable: true }
                      status: { type: string }
                      monthlyLimit: { type: integer, nullable: true }
                      isUnlimited: { type: boolean }
                      currentPeriodStart: { type: string, format: date-time, nullable: true }
                      currentPeriodEnd: { type: string, format: date-time, nullable: true }
                      trialEndsAt: { type: string, format: date-time, nullable: true }
                  signatureCredits:
                    type: object
                    properties:
                      remainingTotal: { type: integer }
                      nearestExpiryAt: { type: string, format: date-time, nullable: true }
                  stats:
                    type: object
                    nullable: true
                    description: null when fatooraCore was unreachable — identity/plan/credits are still returned.
                    properties:
                      totalCount: { type: integer }
                      sentCount: { type: integer }
                      errorCount: { type: integer }
                      totalRevenue: { type: number }
                      byMonth:
                        type: array
                        items:
                          type: object
                          properties:
                            month: { type: string }
                            created: { type: integer }
                            sent: { type: integer }
                            errors: { type: integer }
                  apiKeyStats:
                    type: object
                    nullable: true
                    properties:
                      organizationId: { type: string }
                      totalInvoices: { type: integer }
                      stats:
                        type: array
                        items:
                          type: object
                          properties:
                            appId: { type: string }
                            invoicesCreated: { type: integer }
                            invoicesSigned: { type: integer }
                            invoicesSubmittedToTtn: { type: integer }

  /api/v2/invoices/{id}/sign:
    post:
      summary: Sign an invoice (SEAL or DigiGO)
      security: [{ orgOAuth: [seal:sign] }, { orgOAuth: [digigo:sign] }]
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                provider: { type: string, enum: [seal, digigo] }
      responses:
        "200":
          description: Signed

  /api/v2/invoices/{id}/submit-ttn:
    post:
      summary: Submit an already-signed invoice to TTN
      description: >
        Unlike the Partner API, an organisation's own key is allowed to call this endpoint
        directly (the "must use sign-and-send" restriction only applies to partner tokens).
      security: [{ orgOAuth: [ttn:submit] }]
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "200":
          description: Submitted

  /api/v2/invoices/{id}/sign-and-send:
    post:
      summary: Sign + submit to TTN in one asynchronous operation (recommended)
      security: [{ orgOAuth: [invoice:write] }]
      parameters:
        - $ref: "#/components/parameters/InvoiceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                signatureType: { type: string, enum: [SEAL, DIGIGO] }
      responses:
        "200":
          description: Job created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobCreated" }

  /api/v2/jobs/{jobId}:
    get:
      summary: Poll an async job's status (e.g. sign-and-send)
      security: [{ orgOAuth: [invoice:read] }]
      parameters:
        - name: jobId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobStatus" }

  /api/v2/webhooks:
    post:
      summary: Create a webhook subscription
      security: [{ orgOAuth: [invoice:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string }
                  description: Defaults to the 7 standard event types when omitted (see §9 in the guide body).
                customData: { type: object }
                ttlSeconds: { type: integer, description: "Defaults to 30 days" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookSubscription" }
    get:
      summary: List webhook subscriptions created by this API key
      security: [{ orgOAuth: [invoice:read] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/WebhookSubscription" }

  /api/v2/webhooks/{webhookId}:
    delete:
      summary: Delete a webhook subscription
      security: [{ orgOAuth: [invoice:write] }]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK

  /api/v2/webhooks/{webhookId}/test:
    post:
      summary: Send a test event (webhook.test) to one subscription
      security: [{ orgOAuth: [invoice:write] }]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK

  /api/v2/quotations/draft:
    post:
      summary: Create a draft quotation
      security: [{ orgOAuth: [invoice:write] }]
      responses:
        "201":
          description: Created

  /api/v2/quotations/{id}/convert-to-invoice:
    post:
      summary: Convert an accepted quotation into a new invoice
      security: [{ orgOAuth: [invoice:write] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "201":
          description: Created

components:
  parameters:
    InvoiceId:
      name: id
      in: path
      required: true
      schema: { type: string }

  responses:
    Unauthorized:
      description: Missing/invalid token, or insufficient scope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ValidationError:
      description: TEIF payload fails schema/business validation
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  securitySchemes:
    orgOAuth:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes:
            invoice:write: Create, submit, sign-and-send, and delete invoices
            invoice:read: List and read invoices, jobs, stats, webhooks
            ttn:submit: Manually (re)submit an already-signed invoice to TTN, or sync its TTN status
            seal:sign: Trigger SEAL signing (seal-max plan only)
            digigo:sign: Trigger DigiGO signing (digigo-* plans only)

  schemas:
    TokenResponse:
      type: object
      properties:
        access_token: { type: string }
        token_type: { type: string, example: Bearer }
        expires_in: { type: integer, example: 3600 }
        scope: { type: string }

    OAuthError:
      type: object
      properties:
        error: { type: string }
        error_description: { type: string }

    ErrorResponse:
      type: object
      properties:
        error: { type: string }
        message: { type: string }
        code: { type: string }

    TeifInvoiceJson:
      type: object
      required: [version, controlingAgency, header, body]
      properties:
        version: { type: string, example: "1.8.8" }
        controlingAgency: { type: string, example: TTN }
        header: { type: object }
        body: { type: object }

    InvoiceSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        orgId: { type: string }
        invoiceNumber: { type: string }
        invoiceTypeCode: { type: string }
        invoiceDate: { type: string }
        status: { type: string }
        senderName: { type: string }
        receiverName: { type: string }
        totalAmountExclTax: { type: number }
        totalTaxAmount: { type: number, nullable: true }
        totalAmountInclTax: { type: number, nullable: true }
        currencyCode: { type: string }
        ttnGeneratedRef: { type: string, nullable: true }
        createdAt: { type: string }
        submittedAt: { type: string }
        signedAt: { type: string, nullable: true }
        createdBy: { type: string }
        direction: { type: string, enum: [SENT, RECEIVED] }

    InvoiceDetailResponse:
      type: object
      description: The wrapped shape returned by GET /api/v2/invoices/{id}.
      properties:
        invoice: { $ref: "#/components/schemas/InvoiceSummary" }
        teifJson: { type: string, description: "Generated TEIF document as a JSON string" }
        teifXml: { type: string, description: "Generated TEIF document as raw XML — the real, persisted, signed snapshot once signed" }
        ttnQrCodeBase64: { type: string, nullable: true }
        localSignedXml: { type: string, nullable: true }
        validationErrors: { type: array, items: { type: object }, nullable: true }
        ttnAcknowledgments: { type: array, items: { type: object }, nullable: true }

    TeifValidationResult:
      type: object
      properties:
        valid: { type: boolean }
        issues:
          type: array
          items:
            type: object
            properties:
              type: { type: string, example: XML_SCHEMA_VIOLATION }
              message: { type: string }
              severity: { type: string, example: ERROR }
              xpath: { type: string, example: "line:1" }

    JobCreated:
      type: object
      description: Initial response from POST sign-and-send.
      properties:
        jobId: { type: string }
        invoiceId: { type: string }
        jobType: { type: string, example: SIGN_AND_SUBMIT_SEAL }
        status: { type: string, example: PENDING }
        success: { type: boolean }
        message: { type: string }

    JobStatus:
      type: object
      properties:
        jobId: { type: string }
        invoiceId: { type: string }
        jobType: { type: string }
        status: { type: string, example: COMPLETED }
        progress: { type: integer }
        currentStep: { type: string, nullable: true }
        errorMessage: { type: string, nullable: true }
        outputData: { type: object, nullable: true }
        signatureResult: { type: object, nullable: true }
        ttnSubmissionResult: { type: object, nullable: true }
        ttnFinalResult: { type: object, nullable: true }
        createdAt: { type: string }
        startedAt: { type: string, nullable: true }
        completedAt: { type: string, nullable: true }
        retryCount: { type: integer }

    WebhookSubscription:
      type: object
      description: >
        Response shape for webhook endpoints. `secret` is only present in the POST /webhooks
        creation response (returned once, never again on subsequent GETs).
      properties:
        id: { type: string }
        appId: { type: string, description: "Your API key's client_id" }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        customData: { type: object }
        status: { type: string, example: ACTIVE }
        secret: { type: string, description: "Only present once, on the creation response" }
        expiresAt: { type: string }
        createdAt: { type: string }
        updatedAt: { type: string }
