openapi: 3.0.3
info:
  title: Payment Processing API
  version: 1.0.0
  description: Atomic internal JOD transfers. Send a unique UUID per logical instruction; retry unknown outcomes with the same ID.
servers:
  - url: https://localhost:8443
security:
  - bearerJwt: []
paths:
  /api/v1/payments:
    post:
      operationId: submitPayment
      summary: Submit an authorized payment with payments:submit scope
      description: All accounts and current funds are checked under transaction locks. Exactly one transfer is committed per UUID. Equivalent retries return the original receipt; durable rejections preserve their original reason. Amounts use exact decimals and receipts have scale 3.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PaymentInstruction'}
            example:
              paymentId: d92192df-f79a-4553-8120-5660d2eb1294
              fromAccount: ACC-1001
              toAccount: ACC-2002
              amount: '125.750'
              date: '2026-10-28T13:30:00+03:00'
              purpose: Invoice INV-2048
              notes: October services
          application/xml:
            schema: {$ref: '#/components/schemas/PaymentInstruction'}
            example: '<payment><paymentId>d92192df-f79a-4553-8120-5660d2eb1294</paymentId><fromAccount>ACC-1001</fromAccount><toAccount>ACC-2002</toAccount><amount>125.750</amount><date>2026-10-28T13:30:00+03:00</date><purpose>Invoice INV-2048</purpose><notes>October services</notes></payment>'
      responses:
        '201':
          description: First committed completion
          headers:
            Location: {schema: {type: string}, description: Receipt retrieval URL}
            X-Correlation-ID: {schema: {type: string, format: uuid}}
          content:
            application/json: {schema: {$ref: '#/components/schemas/Receipt'}}
            application/xml: {schema: {$ref: '#/components/schemas/Receipt'}}
        '200': {$ref: '#/components/responses/ReceiptReplay'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthenticated'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '406': {$ref: '#/components/responses/NotAcceptable'}
        '409': {$ref: '#/components/responses/Conflict'}
        '413': {$ref: '#/components/responses/TooLarge'}
        '415': {$ref: '#/components/responses/UnsupportedMedia'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/InternalError'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /api/v1/payments/{paymentId}:
    get:
      operationId: retrievePayment
      summary: Retrieve a permitted payment with payments:read scope
      parameters:
        - in: path
          name: paymentId
          required: true
          schema: {type: string, format: uuid}
      responses:
        '200':
          description: Stored completed receipt or durable REJECTED error outcome
          content:
            application/json: {schema: {oneOf: [{$ref: '#/components/schemas/Receipt'}, {$ref: '#/components/schemas/Error'}]}}
            application/xml: {schema: {oneOf: [{$ref: '#/components/schemas/Receipt'}, {$ref: '#/components/schemas/Error'}]}}
        '401': {$ref: '#/components/responses/Unauthenticated'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '406': {$ref: '#/components/responses/NotAcceptable'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/InternalError'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /api/v1/accounts/{accountId}/balance:
    get:
      operationId: retrieveBalance
      summary: Retrieve an authorized account balance with payments:read scope
      parameters:
        - in: path
          name: accountId
          required: true
          schema: {type: string, maxLength: 128}
      responses:
        '200':
          description: Current available persisted balance
          content:
            application/json: {schema: {$ref: '#/components/schemas/Balance'}}
            application/xml: {schema: {$ref: '#/components/schemas/Balance'}}
        '401': {$ref: '#/components/responses/Unauthenticated'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '406': {$ref: '#/components/responses/NotAcceptable'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/InternalError'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /api/v1/admin/reconciliation:
    get:
      operationId: reconcile
      summary: Reconcile ledger and balances with payments:admin scope
      responses:
        '200':
          description: Consistent snapshot of reconciliation findings; healthy false requires investigation
          content:
            application/json: {schema: {$ref: '#/components/schemas/Reconciliation'}}
            application/xml: {schema: {$ref: '#/components/schemas/Reconciliation'}}
        '401': {$ref: '#/components/responses/Unauthenticated'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '406': {$ref: '#/components/responses/NotAcceptable'}
        '429': {$ref: '#/components/responses/RateLimited'}
        '500': {$ref: '#/components/responses/InternalError'}
        '503': {$ref: '#/components/responses/Unavailable'}
components:
  securitySchemes:
    bearerJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: RS256 JWT from the configured OIDC issuer/audience, with required OAuth scope and account permissions.
  schemas:
    PaymentInstruction:
      type: object
      additionalProperties: false
      xml: {name: payment}
      required: [paymentId, fromAccount, toAccount, amount, date, purpose]
      properties:
        paymentId: {type: string, format: uuid}
        fromAccount: {type: string, minLength: 1, maxLength: 128}
        toAccount: {type: string, minLength: 1, maxLength: 128}
        amount:
          oneOf:
            - {type: string, pattern: '^[0-9]+(\.[0-9]{1,3})?$'}
            - {type: number, minimum: 0, exclusiveMinimum: true, maximum: 10000, multipleOf: 0.001}
          description: Positive exact decimal. Default configurable maximum 10000.000 JOD, at most 3 fractional digits.
        date: {type: string, format: date-time, description: Offset required; at most 5 minutes future and 30 days old for fresh instructions.}
        purpose: {type: string, minLength: 1, maxLength: 255}
        notes: {type: string, maxLength: 1000}
    Receipt:
      type: object
      xml: {name: response}
      required: [paymentId, status, fromAccount, toAccount, amount, currency, processedAt]
      properties:
        paymentId: {type: string, format: uuid}
        status: {type: string, enum: [COMPLETED]}
        fromAccount: {type: string}
        toAccount: {type: string}
        amount: {type: string, pattern: '^[0-9]+\.[0-9]{3}$', example: '125.750'}
        currency: {type: string, enum: [JOD]}
        processedAt: {type: string, format: date-time}
    Balance:
      type: object
      properties:
        accountId: {type: string}
        availableBalance: {type: string, example: '374.250'}
        currency: {type: string, enum: [JOD]}
    Error:
      type: object
      xml: {name: response}
      required: [status, code, message, issues, correlationId, timestamp]
      properties:
        paymentId: {type: string, format: uuid}
        status: {type: string, enum: [REJECTED, ERROR]}
        code: {type: string}
        message: {type: string}
        issues:
          type: array
          items:
            type: object
            properties:
              field: {type: string}
              code: {type: string}
              message: {type: string}
        correlationId: {type: string, format: uuid}
        timestamp: {type: string, format: date-time}
    Reconciliation:
      type: object
      properties:
        healthy: {type: boolean}
        timestamp: {type: string, format: date-time}
        issues: {type: array, items: {type: object, additionalProperties: true}}
  responses:
    ReceiptReplay:
      description: Original receipt; no additional financial effect
      content:
        application/json: {schema: {$ref: '#/components/schemas/Receipt'}}
        application/xml: {schema: {$ref: '#/components/schemas/Receipt'}}
    BadRequest:
      description: MALFORMED_MESSAGE, VALIDATION_FAILED or TLS_REQUIRED; no funds move
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    Unauthenticated:
      description: UNAUTHENTICATED; missing, expired or invalid JWT
      headers: {WWW-Authenticate: {schema: {type: string, example: Bearer}}}
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    Forbidden:
      description: FORBIDDEN; scope, debit or destination authorization denied; account existence is not disclosed
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    NotFound:
      description: NOT_FOUND; unknown or undisclosed payment/account. ACCOUNT_NOT_FOUND is not disclosed to unrelated callers.
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    NotAcceptable:
      description: NOT_ACCEPTABLE; JSON/XML responses only
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
    Conflict:
      description: ACCOUNT_NOT_ACTIVE, INSUFFICIENT_FUNDS, IDEMPOTENCY_CONFLICT or PAYMENT_LIMIT_EXCEEDED; definitive rejection
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    TooLarge:
      description: PAYLOAD_TOO_LARGE; configurable request size limit exceeded
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    UnsupportedMedia:
      description: UNSUPPORTED_MEDIA_TYPE; application/json and application/xml only
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    RateLimited:
      description: RATE_LIMITED; authenticated principal minute quota exceeded
      headers: {Retry-After: {schema: {type: integer, example: 1}}}
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    InternalError:
      description: INTERNAL_ERROR; unanticipated failure, query/retry original ID if response outcome is uncertain
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
    Unavailable:
      description: TEMPORARILY_UNAVAILABLE; DB unavailable or bounded transient retries exhausted; query/retry same ID
      headers: {Retry-After: {schema: {type: integer, example: 1}}}
      content:
        application/json: {schema: {$ref: '#/components/schemas/Error'}}
        application/xml: {schema: {$ref: '#/components/schemas/Error'}}
