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

GET /api/v1/my/payouts — Мои выплаты (агент)

Назначение

Отдаёт агенту постраничную историю его выплат: что уже выплачено, что передано в бухгалтерию и ожидает выплаты, а что было отклонено. Показываются позиции агента в платёжных поручениях его агентства (PayoutOrderItemEntity) — с суммой, числом распределений и датой выплаты — плюс статус, номер и период родительского поручения (PayoutOrderEntity). Это record-level срез: позиции отбираются по AgentUserId = актор (индекс ix_payout_order_items_agent). Дом полей и статусная модель — domain/payout-order.md; базовые проекции PayoutOrderItemModel / PayoutOrderModelREADME.md. Feature group — my-commission.

Metadata

ПолеЗначение
Сервис/контекстrewards
API areaagent
Feature groupmy-commission
Статусdraft
Документdocs/06-services/rewards/api/agent/list-my-payouts.md

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

ПроверкаЗначение
Actor typeUser (агент)
Auth policy / Permissionsrewards.payouts.view-own (op30)
Scope (RBAC)собственные записи в рамках своего агентства (компания Type ∈ {Agency, Broker, Pin})
Record-levelPayoutOrderItemEntity.AgentUserId = UserId актора из токена, не из query
Tenant isolationTenantId = CompanyId агентства из токена; вместе с AgentUserId ведут в ix_payout_order_items_agent (tenant_id, agent_user_id)
Auditread-sensitive (суммы выплат): в trace — actor + фильтры + TotalCount; суммы не логируются

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/my/payouts
Success status200
Idempotency headerнет (безопасное чтение)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
AgentUserIdтокен (sub)GuidДаRequired; не принимается из queryrecord-level фильтр
TenantIdтокен (tenant_id)GuidДаRequired; не принимается из queryTenant isolation
StatusqueryPayoutOrderStatus?Нетподмножество видимых агенту: PendingPayment/Paid/Rejected; Draft/CancelledValidationError
PeriodFromqueryDateTimeOffset?Нетнижняя граница расчётного периода поручения: PayoutOrder.PeriodFrom >= PeriodFrom
PeriodToqueryDateTimeOffset?Нетверхняя граница: PayoutOrder.PeriodTo <= PeriodTo; > PeriodFrom, если обе заданы
Page / PageSizequeryintНетpaging-конвенция — README.md (Page >= 1, PageSize 1..100)

Модель ответа — страница MyPayoutItemModel

Ответ — страница элементов MyPayoutItemModel с полями Items, Page, PageSize, TotalCount (paging-конвенция — README.md).

MyPayoutItemModel — плоская проекция «позиция + её поручение», определяется как дельта: PayoutOrderItemModel (README.md) минус AgentUserId (всегда = актор, избыточно) плюс подмножество полей PayoutOrderModel (PayoutOrderId, Number, Status, PeriodFrom, PeriodTo).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаPayoutOrderItemEntity.IdИдентификатор позиции (строка «моей выплаты»).
PayoutOrderIdGuidДаPayoutOrderItemEntity.PayoutOrderIdРодительское поручение.
NumberstringДаPayoutOrderEntity.Number (PO-{yyyy}-{seq})Человекочитаемый номер поручения.
Statusstring (enum PayoutOrderStatus)ДаPayoutOrderEntity.StatusPendingPayment/Paid/Rejected (маппинг 5.5: ожидается/выплачено/отклонено).
PeriodFromDateTimeOffsetДаPayoutOrderEntity.PeriodFromНачало расчётного периода поручения.
PeriodToDateTimeOffsetДаPayoutOrderEntity.PeriodToКонец периода (исключительно).
AmountMoneyModelДаPayoutOrderItemEntity.Amountsensitive (не логируется)Сумма выплаты агенту по позиции.
DistributionsCountintДаPayoutOrderItemEntity.DistributionsCountЧисло строк распределения, вошедших в позицию.
PaidAtDateTimeOffset?НетPayoutOrderItemEntity.PaidAtМомент выплаты; null пока поручение не Paid.

Коллизия PaidAt. У позиции и у поручения поле PaidAt совпадает по значению (INV-7 domain/payout-order.md: MarkPaid проставляет PaidAt поручению, позициям и строкам распределения в одной транзакции), поэтому в проекции — единственное PaidAt из позиции. InvestorUserId в модели отсутствует (Masking-правила README.md); полный документ поручения и чужие позиции агенту не отдаются.

