openapi: 3.1.0
info:
  title: ФИНАС — Partner API
  version: 1.0.0
  description: |
    API для партнёрских систем (МФО, бухгалтерские сервисы, финмаркетплейсы).
    Позволяет партнёру выдавать клиентам пакеты финансовых услуг ФИНАС
    в момент оформления своего основного продукта.

    **Аутентификация:** Bearer-токен в заголовке `Authorization`.
    Ключ выдаётся партнёру при подключении.

    **Base URL (прод):** `https://fin-ac.ru`

servers:
  - url: https://fin-ac.ru
    description: Production
  - url: http://localhost:3000
    description: Local development

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API-ключ партнёра вида `pk_live_...`

  schemas:
    PackageBrief:
      type: object
      properties:
        slug:
          type: string
          example: individual-medium
        name:
          type: string
          example: Медиум
        credits:
          type: integer
          example: 3
      required: [slug, name, credits]

    TransactionResponse:
      type: object
      properties:
        transaction_id:
          type: string
          format: uuid
          description: |
            Уникальный ID транзакции в **нашей** системе (не путать с вашим `external_id`).
            Сохраните его, если хотите позже проверить статус через `GET /api/v1/partners/transactions/{transaction_id}`.
          example: d29a797c-17c6-484b-b929-6fa21d1ec6c5
        status:
          type: string
          enum: [active, partially_used, fully_used, expired]
          description: |
            Текущий статус пакета:
            - `active` — пакет выдан, кредиты не использованы
            - `partially_used` — клиент использовал часть услуг
            - `fully_used` — все кредиты израсходованы
            - `expired` — истёк срок TTL, неиспользованные кредиты сгорели
          example: active
        package:
          $ref: '#/components/schemas/PackageBrief'
        expires_at:
          type: string
          format: date-time
          description: |
            Дата и время истечения пакета (UTC). Через 21 день после выдачи.
            После этой даты клиент не сможет воспользоваться неиспользованными кредитами.
          example: '2026-05-20T09:43:07.283Z'
      required: [transaction_id, status, package, expires_at]

    TransactionStatusResponse:
      type: object
      properties:
        transaction_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [active, partially_used, fully_used, expired]
        package:
          $ref: '#/components/schemas/PackageBrief'
        credits_remaining:
          type: integer
          description: Сколько кредитов (услуг) осталось
          example: 2
        credits_used:
          type: integer
          description: Сколько кредитов уже использовано
          example: 1
        activated:
          type: boolean
          description: Клиент уже заходил в кабинет и видел пакет
        activated_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    PackageCatalogItem:
      type: object
      properties:
        slug:
          type: string
          example: individual-medium
        name:
          type: string
          example: Медиум
        description:
          type: string
          nullable: true
          example: Полный разбор финансов с антикризисным планом
        credits:
          type: integer
          description: Количество услуг в пакете
          example: 3
        available_services:
          type: array
          items:
            type: string
          description: Slug-и услуг, доступных клиенту
          example: [tax-calculator, personal-finance-checkup, anti-crisis-plan]
        price_b2b2c_kopecks:
          type: integer
          description: Цена для партнёра в копейках
          example: 200000
        ttl_days_partner:
          type: integer
          nullable: true
          description: Срок действия пакета в днях (после выдачи партнёром). null — не сгорает.
          example: 21

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: external_id is required
      required: [error]

