Перейти к основному содержимому

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 areapublic
Feature groupcompanies
Статусdraft
Документdocs/06-services/identity/api/company-user/update-my-company.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.companies.manage (domain/permission.md; дефолт: agency-owner, developer-manager, admin)
Scope (RBAC)организация: Active RoleAssignment со scope Platform или Organization(CompanyId)
Record-levelне применяется — только собственная компания (CompanyId из claims)
Tenant isolationTenantId = CompanyId из JWT; чужая компания недостижима (id в route не принимается)
Auditupdate: actor-stamping UpdatedByUserId, инкремент Version, событие CompanyUpsertedKafkaEvent

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/companies/my
Success status200
Idempotency headerне применяется — PUT семантически идемпотентен (full replace локальных полей)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
Namebodystring?Нет1..256 симв.; при SyncSource = TalentIdentity поле запрещено (INV-5)
SlugbodystringДаpattern ^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$; уникальность INV-1
Descriptionbodystring?Нет<= 2000 симв.
LogoFileIdbodyGuid?Нетпри SyncSource = TalentIdentity запрещено (INV-5)
Websitebodystring?Нет<= 256, валидный absolute URL http/https
ContactEmailbodystring?Нет<= 256, формат email
ContactPhonebodystring?Нет<= 32, нормализация E.164
RequisitesbodyCompanyRequisitesForm?Нетполя/pattern — как в VO CompanyRequisites (Inn ^\d{10}(\d{2})?$, Kpp ^\d{9}$, Ogrn ^\d{13}(\d{2})?$); при SyncSource = TalentIdentity запрещено (INV-5)
WorkspaceSettingsbodyCompanyWorkspaceSettingsFormДаtyped-модель: DefaultRoleId? (Guid, роль этой компании), FixationVisibility (enum), AllowAgentSelfRegistration (bool), NewsCategoriesEnabled (список кодов varchar(64) ≤ 32 шт.), Locale (BCP-47, <= 8)
VersionbodylongДа>= 1; сверка с CompanyEntity.Version (optimistic)
SyncSource-guard: реплика-поля Talent

При 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Описание
IdGuidДаCompanyEntity.IdId компании.
VersionlongДаCompanyEntity.Version (после инкремента)Новая доменная версия.
UpdatedAtDateTimeOffsetДаCompanyEntity.UpdatedAtМомент сохранения.

Полная карточка после сохранения перечитывается клиентом через get-my-company.md.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.companies.manage в scope Organization(CompanyId). Меняется только собственная компания (id в маршруте не принимается).
  2. Проверка входа и предусловий.
    • Синтаксис по таблице входных данных (форматы, длины, паттерны); иначе — ValidationError.
    • Компания существует; иначе — IDENTITY_COMPANY_NOT_FOUND.
    • Компания в статусе Active; Pending/Suspended/ArchivedIDENTITY_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.
  3. Что читаем. Компанию (для проверок выше) и роль по умолчанию из WorkspaceSettings.DefaultRoleId. Других агрегатов нет.
  4. Бизнес-правила и маппинг. Обновляются локальные (PIN-owned) поля: Slug, Description, Website (нормализация URL), ContactEmail (нижний регистр), ContactPhone (E.164), WorkspaceSettings; при SyncSource = Pin дополнительно Name, LogoFileId, Requisites (набор мутируемых полей зависит от SyncSource, INV-5). Версия компании увеличивается, фиксируется автор изменения. Переходов статуса нет. Поднимается доменный факт CompanyUpdatedEvent с изменёнными полями.
  5. Что меняется. Обновляется компания. Конкурентный конфликт → IDENTITY_CONCURRENCY_CONFLICT.
  6. Какие факты/события фиксируются. CompanyUpsertedKafkaEvent (топик pin.identity.companies.v1, ключ Id) — полный snapshot Id/Type/Status/Name/Slug/LogoFileId/Requisites/Version из состояния после изменения.
  7. Согласованность. Обновление компании и событие фиксируются в одном согласованном изменении.
  8. Результат. Идентификатор, новая версия и момент сохранения (200). Полную карточку клиент перечитывает через get-my-company.md. Маскирование не требуется.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияCompanyUpsertedKafkaEventpin.identity.companies.v1, key = CompanyId; потребители: catalog, crm, rewards, news обновляют snapshot-реплики
Окно eventual-consistency

Событие несёт полный снапшот Id/Type/Status/Name/Slug/LogoFileId/Requisites/Version, партиционируется по CompanyId (порядок обновлений одной компании сохраняется). Инициатор сразу видит обновлённую карточку при перечитывании через get-my-company.md; реплики в catalog/crm/rewards/news (название и логотип компании в карточках, ростерах, начислениях, ленте) догоняют за секунды. До обновления реплик потребители показывают прежние значения; так как ключ партиционирования — CompanyId, устаревший снапшот не может перезаписать более новый (по Version).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нарушение format/min/max/patternСписок ошибок полейno retry
IDENTITY_COMPANY_NOT_FOUND404Нет claim tenant_id / компания не найдена«Компания не найдена»no retry
IDENTITY_COMPANY_FIELD_SYNC_MANAGED409Попытка сменить Name/LogoFileId/Requisites у Talent-реплики«Поле управляется синхронизацией Talent»no retry
IDENTITY_COMPANY_SLUG_TAKEN409Slug занят другой компанией (INV-1)«Адрес занят»no retry
IDENTITY_ROLE_NOT_FOUND400DefaultRoleId не найден / чужой / архивный«Роль не найдена»no retry
IDENTITY_COMPANY_INVALID_STATUS_TRANSITION409Status != Active«Компания недоступна для изменений»no retry
IDENTITY_CONCURRENCY_CONFLICT409Версия компании изменилась параллельно«Данные изменены параллельно, обновите страницу»retry после перечитывания
FORBIDDEN403Нет 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), Then IDENTITY_COMPANY_FIELD_SYNC_MANAGED (409); остальные локальные поля (slug, описание, контакты, WorkspaceSettings) при этом менять можно.
  • Given переданный Slug уже занят другой компанией (INV-1), When update, Then IDENTITY_COMPANY_SLUG_TAKEN (409).
  • Given WorkspaceSettings.DefaultRoleId не существует / чужой / архивный, When update, Then IDENTITY_ROLE_NOT_FOUND (400).
  • Given Version в запросе не совпадает с текущей версией компании, When update, Then IDENTITY_CONCURRENCY_CONFLICT (409) — клиент перечитывает и повторяет.
  • Given компания не Active (Pending/Suspended/Archived), When update, Then IDENTITY_COMPANY_INVALID_STATUS_TRANSITION (409).
  • Given нарушены форматы/длины/паттерны полей, When update, Then ValidationError (400); Given нет права, Then FORBIDDEN (403); id чужой компании в маршруте не принимается.

Совместимость и наблюдаемость

  • Новые optional поля формы — additive; изменение семантики WorkspaceSettings — новая версия jsonb-документа (+1) с миграцией чтения (company.md).
  • Logs: CompanyId, UserId, изменённые поля (без значений реквизитов); metrics: identity_company_update_total{result}; traces: OTel, сквозной traceparent; dashboard identity-overview; runbook: конфликт синка — domain/talent-replica.md.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Сущность: domain/company.md (INV-1, INV-5, jsonb-документ).
  • Чтение: get-my-company.md; события: pin.identity.companies.v1 (company.md «События»).