PUT /api/v1/companies/my — Обновление своей компании
Операция изменения локальных (PIN-owned) полей компании: описание, slug, сайт, контакты, реквизиты
(для PIN-компаний), настройки workspace. Реплика-поля Talent (Name, LogoFileId, Requisites при
SyncSource = TalentIdentity) через эту операцию менять нельзя (INV-5 company.md).
Назначение
Руководитель агентства/менеджер застройщика с правом управления компанией редактирует карточку и
настройки workspace своей организации. Owning-контекст — identity (дом полей —
domain/company.md). Feature group — companies.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | companies |
| Статус | draft |
| Документ | docs/06-services/identity/api/company-user/update-my-company.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (пользователь компании) |
| Auth policy / Permissions | identity.companies.manage (domain/permission.md; дефолт: agency-owner, developer-manager, admin) |
| Scope (RBAC) | организация: Active RoleAssignment со scope Platform или Organization(CompanyId) |
| Record-level | не применяется — только собственная компания (CompanyId из claims) |
| Tenant isolation | TenantId = CompanyId из JWT; чужая компания недостижима (id в route не принимается) |
| Audit | update: actor-stamping UpdatedByUserId, инкремент Version, событие CompanyUpsertedKafkaEvent |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/companies/my |
| Success status | 200 |
| Idempotency header | не применяется — PUT семантически идемпотентен (full replace локальных полей) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims | Guid | Да | Required | — |
CompanyId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
Name | body | string? | Нет | 1..256 симв.; при SyncSource = TalentIdentity поле запрещено (INV-5) | — |
Slug | body | string | Да | pattern ^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$; уникальность INV-1 | — |
Description | body | string? | Нет | <= 2000 симв. | — |
LogoFileId | body | Guid? | Нет | при SyncSource = TalentIdentity запрещено (INV-5) | — |
Website | body | string? | Нет | <= 256, валидный absolute URL http/https | — |
ContactEmail | body | string? | Нет | <= 256, формат email | — |
ContactPhone | body | string? | Нет | <= 32, нормализация E.164 | — |
Requisites | body | CompanyRequisitesForm? | Нет | поля/pattern — как в VO CompanyRequisites (Inn ^\d{10}(\d{2})?$, Kpp ^\d{9}$, Ogrn ^\d{13}(\d{2})?$); при SyncSource = TalentIdentity запрещено (INV-5) | — |
WorkspaceSettings | body | CompanyWorkspaceSettingsForm | Да | typed-модель: DefaultRoleId? (Guid, роль этой компании), FixationVisibility (enum), AllowAgentSelfRegistration (bool), NewsCategoriesEnabled (список кодов varchar(64) ≤ 32 шт.), Locale (BCP-47, <= 8) | — |
Version | body | long | Да | >= 1; сверка с CompanyEntity.Version (optimistic) | — |
При SyncSource = TalentIdentity поля Name, LogoFileId, Requisites управляются синхронизацией из
Talent и через эту операцию не редактируются (INV-5, domain/company.md) —
попытка передать их отклоняется с IDENTITY_COMPANY_FIELD_SYNC_MANAGED. Остальные локальные поля (Slug,
Description, Website, ContactEmail, ContactPhone, WorkspaceSettings) редактируются независимо от
SyncSource.
Модель ответа UpdateMyCompanyCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | CompanyEntity.Id | — | Id компании. |
Version | long | Да | CompanyEntity.Version (после инкремента) | — | Новая доменная версия. |
UpdatedAt | DateTimeOffset | Да | CompanyEntity.UpdatedAt | — | Момент сохранения. |
Полная карточка после сохранения перечитывается клиентом через get-my-company.md.
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (
CompanyId); требуется правоidentity.companies.manageв scopeOrganization(CompanyId). Меняется только собственная компания (id в маршруте не принимается). - Проверка входа и предусловий.
- Синтаксис по таблице входных данных (форматы, длины, паттерны); иначе —
ValidationError. - Компания существует; иначе —
IDENTITY_COMPANY_NOT_FOUND. - Компания в статусе
Active;Pending/Suspended/Archived→IDENTITY_COMPANY_INVALID_STATUS_TRANSITION. - Sync-guard (INV-5): при
SyncSource == TalentIdentityполяName/LogoFileId/Requisitesменять нельзя →IDENTITY_COMPANY_FIELD_SYNC_MANAGED. - Уникальность slug (INV-1): при смене
Slugне должно быть другой компании с таким же slug →IDENTITY_COMPANY_SLUG_TAKEN. - Согласованность
WorkspaceSettings.DefaultRoleId: роль по умолчанию, если задана, — существующаяActive-роль этой компании; иначе —IDENTITY_ROLE_NOT_FOUND. - Оптимистичная блокировка: переданная
Versionсовпадает с версией компании; иначе —IDENTITY_CONCURRENCY_CONFLICT.
- Синтаксис по таблице входных данных (форматы, длины, паттерны); иначе —
- Что читаем. Компанию (для проверок выше) и роль по умолчанию из
WorkspaceSettings.DefaultRoleId. Других агрегатов нет. - Бизнес-правила и маппинг. Обновляются локальные (PIN-owned) поля:
Slug,Description,Website(нормализация URL),ContactEmail(нижний регистр),ContactPhone(E.164),WorkspaceSettings; приSyncSource = PinдополнительноName,LogoFileId,Requisites(набор мутируемых полей зависит отSyncSource, INV-5). Версия компании увеличивается, фиксируется автор изменения. Переходов статуса нет. Поднимается доменный фактCompanyUpdatedEventс изменёнными полями. - Что меняется. Обновляется компания. Конкурентный конфликт →
IDENTITY_CONCURRENCY_CONFLICT. - Какие факты/события фиксируются.
CompanyUpsertedKafkaEvent(топикpin.identity.companies.v1, ключId) — полный snapshotId/Type/Status/Name/Slug/LogoFileId/Requisites/Versionиз состояния после изменения. - Согласованность. Обновление компании и событие фиксируются в одном согласованном изменении.
- Результат. Идентификатор, новая версия и момент сохранения (
200). Полную карточку клиент перечитывает через get-my-company.md. Маскирование не требуется.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | CompanyUpsertedKafkaEvent → pin.identity.companies.v1, key = CompanyId; потребители: catalog, crm, rewards, news обновляют snapshot-реплики |
Событие несёт полный снапшот Id/Type/Status/Name/Slug/LogoFileId/Requisites/Version, партиционируется по
CompanyId (порядок обновлений одной компании сохраняется). Инициатор сразу видит обновлённую карточку при
перечитывании через get-my-company.md; реплики в
catalog/crm/rewards/news (название и логотип компании в карточках, ростерах, начислениях, ленте)
догоняют за секунды. До обновления реплик потребители показывают прежние значения; так как ключ
партиционирования — CompanyId, устаревший снапшот не может перезаписать более новый (по Version).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Нарушение format/min/max/pattern | Список ошибок полей | no retry |
IDENTITY_COMPANY_NOT_FOUND | 404 | Нет claim tenant_id / компания не найдена | «Компания не найдена» | no retry |
IDENTITY_COMPANY_FIELD_SYNC_MANAGED | 409 | Попытка сменить Name/LogoFileId/Requisites у Talent-реплики | «Поле управляется синхронизацией Talent» | no retry |
IDENTITY_COMPANY_SLUG_TAKEN | 409 | Slug занят другой компанией (INV-1) | «Адрес занят» | no retry |
IDENTITY_ROLE_NOT_FOUND | 400 | DefaultRoleId не найден / чужой / архивный | «Роль не найдена» | no retry |
IDENTITY_COMPANY_INVALID_STATUS_TRANSITION | 409 | Status != Active | «Компания недоступна для изменений» | no retry |
IDENTITY_CONCURRENCY_CONFLICT | 409 | Версия компании изменилась параллельно | «Данные изменены параллельно, обновите страницу» | retry после перечитывания |
FORBIDDEN | 403 | Нет identity.companies.manage | «Недостаточно прав» | no retry |
Acceptance Criteria
- Given пользователь с
identity.companies.manageв scope своейActive-компании (SyncSource = Pin) и актуальнойVersion, When PUT/companies/myс валидными локальными полями, Then поля обновлены,Versionинкрементирована, зафиксированUpdatedByUserId, уходитCompanyUpsertedKafkaEvent(pin.identity.companies.v1, key =CompanyId), ответ200 {Id, Version, UpdatedAt}. - Given
SyncSource = TalentIdentity, When запрос пытается изменитьName/LogoFileId/Requisites(INV-5), ThenIDENTITY_COMPANY_FIELD_SYNC_MANAGED(409); остальные локальные поля (slug, описание, контакты,WorkspaceSettings) при этом менять можно. - Given переданный
Slugуже занят другой компанией (INV-1), When update, ThenIDENTITY_COMPANY_SLUG_TAKEN(409). - Given
WorkspaceSettings.DefaultRoleIdне существует / чужой / архивный, When update, ThenIDENTITY_ROLE_NOT_FOUND(400). - Given
Versionв запросе не совпадает с текущей версией компании, When update, ThenIDENTITY_CONCURRENCY_CONFLICT(409) — клиент перечитывает и повторяет. - Given компания не
Active(Pending/Suspended/Archived), When update, ThenIDENTITY_COMPANY_INVALID_STATUS_TRANSITION(409). - Given нарушены форматы/длины/паттерны полей, When update, Then
ValidationError(400); Given нет права, ThenFORBIDDEN(403); id чужой компании в маршруте не принимается.
Совместимость и наблюдаемость
- Новые optional поля формы — additive; изменение семантики
WorkspaceSettings— новая версия jsonb-документа (+1) с миграцией чтения (company.md). - Logs:
CompanyId,UserId, изменённые поля (без значений реквизитов); metrics:identity_company_update_total{result}; traces: OTel, сквознойtraceparent; dashboardidentity-overview; runbook: конфликт синка — domain/talent-replica.md.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): GET /api/v1/companies/my — Просмотр своей компании, Identity Company-User API — реестр методов и общие правила области
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: domain/company.md (INV-1, INV-5, jsonb-документ).
- Чтение: get-my-company.md; события:
pin.identity.companies.v1(company.md «События»).