Алгоритм

  1. Контекст и доступ. Актор — агент; tenant — его агентство (Type ∈ {Agency, Broker, Pin}); требуется право rewards.payouts.view-own. Агент видит только свои позиции: AgentUserId берётся из токена, не из запроса. Тип компании актора вне {Agency, Broker, Pin}FORBIDDEN.
  2. Проверка входа. Status, если задан, ∈ видимого агенту подмножества {PendingPayment, Paid, Rejected} (значения Draft/Cancelled невидимы агенту → ValidationError); PeriodTo > PeriodFrom, если обе заданы; Page/PageSize в границах конвенции — иначе ValidationError. Входных id, требующих существования, нет.
  3. Что читаем. Страница позиций агента, связанных с их родительскими поручениями. Отбор: позиция принадлежит агенту (AgentUserId = актор) и его агентству (TenantId), поручение в видимом агенту статусе {PendingPayment, Paid, Rejected} (черновики и отменённые не показываются); дополнительно — заданный Status и границы расчётного периода поручения (PeriodFrom/PeriodTo). Сортировка: сначала по дате выплаты (PaidAt, невыплаченные — в конце), затем по концу периода — свежие выплаты сверху. Опорный индекс — ix_payout_order_items_agent (tenant_id, agent_user_id).
  4. Бизнес-правила и маппинг. Проекция «позиция + её поручение» в MyPayoutItemModel поле-в-поле (дом README.md): Id, PayoutOrderId, Amount, DistributionsCount, PaidAt — из позиции; Number, Status (enum строкой), PeriodFrom, PeriodTo — из поручения; AgentUserId не проецируется (всегда актор). Переходов состояний нет.
  5. Результат. Страница MyPayoutItemModel (Items, Page, PageSize, TotalCount) → HTTP 200; суммы маскируются в логах (в теле — отдаются владельцу-агенту).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Status вне видимого агенту подмножества, PeriodTo <= PeriodFrom, PageSize вне 1..100«Проверьте параметры фильтра»no retry
FORBIDDEN403Нет rewards.payouts.view-own / актор не в агентстве«Недостаточно прав»no retry
UNAUTHORIZED401Нет/просрочен JWT«Требуется авторизация»no retry

Специфичных REWARDS_*-кодов у read-операции нет (мутаций/переходов нет); коды выше — общие, дом README.md «Общие ошибки». Record-level изоляция реализуется фильтром, а не отдельной ошибкой: чужие позиции просто не попадают в выборку (fail-closed).

Предусловия и постусловия

  • Предусловия. Агент авторизован, состоит в агентстве (Type ∈ {Agency, Broker, Pin}) и имеет право rewards.payouts.view-own. Позиции выплат (PayoutOrderItemEntity) появляются, когда руководитель/ бухгалтер включает Agent-строки распределения в платёжное поручение (create-payout-order.md / update-payout-order-items.md); агенту видны только поручения в статусах PendingPayment/Paid/Rejected (черновики и отменённые скрыты).
  • Постусловия. Только чтение — состояние не меняется, событий нет. Возвращена страница MyPayoutItemModel, отобранная по AgentUserId = актор и видимым статусам, отсортированная по PaidAt (невыплаченные — в конце), затем по концу периода. PaidAt непуст только у позиций выплаченных поручений (INV-7 каскад). InvestorUserId и чужие позиции в ответ не попадают (masking / record-level).

Acceptance-критерии

  • Happy path. Дано: агент с правом rewards.payouts.view-own, позиции в поручениях PendingPayment/Paid/Rejected. Когда: GET /api/v1/my/payouts. Тогда: 200, страница MyPayoutItemModel с суммой позиции, числом распределений, номером/статусом/периодом поручения; выплаченные (Paid) — сверху с непустым PaidAt.
  • Фильтр по видимому статусу. Дано: Status = Paid. Когда: GET. Тогда: 200, только позиции выплаченных поручений.
  • Скрытый статус. Дано: Status = Draft (или Cancelled). Когда: GET. Тогда: 400 ValidationError — эти статусы агенту не видны (подмножество {PendingPayment, Paid, Rejected}).
  • Фильтр периода. Дано: заданы PeriodFrom/PeriodTo расчётного периода поручения. Когда: GET. Тогда: 200, только поручения с PeriodFrom >= PeriodFrom и PeriodTo <= PeriodTo.
  • Некорректный период. Дано: PeriodTo <= PeriodFrom. Когда: GET. Тогда: 400 ValidationError.
  • Пустая выборка. Дано: у агента ещё нет ни одной позиции в видимых поручениях. Когда: GET. Тогда: 200, Items = [], TotalCount = 0не 404.
  • Только свои позиции. Дано: агент пытается передать AgentUserId в query. Когда: GET. Тогда: параметр игнорируется — ключ из токена; чужие позиции не попадают в выборку (fail-closed record-level).
  • Нет права / не в агентстве. Дано: нет rewards.payouts.view-own либо тип компании актора вне {Agency, Broker, Pin}. Когда: GET. Тогда: 403 FORBIDDEN.

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

  • MyPayoutItemModel — дельта-проекция от PayoutOrderItemModel + PayoutOrderModel (README.md): новые optional поля additive; удаление/переименование/смена типа — breaking (migration + rollout). Новые значения PayoutOrderStatus — по unknown-enum-правилу cross-cutting (агенту остаётся видимо подмножество {PendingPayment, Paid, Rejected}).
  • Logs: AgentUserId, TenantId, фильтры (Status, границы периода), TotalCount (суммы выплат не логируются); metrics: rewards_api_request_duration_seconds{route}; traces: сквозной trace через traceparent; dashboard rewards-overview.

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

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

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