Project

General

Profile

Партнерский API (#19245)

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

Общие положения

  1. Базовый URL: https://api-test.docura.net/ap1/partner/api/
  2. Аутентификация: заголовок X-Authorization: <api_key> (токен пользователя, поле api_key в таблице users).
  3. Заголовок X-App-Key не используется клиентами — на бэкенде эта проверка в настоящее время не подключена ни к одной цепочке фильтров (мертвый код с 2023 года), поэтому его можно опустить.
  4. Формат — JSON (если явно не указано иное).
  5. Роли (полномочия): 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 организации).

Ошибки:

  1. 409 Conflict (organization.create.conflict.tax_id) — организация с таким taxId уже существует.
  2. 400 — недопустимые поля тела запроса (валидация бинов) или локаль не найдена (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 для новой организации

  1. Вызывающий — softera → всегда Payment (жестко задано, независимо от чего-либо).
  2. В противном случае → connectionType = connectionType организации-оператора (рассчитанное выше — поставщик или резервная организация). Клиент не может явно указать connectionType/профиль интеграции.

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

GET /organizations/search?query=<string>

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

Параметры:

Параметр Тип Обязательный Описание
query string да Строка поиска. Пустая/отсутствующая строка → пустой список без ошибки.

Логика поиска в БД — одиночное OR по организации:

  1. точное совпадение по id (как текст);
  2. точное совпадение по iln (GLN);
  3. точное совпадение по regNr (без учета регистра);
  4. точное совпадение по vatNr (без учета регистра);
  5. подстрока (ILIKE) в name организации;
  6. подстрока (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/name, а не по имени локации, locations будет пустым массивом. Это преднамеренное поведение, а не ошибка.


3. Адреса LOCCAT организации

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

Права (общие для всех трёх методов ниже):

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

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

Логика Upsert:

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

Тело запроса (LoccatLocationWithIdData — LoccatLocationData + необязательный 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 (ядро):

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

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

Известные ограничения (принятые как есть, без дополнительной защиты):

  1. Отсутствует транзакционная блокировка между «получением следующего GLN» и «созданием местоположения с ним» — при параллельных запросах без iln возможна гонка и 400 duplicate.line.iln.
  2. Увеличение при переполнении (тело GLN = 999999999999) теоретически может привести к GLN на один символ длиннее обычного — крайне маловероятный крайний случай, исправление не запрашивалось.
  3. Флаг legal НЕ устанавливается автоматически при создании первого местоположения без iln — если у организации нет юридического местоположения, повторный POST без iln попытается снова сгенерировать тот же GLN организации и завершится ошибкой 400 duplicate.line.iln. Клиент должен либо явно передавать iln во втором и последующих запросах, либо управлять legal самостоятельно.

3.2 GET /organization/{gln}/locations — получить локации

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

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

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

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

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

3.3 DELETE /organization/{gln}/locations

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

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

Удаление только по внутреннему идентификатору местоположения; удаление по iln не реализовано (было рассмотрено и отменено в рамках данного тикета).

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


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

PATCH /organization/{organizationId}/gln?gln=<new GLN>

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

Права:

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

Параметры:

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

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

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

Ответ (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)
GET /organization/{gln}/locations GLN SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (соответствие operatorId)
DELETE /organization/{gln}/locations GLN SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (соответствие operatorId)
PATCH /organization/{organizationId}/gln числовой идентификатор SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (соответствие operatorId)

6. Примеры

# Create an organisation
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" 
}'

# Search organisations
curl --location 'https://api-test.docura.net/ap1/partner/api/organizations/search?query=Example' \
--header 'X-Authorization: <token>'

# Create a location without an explicit GLN (auto-generation)
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" 
}'

# Get all locations of an organisation
curl --location 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>'

# Delete a location by 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}'

# Change an organisation's GLN
curl --location --request PATCH 'https://api-test.docura.net/ap1/partner/api/organization/<numeric_id>/gln?gln=2000005819995' \
--header 'X-Authorization: <token>'

Source version: 3 · Status: fresh · Roman Startsev