paths:
  /api/v1/partners/packages:
    get:
      summary: Список доступных пакетов
      description: |
        Возвращает каталог активных пакетов с ценами для партнёра и составом услуг.
        Используйте для отображения выбора пакета в вашей CRM.
      operationId: listPackages
      tags: [Packages]
      responses:
        '200':
          description: Список пакетов
          content:
            application/json:
              schema:
                type: object
                properties:
                  packages:
                    type: array
                    items:
                      $ref: '#/components/schemas/PackageCatalogItem'
              example:
                packages:
                  - slug: individual-super-light
                    name: Супер Лайт
                    description: Узнайте свою налоговую нагрузку
                    credits: 1
                    available_services: [tax-calculator]
                    price_b2b2c_kopecks: 50000
                    ttl_days_partner: 21
                  - slug: individual-medium
                    name: Медиум
                    description: Полный разбор финансов с антикризисным планом
                    credits: 3
                    available_services: [tax-calculator, personal-finance-checkup, anti-crisis-plan]
                    price_b2b2c_kopecks: 200000
                    ttl_days_partner: 21
        '401':
          description: Неверный или отсутствующий API-ключ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/partners/transactions:
    post:
      summary: Создать транзакцию (выдать пакет клиенту)
      description: |
        Вызывается в момент продажи вашего продукта клиенту (оформление займа, открытие счёта и т.д.).
        Создаёт запись в нашей системе и привязывает пакет к клиенту по номеру телефона.

        **Идемпотентность:** повторный запрос с тем же `external_id` вернёт `200` и существующую транзакцию — новая запись создана не будет.

        **Несколько пакетов у одного клиента:** один клиент может получить несколько пакетов одновременно — они все будут активны параллельно до истечения TTL.
      operationId: createTransaction
      tags: [Transactions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [external_id, package_slug, client]
              properties:
                external_id:
                  type: string
                  description: |
                    **ID сделки в вашей системе** (не у нас).

                    Используется для защиты от дублей: если запрос отправить дважды
                    с одним и тем же `external_id` — грант создастся только один раз,
                    второй вызов вернёт `200` с уже существующей транзакцией.

                    Передавайте любой уникальный идентификатор сделки из вашей CRM —
                    номер займа, ID заявки, номер договора и т.д.
                  example: loan-12345
                package_slug:
                  type: string
                  description: |
                    Slug пакета, который вы хотите выдать клиенту.
                    Актуальный список slug-ов — в `GET /api/v1/partners/packages`.
                  example: individual-medium
                  enum:
                    - individual-super-light
                    - individual-light
                    - individual-medium
                    - individual-premium
                    - entrepreneur-super-light
                    - entrepreneur-light
                    - entrepreneur-light-plus
                    - entrepreneur-medium
                    - entrepreneur-premium
                client:
                  type: object
                  required: [phone, email]
                  description: Данные клиента. Телефон и email обязательны, остальное — опционально.
                  properties:
                    phone:
                      type: string
                      description: |
                        Номер телефона клиента в формате E.164 (`+7...`).
                        Хранится в системе для связи и идентификации.
                        Если клиент уже существует — мы привяжем грант к существующему аккаунту.
                      example: '+79001234567'
                    email:
                      type: string
                      format: email
                      description: |
                        Email клиента — **главный идентификатор для входа в кабинет**.
                        Клиент войдёт по email + одноразовому коду из письма. Обязателен.
                      example: ivan@example.com
                    first_name:
                      type: string
                      description: Имя клиента.
                      example: Иван
                    last_name:
                      type: string
                      description: Фамилия клиента.
                      example: Иванов
                    middle_name:
                      type: string
                      description: Отчество клиента.
                      example: Иванович
                    inn:
                      type: string
                      description: ИНН клиента (12 цифр для физлица, 10 для ИП/организации). Опционально.
                      example: '770000000000'
                metadata:
                  type: object
                  additionalProperties: true
                  description: |
                    Произвольные данные вашей системы — сохраняются «как есть» для аналитики и сверки.
                    Сюда можно передать ID менеджера, название филиала, тип продукта и т.д.
                    На логику не влияют.
                  example:
                    loan_id: '12345'
                    branch: Москва
                    manager_id: user-42
                loan_context:
                  type: object
                  description: |
                    **Контекст текущего займа** — опциональное поле для МКК-партнёров.
                    Если передано, отчёты для клиента строятся с учётом параметров
                    выданного займа, а анкеты долговых продуктов автоматически
                    заполняются — клиент не вводит то, что мы и так знаем от вас.

                    **Юр.требование:** ваш договор с заёмщиком должен содержать
                    согласие на передачу финансовых данных в ФИН-АС для целей анализа
                    (см. агентский договор).
                  required: [amount_kopecks, term_months]
                  properties:
                    amount_kopecks:
                      type: integer
                      description: Сумма займа в копейках. Целое, > 0, ≤ 1 000 000 000 (10 млн ₽).
                      example: 5000000
                    term_months:
                      type: integer
                      minimum: 1
                      maximum: 360
                      description: Срок займа в месяцах.
                      example: 12
                    monthly_payment_kopecks:
                      type: integer
                      nullable: true
                      description: Ежемесячный платёж в копейках. Опционально.
                      example: 500000
                    interest_rate_annual_pct:
                      type: number
                      nullable: true
                      minimum: 0
                      maximum: 365
                      description: |
                        Эффективная ставка годовых, %. Опционально.
                        Лимит 365% соответствует ограничению ставки МКК
                        по 353-ФЗ (~292% эффективная).
                      example: 35.5
                    issued_at:
                      type: string
                      format: date
                      nullable: true
                      description: Дата выдачи займа (ISO `YYYY-MM-DD`).
                      example: '2026-05-07'
                    loan_type:
                      type: string
                      nullable: true
                      enum: [pdl, consumer, pos, other]
                      description: |
                        Тип займа:
                        - `pdl` — payday loan (микрозайм до зарплаты)
                        - `consumer` — потребительский
                        - `pos` — POS-кредит / товарная рассрочка
                        - `other` — другое
                      example: pdl
            example:
              external_id: loan-12345
              package_slug: individual-medium
              client:
                phone: '+79001234567'
                email: ivan@example.com
                first_name: Иван
                last_name: Иванов
              loan_context:
                amount_kopecks: 5000000
                term_months: 12
                monthly_payment_kopecks: 500000
                interest_rate_annual_pct: 35.5
                issued_at: '2026-05-07'
                loan_type: pdl
              metadata:
                loan_id: '12345'
                branch: Москва
      responses:
        '201':
          description: Транзакция создана успешно
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
              example:
                transaction_id: d29a797c-17c6-484b-b929-6fa21d1ec6c5
                status: active
                package:
                  slug: individual-medium
                  name: Медиум
                  credits: 3
                expires_at: '2026-05-20T09:43:07.283Z'
        '200':
          description: Идемпотентный ответ — транзакция с таким external_id уже существует
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
        '400':
          description: Ошибка валидации (не хватает обязательного поля)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                no_external_id:
                  value:
                    error: external_id is required
                no_phone:
                  value:
                    error: client.phone is required
        '401':
          description: Неверный или отсутствующий API-ключ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Пакет с таким slug не найден или неактивен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Конфликт — external_id уже используется с другим package_slug
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: 'Conflict: external_id already used with a different package_slug'

  /api/v1/partners/transactions/{transaction_id}:
    get:
      summary: Статус транзакции
      description: Возвращает текущий статус выданного пакета — сколько кредитов осталось, зашёл ли клиент, не истёк ли срок.
      operationId: getTransaction
      tags: [Transactions]
      parameters:
        - name: transaction_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: transaction_id из ответа POST /api/v1/partners/transactions
          example: d29a797c-17c6-484b-b929-6fa21d1ec6c5
      responses:
        '200':
          description: Статус транзакции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionStatusResponse'
              example:
                transaction_id: d29a797c-17c6-484b-b929-6fa21d1ec6c5
                status: partially_used
                package:
                  slug: individual-medium
                  name: Медиум
                  credits: 3
                credits_remaining: 2
                credits_used: 1
                activated: true
                activated_at: '2026-04-30T14:22:00.000Z'
                expires_at: '2026-05-20T09:43:07.283Z'
                created_at: '2026-04-29T09:43:07.283Z'
        '401':
          description: Неверный или отсутствующий API-ключ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Транзакция не найдена или принадлежит другому партнёру
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/partners/transactions/{transaction_id}/certificate:
    get:
      summary: Сертификат для печати
      description: |
        Возвращает готовый HTML-сертификат, заполненный данными клиента и пакета.
        Партнёр открывает ссылку в браузере → **Cmd+P** → печатает или сохраняет как PDF → вручает клиенту.

        Данные заполняются автоматически: ФИО, телефон, пакет, услуги, дата, срок действия.

        Для использования шаблона без API — скачайте `/certificate-template.html`.
      operationId: getCertificate
      tags: [Certificates]
      parameters:
        - name: transaction_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          example: d29a797c-17c6-484b-b929-6fa21d1ec6c5
      responses:
        '200':
          description: HTML-сертификат, готовый для печати
          content:
            text/html:
              schema:
                type: string
        '401':
          description: Неверный или отсутствующий API-ключ
        '404':
          description: Транзакция не найдена или принадлежит другому партнёру

tags:
  - name: Packages
    description: Каталог пакетов — для отображения в вашей CRM
  - name: Transactions
    description: Создание и отслеживание выданных клиентам пакетов
  - name: Certificates
    description: Печатные сертификаты для клиентов
