Project

General

Profile

Partner API (#19245)

Документация по эндпоинтам softera API, реализованным/расширенным в рамках тикета #19245: создание организации-партнёра, поиск организаций, CRUD локаций LOCCAT-каталога, смена GLN организации.

Общее

  • Base URL: https://api-test.docura.net/ap1/partner/api/
  • Аутентификация: заголовок X-Authorization: <api_key> (токен пользователя, поле api_key в таблице users).
  • Заголовок X-App-Key клиентами не используется — на бэкенде эта проверка сейчас не подключена ни к одной цепочке фильтров (мёртвый код с 2023 года), можно не отправлять.
  • Формат — JSON (кроме отдельно оговорённых случаев).
  • Роли (authority): SYSTEM_ADMIN, SYSTEM_SUPPORT, CLIENT_ADMIN, CLIENT_EDITOR, PROVIDER_ADMIN — конкретные допустимые роли см. в описании каждого эндпоинта.

1. Создание организации-партнёра

POST /user/organization

Создаёт новую организацию и добавляет вызывающего пользователя в неё как CLIENT_ADMIN (через отдельный, более ранний шаг в пайплайне создания).

Права: без ограничений по роли — доступно любому аутентифицированному пользователю. Права выражены не через роль вызывающего, а через то, какой организации он сам принадлежит (см. ниже логику operatorId).

Тело запроса:

Поле Тип Обязательное Ограничения
taxId string да 2–20 симв.
registrationNumber string да 2–50 симв.
name string да 2–50 симв.
countryCode string да 2 симв.
locale string да 2 симв., должен существовать в справочнике локалей
errorEmail string нет до 250 симв., формат email
notes string нет до 255 симв.
phones string нет до 50 симв.
email string нет до 150 симв., формат email

Поле integrationProfileType, фигурировавшее в более ранних итерациях этой фичи в рамках того же тикета, из финальной версии убрано — клиент не может выбирать профиль/connectionType явно (см. ниже).

Ответ (200): тот же набор полей + id (Long), iln (сгенерированный GLN организации).

Ошибки:
  • 409 Conflict (organization.create.conflict.tax_id) — организация с таким taxId уже существует.
  • 400 — невалидные поля тела запроса (bean validation), либо не найдена локаль (not.exists.specified.organization.locale).

Логика определения оператора (operatorId) новой организации

Вычисляется по организации САМОГО вызывающего пользователя (callerIln — ILN текущей организации сессии), без явных параметров в запросе:

  1. Если организация вызывающего — сама softera (profile.softeraOrganizationIln) → оператор новой организации = сама softera. Дополнительно в этом случае вызывающему в новую организацию как CLIENT_ADMIN добавляется дефолтный пользователь softera (profile.softeraDefaultUserId, если задан).
  2. Если организация вызывающего — провайдер (role='P', но не softera) → оператор = сама организация вызывающего.
  3. Иначе (не softera и не провайдер) → оператор = организация из profile.defaultOperatorOrganizationIln (fallback на "оператора по умолчанию").
  4. Если ни организация вызывающего, ни fallback-организация не найдены по ILN — 500 (IllegalStateException, не должно происходить в норме).

Логика connectionType новой организации

  • Вызывающий — softera → всегда Payment (жёстко, независимо ни от чего).
  • Иначе → connectionType = connectionType САМОЙ операторской организации (той, что вычислена выше, — провайдера или fallback-организации). Клиент явно указать connectionType/профиль интеграции не может.

2. Поиск организаций

GET /organizations/search?query=<строка>

Права: SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN. Никакого ограничения по operatorId — провайдер видит результаты по всей базе организаций, не только по своим клиентам (осознанное решение, принято в рамках этого тикета).

Параметры:

Параметр Тип Обязательный Описание
query string да Поисковая строка. Пустая/отсутствующая строка → пустой список без ошибки.
Логика поиска в БД — единый OR по организации:
  • точное совпадение id (как текст);
  • точное совпадение iln (GLN);
  • точное совпадение regNr (без учёта регистра);
  • точное совпадение vatNr (без учёта регистра);
  • подстрока (ILIKE) в name организации;
  • подстрока (ILIKE) в name любой её LOCCAT-локации.

Ответ (200): массив, каждый элемент обёрнут в объект organization:

[
  {
    "organization": {
      "id": 100099815,
      "name": "New brand sof",
      "iln": "2000005810008",
      "taxId": "...",
      "registrationNumber": "...",
      "countryCode": "EE",
      "locations": [ { "id": ..., "iln": "...", "name": "...", "formattedAddress": "...", ... } ]
    }
  }
]

Важный нюанс: locations[] содержит только те локации, имя которых само совпало с поисковой строкой — а не все локации найденной организации. Если организация найдена по id/iln/regNr/vatNr/имени, а не по имени локации, locations будет пустым массивом. Это осознанное поведение, не баг.


3. Локации LOCCAT-каталога организации

Базовый путь: /organization/{gln}/locations, где {gln} — GLN организации (не числовой id).

Права (общие для всех трёх методов ниже):
  • SYSTEM_ADMIN / SYSTEM_SUPPORT — без ограничений.
  • PROVIDER_ADMIN — только если организация вызывающего = operatorId целевой организации (провайдер управляет локациями организаций, которые сам оперирует).
  • Прочие роли — 403 (not.have.access).
  • Организация не найдена по GLN → 400 (organization.not.exists).

3.1 POST /organization/{gln}/locations — создание/обновление локации

Upsert-логика:

  • Если в теле передан id — явное обновление существующей локации по её внутреннему id (должна принадлежать указанной организации, иначе 404).
  • Если id не передан, но передан iln — ищется существующая локация организации с таким iln; если найдена — обновляется, если нет — создаётся новая.
  • Если iln тоже не передан (или "пустой" по IlnHelper.isEmpty) — GLN локации генерируется автоматически (см. ниже).

Тело запроса (LoccatLocationWithIdDataLoccatLocationData + опциональный id):

Поле Тип Обязательное Ограничения
id Long нет для явного апдейта
iln string нет до 13 симв.; при отсутствии — автогенерация
codeBySender string да до 70 симв.
codeByReceiver string нет до 70 симв.
name string да до 175 симв.
legal boolean нет признак "юридический адрес"
formattedAddress string нет до 300 симв.
streetAndNumber string нет до 140 симв.
streetAndNumber2 string нет до 140 симв.
city string нет до 175 симв.
state string нет до 175 симв.
postalCode string нет до 9 симв.
countryCode string нет до 2 симв.
latitude / longitude double нет
remarks string нет до 350 симв.
phoneNumber string нет до 175 симв.
email string нет до 350 симв., формат email
web string нет до 175 симв.

Ответ (200): то же тело + id созданной/обновлённой локации.

Автогенерация GLN локации (когда iln не передан)

Переиспользует легаси-алгоритм UserWebController.getNextLocalizationCatalogGln (core):

  1. Если у организации уже есть локации (maxIln найден) → следующий GLN = тело последнего сгенерированного GLN + 1.
  2. Если локаций нет и организация тестовая (isTest) → GLN строится из glnPrefix организации, дополненного нулями.
  3. Если локаций нет, организация не тестовая и нет "legal"-локации → возвращается GLN самой организации.
  4. Если есть legal-локация, но нет других (граничный случай) → GLN строится из glnPrefix.

Дополнительно (правка этого тикета): если сгенерированный таким образом GLN совпадает с GLN самой организации (ветки 2 и 3 выше) — он инкрементируется на 1, чтобы никогда не дублировать GLN организации.

Известные ограничения (приняты как есть, без дополнительной защиты):
  • Нет транзакционной блокировки между "получить следующий GLN" и "создать локацию с ним" — при параллельных запросах без iln возможна гонка и 400 duplicate.line.iln.
  • Инкремент при переполнении (тело GLN = 999999999999) теоретически может дать GLN на 1 символ длиннее нормы — крайне маловероятный edge-case, фикс не запрошен.
  • legal флаг НЕ проставляется автоматически на первую созданную без iln локацию — если у организации нет ни одной legal-локации, повторный POST без iln будет пытаться сгенерировать тот же GLN организации повторно и упадёт 400 duplicate.line.iln. Клиент должен либо передавать iln явно на второй и последующие запросы, либо сам управлять legal.

3.2 GET /organization/{gln}/locations — получение локаций

Особенность: фильтр передаётся в теле GET-запроса (JSON), это не query-параметры. Если тело пустое (Content-Length <= 0) — возвращаются вообще все локации организации.

Тело запроса (опционально):

Поле Тип Описание
id Long точный поиск по id локации (плюс опциональная сверка с iln, если передан)
iln string фильтр по GLN
name string фильтр по имени (подстрока)
address string фильтр по адресу (подстрока)

Приоритет: если передан id — возвращается (максимум) одна локация по id; иначе если задан хотя бы один из iln/name/address — поиск с фильтрацией; иначе — все локации организации.

Ответ (200): массив локаций (тот же формат, что в ответе POST).

3.3 DELETE /organization/{gln}/locations

Тело запроса (обязательно):

Поле Тип Обязательное
id Long да

Удаление только по внутреннему id локации; удаление по iln не реализовано (рассматривалось и было откачено в рамках этого тикета).

Ответ: 200 при успехе, 404 если локация с таким id не найдена в организации, 400 location.id.should.be.specified если id не передан.


4. Смена GLN организации

PATCH /organization/{organizationId}/gln?gln=<новый GLN>

Обратите внимание: здесь {organizationId} — числовой внутренний id организации (в отличие от эндпоинтов локаций выше, которые принимают GLN в пути).

Права:
  • SYSTEM_ADMIN / SYSTEM_SUPPORT — без ограничений.
  • PROVIDER_ADMIN — только если организация вызывающего = operatorId целевой организации.
  • Прочие — 403 not.have.access.
  • Организация не найдена по id → 404 (пустое тело).

Параметры:

Параметр Тип Обязательный
gln string да, query-параметр

Что происходит внутри (переиспользуется стандартный пайплайн апдейта организации, OrganizationUpdateEventOrganizationCommonUpdateHandler):

  1. Валидация формата GLN + контрольной цифры → 400 при ошибке.
  2. Проверка на дубликат — GLN не должен принадлежать другой организации → 400 Duplicate ILN.
  3. Старое значение сохраняется в previousIln.
  4. Если у организации есть "legal" LOCCAT-локация — её iln автоматически синхронизируется с новым GLN организации (loccatReconciliation). Если legal-локации нет — шаг тихо пропускается.
  5. Стандартные reconciliation-шаги апдейта организации (partner/condition/notifications и т.д., не специфичны для этой фичи).

Ответ (200): тело организации (OrganizationWithIdData) с уже применённым новым GLN.


5. Сводная таблица эндпоинтов

Метод Путь Идентификатор в пути Роли
POST /user/organization любой аутентифицированный
GET /organizations/search SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN
POST /organization/{gln}/locations GLN SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match)
GET /organization/{gln}/locations GLN SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match)
DELETE /organization/{gln}/locations GLN SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match)
PATCH /organization/{organizationId}/gln числовой id SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match)

6. Примеры

# Создание организации
curl --location --request POST 'https://api-test.docura.net/ap1/partner/api/user/organization' \
--header 'X-Authorization: <token>' \
--header 'Content-Type: application/json' \
--data '{
  "taxId": "EE100000001",
  "registrationNumber": "12345678",
  "name": "Example OU",
  "countryCode": "EE",
  "locale": "en" 
}'

# Поиск организаций
curl --location 'https://api-test.docura.net/ap1/partner/api/organizations/search?query=Example' \
--header 'X-Authorization: <token>'

# Создание локации без явного GLN (автогенерация)
curl --location --request POST 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>' \
--header 'Content-Type: application/json' \
--data '{
  "name": "Warehouse 1",
  "codeBySender": "WH1",
  "countryCode": "EE" 
}'

# Получение всех локаций организации
curl --location 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>'

# Удаление локации по id
curl --location --request DELETE 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>' \
--header 'Content-Type: application/json' \
--data '{"id": 682021}'

# Смена GLN организации
curl --location --request PATCH 'https://api-test.docura.net/ap1/partner/api/organization/<numeric_id>/gln?gln=2000005819995' \
--header 'X-Authorization: <token>'