{
  "info": {
    "_postman_id": "f1e2d3c4-b5a6-4789-9abc-def012345678",
    "name": "ФИНАС — Partner API",
    "description": "Postman-коллекция для интеграции с партнёрским API ФИНАС.\n\n## Что внутри\n\n4 эндпоинта в порядке типичного использования:\n1. **GET** `/api/v1/partners/packages` — каталог пакетов с ценами и составом услуг\n2. **POST** `/api/v1/partners/transactions` — выдать пакет клиенту (с идемпотентностью по `external_id`)\n3. **GET** `/api/v1/partners/transactions/{transaction_id}` — статус выданного пакета\n4. **GET** `/api/v1/partners/transactions/{transaction_id}/certificate` — HTML-сертификат для печати\n\nДополнительно — пять примеров негативных сценариев (невалидный slug, дубль external_id, без email и т. д.) для проверки обработки ошибок.\n\n## Как начать\n\n1. Импортируйте коллекцию в Postman.\n2. Откройте вкладку **Variables** коллекции.\n3. Заполните `apiKey` — тестовый ключ можно получить на странице [/partners/sandbox](https://fin-ac.ru/partners/sandbox).\n4. При необходимости поменяйте `baseUrl`:\n   - `https://fin-ac.ru` — прод\n   - `http://localhost:3000` — локальная разработка\n5. Запустите запросы по порядку. После выполнения **2. Создать транзакцию** переменная `transactionId` сохранится автоматически и подставится в шаги 3 и 4.\n\n## Аутентификация\n\nBearer-токен в заголовке `Authorization`. Уже настроен на уровне коллекции через `{{apiKey}}` — отдельные запросы наследуют.\n\n## Идемпотентность\n\nПовторный POST с тем же `external_id` вернёт `200` и существующую транзакцию — это нормально.\n\n## Документация\n\n- Swagger UI: https://fin-ac.ru/api-docs\n- OpenAPI 3.1 spec: https://fin-ac.ru/openapi.yaml\n- Sandbox: https://fin-ac.ru/partners/sandbox",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "1. Список доступных пакетов",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/v1/partners/packages",
          "host": ["{{baseUrl}}"],
          "path": ["api", "v1", "partners", "packages"]
        },
        "description": "Возвращает каталог активных пакетов с ценами для партнёра и составом услуг.\n\nИспользуйте для отображения выбора пакета в вашей CRM. Slug нужного пакета передавайте в следующий запрос (`POST /api/v1/partners/transactions`).\n\n**Ответ 200:**\n```json\n{\n  \"packages\": [\n    {\n      \"slug\": \"individual-medium\",\n      \"name\": \"Медиум\",\n      \"description\": \"Полный разбор финансов с антикризисным планом\",\n      \"credits\": 3,\n      \"available_services\": [\"tax-calculator\", \"personal-finance-checkup\", \"anti-crisis-plan\"],\n      \"price_b2b2c_kopecks\": 200000,\n      \"ttl_days_partner\": 21\n    }\n  ]\n}\n```"
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test(\"Status code is 200\", function () {",
              "    pm.response.to.have.status(200);",
              "});",
              "",
              "pm.test(\"Response has packages array\", function () {",
              "    const json = pm.response.json();",
              "    pm.expect(json).to.have.property(\"packages\").that.is.an(\"array\");",
              "    pm.expect(json.packages.length).to.be.greaterThan(0);",
              "});"
            ]
          }
        }
      ]
    },
    {
      "name": "2. Создать транзакцию (выдать пакет)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"external_id\": \"loan-{{$timestamp}}\",\n  \"package_slug\": \"individual-medium\",\n  \"client\": {\n    \"phone\": \"+79001234567\",\n    \"email\": \"test@example.com\",\n    \"first_name\": \"Иван\",\n    \"last_name\": \"Иванов\"\n  },\n  \"loan_context\": {\n    \"amount_kopecks\": 5000000,\n    \"term_months\": 12,\n    \"monthly_payment_kopecks\": 500000,\n    \"interest_rate_annual_pct\": 35.5,\n    \"issued_at\": \"2026-05-07\",\n    \"loan_type\": \"pdl\"\n  },\n  \"metadata\": {\n    \"loan_id\": \"12345\",\n    \"branch\": \"Москва\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{baseUrl}}/api/v1/partners/transactions",
          "host": ["{{baseUrl}}"],
          "path": ["api", "v1", "partners", "transactions"]
        },
        "description": "Вызывается в момент продажи вашего продукта клиенту (оформление займа, открытие счёта и т.д.).\n\nСоздаёт запись в нашей системе и привязывает пакет к клиенту по номеру телефона и email.\n\n## Поля\n\n- **external_id** *(обязательно)* — ID сделки в **вашей** системе. Используется для защиты от дублей: повторный POST с тем же `external_id` вернёт `200` (не `201`) с уже существующей транзакцией.\n- **package_slug** *(обязательно)* — slug пакета из `GET /api/v1/partners/packages`.\n- **client.phone** *(обязательно)* — телефон в формате E.164 (`+7...`).\n- **client.email** *(обязательно)* — email клиента, главный идентификатор для входа в кабинет.\n- **client.first_name / last_name / middle_name / inn** — опционально.\n- **metadata** — произвольные поля для аналитики, на логику не влияют.\n\n## Скрипт после ответа\n\nПри успехе сохраняет `transaction_id` в переменную коллекции `transactionId` — она автоматически подставится в шаги 3 и 4.\n\n## Возможные ответы\n\n- `201` — транзакция создана\n- `200` — идемпотентный ответ (такой external_id уже был)\n- `400` — ошибка валидации\n- `401` — неверный API-ключ\n- `404` — package_slug не найден\n- `409` — external_id уже использован с другим package_slug"
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test(\"Status is 201 or 200 (idempotent)\", function () {",
              "    pm.expect(pm.response.code).to.be.oneOf([200, 201]);",
              "});",
              "",
              "const json = pm.response.json();",
              "",
              "pm.test(\"Response has transaction_id\", function () {",
              "    pm.expect(json).to.have.property(\"transaction_id\");",
              "});",
              "",
              "pm.test(\"Response has package info\", function () {",
              "    pm.expect(json).to.have.property(\"package\");",
              "    pm.expect(json.package).to.have.property(\"slug\");",
              "    pm.expect(json.package).to.have.property(\"credits\");",
              "});",
              "",
              "pm.test(\"Status is active\", function () {",
              "    pm.expect(json.status).to.eql(\"active\");",
              "});",
              "",
              "// Сохраняем transaction_id для следующих шагов",
              "if (json.transaction_id) {",
              "    pm.collectionVariables.set(\"transactionId\", json.transaction_id);",
              "    console.log(\"Сохранён transactionId:\", json.transaction_id);",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "3. Статус транзакции",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/v1/partners/transactions/{{transactionId}}",
          "host": ["{{baseUrl}}"],
          "path": ["api", "v1", "partners", "transactions", "{{transactionId}}"]
        },
        "description": "Возвращает текущий статус выданного пакета: сколько кредитов осталось, зашёл ли клиент в кабинет, не истёк ли срок.\n\n`{{transactionId}}` подставляется автоматически из ответа предыдущего запроса.\n\n## Поля ответа\n\n- **status** — `active` / `partially_used` / `fully_used` / `expired`\n- **credits_remaining** — сколько услуг ещё доступно клиенту\n- **credits_used** — сколько уже использовано\n- **activated** — заходил ли клиент в кабинет\n- **activated_at** — когда впервые активировал\n- **expires_at** — дата истечения TTL\n\n## Возможные ответы\n\n- `200` — статус получен\n- `401` — неверный API-ключ\n- `404` — транзакция не найдена или принадлежит другому партнёру"
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test(\"Status code is 200\", function () {",
              "    pm.response.to.have.status(200);",
              "});",
              "",
              "pm.test(\"Response has credit counts\", function () {",
              "    const json = pm.response.json();",
              "    pm.expect(json).to.have.property(\"credits_remaining\");",
              "    pm.expect(json).to.have.property(\"credits_used\");",
              "    pm.expect(json).to.have.property(\"status\");",
              "});",
              "",
              "pm.test(\"transaction_id matches saved value\", function () {",
              "    const json = pm.response.json();",
              "    pm.expect(json.transaction_id).to.eql(pm.collectionVariables.get(\"transactionId\"));",
              "});"
            ]
          }
        }
      ]
    },
    {
      "name": "4. HTML-сертификат для печати",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/v1/partners/transactions/{{transactionId}}/certificate",
          "host": ["{{baseUrl}}"],
          "path": ["api", "v1", "partners", "transactions", "{{transactionId}}", "certificate"]
        },
        "description": "Возвращает готовый HTML-сертификат, заполненный данными клиента и пакета.\n\nПартнёр открывает HTML в браузере → **Cmd+P** → печатает или сохраняет как PDF → вручает клиенту.\n\nДанные подставляются автоматически: ФИО, телефон, пакет, услуги, дата, срок действия, номер сертификата `FINS-{КОД-ПАРТНЁРА}-XXXXXXXX`.\n\nДля сохранения в файл из Postman: правый верх ответа → **Save Response → Save to a file** → `certificate.html`.\n\n## Возможные ответы\n\n- `200` — HTML-документ\n- `401` — неверный API-ключ\n- `404` — транзакция не найдена или принадлежит другому партнёру"
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test(\"Status code is 200\", function () {",
              "    pm.response.to.have.status(200);",
              "});",
              "",
              "pm.test(\"Content-Type is text/html\", function () {",
              "    pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"text/html\");",
              "});",
              "",
              "pm.test(\"HTML contains FINS cert number\", function () {",
              "    pm.expect(pm.response.text()).to.match(/FINS-[A-Z0-9]+-[A-F0-9]+/);",
              "});"
            ]
          }
        }
      ]
    },
    {
      "name": "Негативные сценарии",
      "description": "Запросы для проверки обработки ошибок партнёрской системой. Полезно при первичной интеграции, чтобы убедиться что вы корректно реагируете на 400/401/404/409.",
      "item": [
        {
          "name": "401 — без API-ключа",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partners/packages",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "partners", "packages"]
            },
            "description": "Запрос без `Authorization` заголовка. Ожидается ответ `401 Unauthorized`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 401\", function () {",
                  "    pm.response.to.have.status(401);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "400 — без обязательного email",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_id\": \"test-no-email-{{$timestamp}}\",\n  \"package_slug\": \"individual-medium\",\n  \"client\": {\n    \"phone\": \"+79001234567\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/partners/transactions",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "partners", "transactions"]
            },
            "description": "POST без `client.email`. Ожидается `400 Bad Request` с сообщением о недостающем поле."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 400\", function () {",
                  "    pm.response.to.have.status(400);",
                  "});",
                  "",
                  "pm.test(\"Response has error message\", function () {",
                  "    pm.expect(pm.response.json()).to.have.property(\"error\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "404 — несуществующий package_slug",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_id\": \"test-bad-slug-{{$timestamp}}\",\n  \"package_slug\": \"nonexistent-package\",\n  \"client\": {\n    \"phone\": \"+79001234567\",\n    \"email\": \"test@example.com\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/partners/transactions",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "partners", "transactions"]
            },
            "description": "POST с невалидным `package_slug`. Ожидается `404 Not Found`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 404\", function () {",
                  "    pm.response.to.have.status(404);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "200 — идемпотентность (повторный POST)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_id\": \"idempotent-fixed-id-001\",\n  \"package_slug\": \"individual-medium\",\n  \"client\": {\n    \"phone\": \"+79001234567\",\n    \"email\": \"test@example.com\",\n    \"first_name\": \"Иван\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/partners/transactions",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "partners", "transactions"]
            },
            "description": "Запустите этот запрос **дважды**. Первый раз вернётся `201 Created`, второй — `200 OK` с тем же `transaction_id` (защита от дублей по `external_id`)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201 or 200\", function () {",
                  "    pm.expect(pm.response.code).to.be.oneOf([200, 201]);",
                  "});",
                  "",
                  "if (pm.response.code === 200) {",
                  "    console.log(\"Идемпотентный ответ — транзакция уже существует, новая запись не создавалась.\");",
                  "} else {",
                  "    console.log(\"Транзакция создана. Запустите запрос ещё раз для проверки идемпотентности.\");",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "409 — external_id с другим package_slug",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_id\": \"idempotent-fixed-id-001\",\n  \"package_slug\": \"individual-light\",\n  \"client\": {\n    \"phone\": \"+79001234567\",\n    \"email\": \"test@example.com\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/partners/transactions",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "partners", "transactions"]
            },
            "description": "Запустите **после** запроса «200 — идемпотентность» — он использует тот же `external_id`, но другой `package_slug`. Ожидается `409 Conflict`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 409\", function () {",
                  "    pm.response.to.have.status(409);",
                  "});",
                  "",
                  "pm.test(\"Error mentions package_slug\", function () {",
                  "    pm.expect(pm.response.json().error.toLowerCase()).to.include(\"package\");",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://fin-ac.ru",
      "type": "string",
      "description": "Базовый URL API. Прод: https://fin-ac.ru, локально: http://localhost:3000"
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string",
      "description": "API-ключ партнёра. Получить тестовый: https://fin-ac.ru/partners/sandbox (формат: pk_test_... для sandbox, pk_live_... для прода)"
    },
    {
      "key": "transactionId",
      "value": "",
      "type": "string",
      "description": "ID транзакции — заполняется автоматически после успешного POST /transactions"
    }
  ]
}
