diff --git a/proto/customer.thrift b/proto/customer.thrift index 70b5314f..ee66aea1 100644 --- a/proto/customer.thrift +++ b/proto/customer.thrift @@ -74,6 +74,56 @@ struct ProviderTerminalKey { 2: required domain.TerminalRef terminal_ref } +/* Terminal affinity — привязка плательщика к терминалу */ + +/** + * Привязка Customer к терминалу. + * + * У Customer может быть несколько активных привязок. При выборе роута среди + * доступных кандидатов предпочитается привязка, созданная раньше + * (с меньшим bind_seq). См. domain.RoutingAffinity. + */ +struct TerminalAffinity { + 1: required domain.ProviderRef provider_ref + 2: required domain.TerminalRef terminal_ref + /** + * Порядковый номер привязки, растёт монотонно. Чем меньше, тем раньше + * создана привязка и тем выше её приоритет. + */ + 3: required i64 bind_seq + /** Время создания привязки; точка отсчёта для TTL since_bound */ + 4: required base.Timestamp bound_at + /** + * Время последнего успешного платежа через привязку; + * точка отсчёта для TTL since_last_use. + */ + 5: required base.Timestamp last_used_at +} + +/** + * Параметры привязки Customer к терминалу. + * Операция идемпотентна: повторный вызов не меняет bind_seq, только last_used_at. + */ +struct TerminalAffinityParams { + 1: required CustomerID customer_id + 2: required domain.ProviderRef provider_ref + 3: required domain.TerminalRef terminal_ref + /** + * Если задан и существующая привязка к этому терминалу по нему уже истекла, + * она снимается, а вместо неё создаётся новая с очередным bind_seq. + */ + 4: optional domain.RoutingAffinityTtl ttl +} + +/** + * Параметры снятия привязки. + */ +struct ReleaseTerminalAffinityParams { + 1: required CustomerID customer_id + 2: required ProviderTerminalKey key + 3: optional string reason +} + /** * BankCard — самостоятельная сущность банковской карты. */ @@ -141,6 +191,13 @@ struct Customer { 6: optional domain.Metadata metadata /** Внешний идентификатор Customer */ 7: optional string external_id + /** + * Email плательщика в нормализованном виде (нижний регистр, без пробелов + * по краям). Уникален в рамках party_ref. Служит идентификатором плательщика + * для привязки к терминалу; не путать с external_id — идентификатором, + * который назначает мерчант. + */ + 8: optional string email } /** @@ -182,6 +239,8 @@ struct CustomerParams { 3: optional domain.Metadata metadata /** Внешний идентификатор Customer */ 4: optional string external_id + /** Email плательщика; нормализуется сервисом. Уникален в рамках party_ref */ + 5: optional string email } /** @@ -243,6 +302,11 @@ exception CustomerAlreadyExists { 1: required CustomerID id } +/** Email уже занят другим Customer этой party */ +exception CustomerEmailConflict { + 1: required CustomerID id +} + exception BankCardNotFound {} exception InvalidRecurrentParent { @@ -280,6 +344,7 @@ service CustomerManagement { throws ( 1: CustomerAlreadyExists already_exists 2: base.InvalidRequest invalid_request + 3: CustomerEmailConflict email_conflict ) /** @@ -374,6 +439,60 @@ service CustomerManagement { 3: optional ContinuationToken continuation_token ) throws (1: CustomerNotFound not_found) + + /** + * Найти Customer по email в рамках party или создать, если его нет. + * Операция идемпотентна и безопасна при конкурентных вызовах: единственность + * Customer гарантирует уникальность email в рамках party. Email нормализуется + * сервисом. CustomerEmailConflict не бросает. + */ + Customer FindOrCreateByEmail( + 1: domain.PartyConfigRef party_ref, + 2: string email + ) + throws (1: base.InvalidRequest invalid_request) + + /** + * Получить Customer по email и party. + */ + CustomerState GetByEmail( + 1: domain.PartyConfigRef party_ref, + 2: string email + ) + throws (1: CustomerNotFound not_found) + + /** + * Получить активные привязки Customer к терминалам. + * Истечение по TTL здесь не проверяется: TTL задаётся в настройках кандидата + * роутинга, и учитывать его должна вызывающая сторона. + */ + list GetTerminalAffinities(1: CustomerID customer_id) + throws (1: CustomerNotFound not_found) + + /** + * Привязать Customer к терминалу. + * Идемпотентно: повторный вызов не меняет bind_seq, только last_used_at. + */ + TerminalAffinity BindTerminalAffinity(1: TerminalAffinityParams params) + throws ( + 1: CustomerNotFound not_found + 2: base.InvalidRequest invalid_request + ) + + /** + * Снять привязку Customer к терминалу. + */ + void ReleaseTerminalAffinity(1: ReleaseTerminalAffinityParams params) + throws (1: CustomerNotFound not_found) + + /** + * Снять привязки всех Customer к терминалу. + * Административная операция, например при выводе терминала из эксплуатации. + */ + void ReleaseTerminalAffinitiesByTerminal( + 1: ProviderTerminalKey key, + 2: optional string reason + ) } /** diff --git a/proto/domain.thrift b/proto/domain.thrift index 2a8d9eda..1df7032a 100644 --- a/proto/domain.thrift +++ b/proto/domain.thrift @@ -589,9 +589,20 @@ struct PaymentRoute { 2: required TerminalRef terminal } +/** + * Скоры роута. Сравниваются как кортеж в порядке объявления полей, поэтому + * чем выше поле объявлено, тем значимее критерий. + */ struct PaymentRouteScores { 1: optional i32 availability_condition 2: optional i32 conversion_condition + /** + * Ранг привязки плательщика к терминалу (см. RoutingAffinity). + * 0 — привязки нет; чем больше значение, тем раньше была создана привязка. + * Поле намеренно объявлено выше приоритета и веса: привязка важнее + * настроек роутинга, но уступает критическим состояниям по fault detector. + */ + 9: optional i32 terminal_affinity 3: optional i32 terminal_priority_rating 4: optional i32 route_pin 5: optional i32 random_condition @@ -2586,6 +2597,46 @@ struct RoutingPin { 1: required set features } +/** + * Привязка плательщика к терминалу. + * + * Плательщик идентифицируется по email через Customer. Терминал, через который + * прошёл первый успешный платёж, запоминается, и в последующих платежах имеет + * преимущество перед приоритетом и весом кандидатов, пока доступен. + * Настройка действует только на кандидатов, у которых она задана: остальные + * не участвуют в привязке ни при выборе роута, ни при её создании. + */ +struct RoutingAffinity { + /** Время жизни привязки; если не задано — бессрочно. */ + 1: optional RoutingAffinityTtl ttl +} + +/** + * Время жизни привязки: каждый вариант задаёт момент истечения. + * Привязка считается истёкшей, как только этот момент наступил. + */ +union RoutingAffinityTtl { + /** + * Абсолютная отсечка: все привязки кандидата истекают в этот момент, + * независимо от того, когда были созданы и когда использовались. + * Например, для плановой переразметки плательщиков по актуальным весам + * начиная с заданной даты. + */ + 1: base.Timestamp deadline + /** + * Срок с момента создания привязки: истекает через since_bound секунд + * после bound_at, независимо от того, платит ли плательщик. + * Подходит для периодической переразметки по актуальным весам. + */ + 2: base.Timeout since_bound + /** + * Скользящий срок: истекает, если через привязку не было успешных платежей + * дольше since_last_use секунд (считая от last_used_at). Каждый успешный + * платёж продлевает срок. + */ + 3: base.Timeout since_last_use +} + struct RoutingCandidate { 1: optional string description 2: required Predicate allowed @@ -2593,6 +2644,7 @@ struct RoutingCandidate { 4: optional i32 priority = CANDIDATE_PRIORITY 5: optional RoutingPin pin 6: optional i32 weight = CANDIDATE_WEIGHT + 7: optional RoutingAffinity affinity } /* Root config */ diff --git a/proto/payment_processing.thrift b/proto/payment_processing.thrift index a03f0bf1..12a3c130 100644 --- a/proto/payment_processing.thrift +++ b/proto/payment_processing.thrift @@ -150,6 +150,7 @@ union InvoicePaymentChangePayload { 18: InvoicePaymentShopLimitApplied invoice_payment_shop_limit_applied 19: InvoicePaymentCascadeTokensLoaded invoice_payment_cascade_tokens_loaded 20: InvoicePaymentExchangeContextChanged invoice_payment_exchange_context_changed + 21: InvoicePaymentTerminalAffinitiesLoaded invoice_payment_terminal_affinities_loaded } /** @@ -488,6 +489,15 @@ struct InvoicePaymentCascadeTokensLoaded { 1: required list tokens } +/** + * Событие о загрузке привязок плательщика к терминалам из хранилища. + * Содержит только действующие (не истёкшие по TTL) привязки. Пустой список — + * тоже событие: оно фиксирует, что загрузка для этого платежа уже выполнена. + */ +struct InvoicePaymentTerminalAffinitiesLoaded { + 1: required list affinities +} + struct InvoicePaymentCaptureStarted { 1: required InvoicePaymentCaptureData data } @@ -900,6 +910,11 @@ typedef map> RouteLimitContext struct RouteDecisionContext { 1: optional bool skip_recurrent + /** + * У выбранного кандидата задана настройка привязки к терминалу + * (domain.RoutingAffinity): при успешном платеже привязка будет создана. + */ + 2: optional bool terminal_affinity } // Exceptions