openapi: 3.1.0
info:
  title: PayGrid API
  version: 0.3.6-phase3
  description: |
    Mongolia's bank ↔ wallet payment bridge — Berlin Group NextGenPSD2
    v2.x verbatim adoption + PayGrid extensions (alias DICT, MNQR Switch,
    VRP / mandate framework). See PLAN/05-API-SPEC.md and 15-QR-SWITCH.md
    for the canonical specs.
  contact:
    name: PayGrid Operations
    url: https://paygrid.mn

servers:
  - url: https://api.paygrid.mn/v1
    description: Production (planned)
  - url: https://sandbox.paygrid.mn/v1
    description: Sandbox (planned)
  - url: https://paygrid.mn/v1
    description: Phase-3 single-host sandbox

tags:
  - name: health
  - name: alias
  - name: payments
  - name: psp
  - name: qr
  - name: mandate
  - name: internal

paths:
  /health/live:
    get:
      tags: [health]
      summary: Liveness probe
      responses: { "200": { description: Process is up } }

  /health/ready:
    get:
      tags: [health]
      summary: Readiness probe — checks Postgres + Redis
      responses:
        "200": { description: All dependencies reachable }
        "503": { description: One or more dependencies unreachable }

  /version:
    get:
      tags: [health]
      summary: Build identifier
      responses: { "200": { description: Version object } }

  /aliases:
    post:
      tags: [alias]
      summary: Register a merchant alias
      parameters:
        - $ref: "#/components/parameters/ParticipantBIC"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AliasRegisterRequest" }
      responses:
        "201": { description: Created }
        "401": { description: Owner-attestation JWS invalid }

  /aliases/{alias}:
    get:
      tags: [alias]
      summary: Resolve alias to IBAN + PSP
      parameters:
        - in: path
          name: alias
          required: true
          schema: { type: string, maxLength: 128 }
      responses:
        "200":
          description: Found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AliasResolveResponse" }
        "404": { description: Not found }
    delete:
      tags: [alias]
      summary: Revoke alias (owning PSP only)
      parameters:
        - in: path
          name: alias
          required: true
          schema: { type: string }
        - $ref: "#/components/parameters/ParticipantBIC"
        - in: query
          name: reason
          schema: { type: string }
      responses:
        "204": { description: Revoked }
        "404": { description: Not found / not owned / already revoked }

  /aliases/{alias}/verify:
    post:
      tags: [alias]
      summary: Confirmation of Payee — match expected name
      parameters:
        - in: path
          name: alias
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                expected_name:      { type: string }
                expected_name_hash: { type: string, description: SHA-256 hex }
      responses:
        "200":
          description: EXACT or NO_MATCH verdict
          content:
            application/json:
              schema:
                type: object
                properties:
                  match:        { type: string, enum: [EXACT, CLOSE, NO_MATCH] }
                  match_score:  { type: number }

  /payments/sepa-credit-transfers:
    post:
      tags: [payments]
      summary: Initiate a SEPA credit transfer (Berlin Group SCT)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/ParticipantBIC"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SCTInitiateRequest" }
      responses:
        "201":
          description: Payment intent received
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SCTInitiateResponse" }
        "200": { description: Idempotent replay }
        "409": { description: Idempotency conflict (different body, same key) }
        "422": { description: FRM hard-block / mandate invalid }

  /payments/sepa-credit-transfers/{id}:
    get:
      tags: [payments]
      summary: Get payment intent
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses: { "200": { description: Found } }

  /payments/sepa-credit-transfers/{id}/status:
    get:
      tags: [payments]
      summary: Get payment status
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200":
          description: Status
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SCTStatusResponse" }

  /psp/onboard:
    post:
      tags: [psp]
      summary: Self-onboard PSP (sandbox only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, type, contact_email]
              properties:
                name:               { type: string }
                type:               { type: string, enum: [EMI, NBFI, OPERATOR] }
                country:            { type: string, default: MN }
                contact_email:      { type: string, format: email }
                expected_use_cases: { type: array, items: { type: string } }
                iban_range_prefix:  { type: string }
                webhook_url:        { type: string, format: uri }
      responses:
        "201":
          description: Created — returns assigned BIC, JWT signing keypair, webhook HMAC
        "410": { description: Endpoint disabled (production) }

  /psp/{bic}/balance:
    get:
      tags: [psp]
      summary: Bilateral nostro positions
      parameters:
        - { in: path, name: bic, required: true, schema: { type: string, minLength: 11, maxLength: 11 } }
        - $ref: "#/components/parameters/ParticipantBIC"
      responses:
        "200": { description: Per-counterparty positions }
        "403": { description: Caller not authorised for this BIC }

  /qr/static/create:
    post:
      tags: [qr]
      summary: Generate static MPQR (sticker)
      parameters:
        - $ref: "#/components/parameters/ParticipantBIC"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QRStaticCreateRequest" }
      responses:
        "201":
          description: QR created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QRCreateResponse" }

  /qr/dynamic/create:
    post:
      tags: [qr]
      summary: Generate one-time dynamic MPQR (POS)
      parameters:
        - $ref: "#/components/parameters/ParticipantBIC"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [creditor_iban, creditor_psp_bic, amount_minor, merchant_name, merchant_city, merchant_category_code]
              properties:
                creditor_iban:          { type: string }
                creditor_psp_bic:       { type: string }
                creditor_alias:         { type: string }
                amount_minor:           { type: integer }
                currency:               { type: string, default: MNT }
                merchant_name:          { type: string }
                merchant_city:          { type: string }
                merchant_category_code: { type: string }
                reference:              { type: string }
                order_id:               { type: string }
                ttl_seconds:            { type: integer, default: 300 }
                callback_url:           { type: string, format: uri }
      responses:
        "201": { description: Created — includes pay_uri + image link }

  /qr/cpqr/generate:
    post:
      tags: [qr]
      summary: Generate Consumer-Presented QR
      parameters:
        - $ref: "#/components/parameters/ParticipantBIC"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [debtor_iban, debtor_psp_bic]
              properties:
                debtor_iban:      { type: string }
                debtor_psp_bic:   { type: string }
                debtor_alias:     { type: string }
                max_amount_minor: { type: integer }
                currency:         { type: string, default: MNT }
                ttl_seconds:      { type: integer, default: 300 }
      responses:
        "201": { description: Created — includes ephemeral_token + image link }

  /qr/resolve:
    post:
      tags: [qr]
      summary: Decode an EMVCo TLV payload
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [qr_payload]
              properties:
                qr_payload:    { type: string, description: EMVCo TLV string }
                psu_ip_address:{ type: string }
      responses:
        "200":
          description: Resolved view (DICT-enriched)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QRResolveResponse" }
        "400": { description: invalid_qr — bad TLV, CRC mismatch, unknown alias }

  /qr/{qrID}/status:
    get:
      tags: [qr]
      summary: QR lifecycle status
      parameters:
        - { in: path, name: qrID, required: true, schema: { type: string } }
      responses:
        "200":
          description: Current status
          content:
            application/json:
              schema:
                type: object
                properties:
                  qr_id:      { type: string }
                  status:     { type: string, enum: [CREATED, SCANNED, PAID, EXPIRED, CANCELLED] }
                  payment_id: { type: string }
                  scanned_at: { type: string, format: date-time }
                  paid_at:    { type: string, format: date-time }

  /qr/{qrID}.png:
    get:
      tags: [qr]
      summary: Render QR as PNG
      parameters:
        - { in: path, name: qrID, required: true, schema: { type: string } }
        - { in: query, name: size, schema: { type: integer, minimum: 64, maximum: 1024, default: 256 } }
      responses:
        "200":
          description: PNG image
          content:
            image/png: {}

  /qr/{qrID}:
    delete:
      tags: [qr]
      summary: Cancel / revoke QR
      parameters:
        - { in: path, name: qrID, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/ParticipantBIC"
      responses:
        "204": { description: Cancelled }

  /mandates:
    post:
      tags: [mandate]
      summary: Register a VRP mandate
      parameters:
        - $ref: "#/components/parameters/ParticipantBIC"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MandateCreateRequest" }
      responses:
        "201": { description: Created }
        "401": { description: evidence_jws invalid }

  /mandates/{id}:
    get:
      tags: [mandate]
      summary: Read mandate state + consumed-this-period tally
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/ParticipantBIC"
      responses:
        "200": { description: Mandate state }
        "403": { description: Caller is not party to this mandate }
    delete:
      tags: [mandate]
      summary: Revoke mandate (debtor or creditor PSP)
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/ParticipantBIC"
        - { in: query, name: reason, schema: { type: string } }
      responses:
        "204": { description: Revoked }

  /internal/bank-confirm/{paymentId}:
    post:
      tags: [internal]
      summary: Bank → PayGrid back-channel for async ACSC
      description: |
        Internal — only the debtor bank may call. nginx vhost MUST NOT
        proxy this path beyond the internal Docker network.
      parameters:
        - { in: path, name: paymentId, required: true, schema: { type: string } }
        - { in: header, name: X-Bank-BIC,  required: true, schema: { type: string } }
        - { in: header, name: X-Mock-Token, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                bank_payment_id:     { type: string }
                transaction_status:  { type: string }
                settlement_datetime: { type: string, format: date-time }
                debtor_iban:         { type: string }
                amount:              { type: string }
                bank_bic:            { type: string }
      responses:
        "200": { description: Transitioned (or no-op if already terminal) }

components:
  parameters:
    IdempotencyKey:
      in: header
      name: X-Idempotency-Key
      required: true
      schema: { type: string, maxLength: 100 }
    ParticipantBIC:
      in: header
      name: X-Participant-BIC
      required: true
      schema: { type: string, minLength: 11, maxLength: 11 }

  schemas:
    Money:
      type: object
      required: [currency, amount]
      properties:
        currency: { type: string, example: MNT }
        amount:   { type: string, example: "100000.00" }

    Account:
      type: object
      properties:
        iban: { type: string, minLength: 15, maxLength: 34 }

    AliasRegisterRequest:
      type: object
      required: [alias, type, iban, owner_attestation]
      properties:
        alias:              { type: string, maxLength: 128 }
        type:               { type: string, enum: [PHONE, EMAIL, NID, BUSINESS_REG, RANDOM, MERCHANT] }
        iban:               { type: string }
        owner_attestation:  { type: string, description: "ES256 JWS signed by registering PSP's JWT_SIGN key" }
        owner_name_hash:    { type: string, description: "SHA-256 hex of legal name" }
        owner_name_display: { type: string }
        kyc_level:          { type: string, enum: [BASIC, STANDARD, QUALIFIED, ENHANCED] }
        category_code:      { type: string, description: MCC }

    AliasResolveResponse:
      type: object
      properties:
        alias:                 { type: string }
        type:                  { type: string }
        psp_bic:               { type: string }
        iban:                  { type: string }
        kyc_level:             { type: string }
        merchant_name_display: { type: string }
        category_code:         { type: string }
        verified_business:     { type: boolean }
        active:                { type: boolean }

    SCTInitiateRequest:
      type: object
      required: [instructedAmount, debtorAccount, endToEndIdentification]
      properties:
        instructedAmount: { $ref: "#/components/schemas/Money" }
        debtorAccount:    { $ref: "#/components/schemas/Account" }
        creditorAccount:  { $ref: "#/components/schemas/Account" }
        creditorAlias:    { type: string }
        creditorName:     { type: string }
        remittanceInformationUnstructured: { type: string }
        endToEndIdentification: { type: string, maxLength: 35 }
        binding_jws:           { type: string, description: "PSD2 RTS Article 5 Dynamic Linking" }
        mandate_id:            { type: string, description: "PayGrid VRP — pre-authorised debit" }

    SCTInitiateResponse:
      type: object
      properties:
        transactionStatus: { type: string, enum: [RCVD, PDNG, ACTC, ACFC, ACSP, ACSC, RJCT, CANC] }
        paymentId:         { type: string }
        _links:            { type: object }
        _paygrid:
          type: object
          properties:
            frm_score:  { type: number }
            frm_action: { type: string, enum: [PASS, INVESTIGATE, SOFT_CHALLENGE, HARD_BLOCK] }
            frm_matched: { type: array, items: { type: string } }

    SCTStatusResponse:
      type: object
      properties:
        transactionStatus:  { type: string }
        fundsAvailable:     { type: boolean }
        settlementDateTime: { type: string, format: date-time }
        uetr:               { type: string, format: uuid }

    QRStaticCreateRequest:
      type: object
      required: [creditor_iban, creditor_psp_bic, merchant_name, merchant_city, merchant_category_code]
      properties:
        creditor_iban:          { type: string }
        creditor_psp_bic:       { type: string }
        creditor_alias:         { type: string }
        merchant_name:          { type: string, maxLength: 25 }
        merchant_city:          { type: string, maxLength: 15 }
        merchant_category_code: { type: string }
        currency:               { type: string, default: MNT }
        reference:              { type: string, maxLength: 50 }

    QRCreateResponse:
      type: object
      properties:
        qr_id:        { type: string }
        qr_payload:   { type: string }
        size_bytes:   { type: integer }
        registered_at:{ type: string, format: date-time }

    QRResolveResponse:
      type: object
      properties:
        qr_id:                 { type: string }
        qr_type:               { type: string, enum: [MPQR_STATIC, MPQR_DYNAMIC, CPQR] }
        creditor_iban:         { type: string }
        creditor_psp_bic:      { type: string }
        creditor_alias:        { type: string }
        creditor_name_display: { type: string }
        merchant_category_code:{ type: string }
        merchant_city:         { type: string }
        kyc_level:             { type: string }
        verified_business:     { type: boolean }
        amount_minor:          { type: integer }
        currency:              { type: string }
        reference:             { type: string }
        order_id:              { type: string }
        pay_uri:               { type: string }

    MandateCreateRequest:
      type: object
      required: [debtor_iban, debtor_psp_bic, creditor_iban, creditor_psp_bic, max_amount_minor, period]
      properties:
        debtor_iban:        { type: string }
        debtor_psp_bic:     { type: string }
        creditor_iban:      { type: string }
        creditor_psp_bic:   { type: string }
        creditor_alias:     { type: string }
        max_amount_minor:   { type: integer }
        currency:           { type: string, default: MNT }
        period:             { type: string, enum: [ONESHOT, DAILY, WEEKLY, MONTHLY, YEARLY] }
        period_count_limit: { type: integer, default: 1 }
        purpose:            { type: string, maxLength: 140 }
        evidence_jws:       { type: string, description: "ES256 JWS signed by debtor PSP's JWT_SIGN key" }
        expires_at:         { type: string, format: date-time }
