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 симв. |
| 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 текущей организации сессии), без явных параметров в запросе:
- Если организация вызывающего — сама softera (
profile.softeraOrganizationIln) → оператор новой организации = сама softera. Дополнительно в этом случае вызывающему в новую организацию какCLIENT_ADMINдобавляется дефолтный пользователь softera (profile.softeraDefaultUserId, если задан). - Если организация вызывающего — провайдер (
role='P', но не softera) → оператор = сама организация вызывающего. - Иначе (не softera и не провайдер) → оператор = организация из
profile.defaultOperatorOrganizationIln(fallback на "оператора по умолчанию"). - Если ни организация вызывающего, ни 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 локации генерируется автоматически (см. ниже).
Тело запроса (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 симв. |
| string | нет | до 350 симв., формат email | |
| web | string | нет | до 175 симв. |
Ответ (200): то же тело + id созданной/обновлённой локации.
Автогенерация GLN локации (когда iln не передан)¶
Переиспользует легаси-алгоритм UserWebController.getNextLocalizationCatalogGln (core):
- Если у организации уже есть локации (
maxIlnнайден) → следующий GLN = тело последнего сгенерированного GLN + 1. - Если локаций нет и организация тестовая (
isTest) → GLN строится изglnPrefixорганизации, дополненного нулями. - Если локаций нет, организация не тестовая и нет "legal"-локации → возвращается GLN самой организации.
- Если есть 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-параметр |
Что происходит внутри (переиспользуется стандартный пайплайн апдейта организации, OrganizationUpdateEvent → OrganizationCommonUpdateHandler):
- Валидация формата GLN + контрольной цифры →
400при ошибке. - Проверка на дубликат — GLN не должен принадлежать другой организации →
400 Duplicate ILN. - Старое значение сохраняется в
previousIln. - Если у организации есть "legal" LOCCAT-локация — её
ilnавтоматически синхронизируется с новым GLN организации (loccatReconciliation). Если legal-локации нет — шаг тихо пропускается. - Стандартные 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>'