road-runner-common 0.16.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
//! Canonical Novu workflow trigger identifiers — the single Rust-side source of truth.
//!
//! A value here must match the workflow's trigger identifier in the notification center
//! **character for character**; an unknown identifier yields HTTP 422
//! `workflow_not_found`. The Java sender (`cex-keycloak`) and the notification service
//! (`cex-notification`) keep mirrored constants; all three must agree.
//!
//! Two naming styles are in use because the notification center holds both: the
//! security/identity workflows were created as kebab-case slugs, the newer
//! transactional ones as `Pascal_Snake_Case`. Neither is "the convention" — copy the
//! identifier from the notification center rather than deriving it from the name, and
//! add the constant here so no service ever spells it out inline.

// --- Identity & security -------------------------------------------------------

/// Sensitive-action confirmation OTP (withdraw / api-key / 2FA-method change).
pub const ACTION_CONFIRMATION: &str = "action-confirmation";
/// Security alert when the account's second-factor method changes.
pub const SECURITY_TWO_FACTOR_CHANGED: &str = "Security_Two_Factor_Changed";
/// Two-factor authentication was turned on.
pub const TWO_FACTOR_ENABLED: &str = "2FA_Enabled";
/// Two-factor authentication was turned off, or a reset was requested.
///
/// The two are one message on purpose: from the account owner's side both mean "the second
/// factor protecting this account is no longer what it was", and that is the sentence
/// somebody who did not do it needs to read.
pub const TWO_FACTOR_DISABLED_RESET: &str = "2FA_Disabled_Reset";
/// A sign-in from a device this member has not used before.
pub const NEW_DEVICE_LOGIN: &str = "New_Device_Login";
/// A sign-in from a country the member has not signed in from before, or one that could
/// not plausibly follow their previous sign-in.
pub const NEW_IP_LOCATION_LOGIN: &str = "New_IP_Location_Login";
/// Pre-registration email verification OTP.
pub const REGISTER_EMAIL_VERIFICATION: &str = "register-email-verification";
/// Pre-registration phone verification OTP.
pub const REGISTER_PHONE_VERIFICATION: &str = "register-phone-verification";

// --- Identity verification (KYC) --------------------------------------------------
//
// Driven by the case log in cex-compliance: one workflow per fact the log can record, so
// which message a member gets is decided by the same event that moved their case.
//
//   application received → under review → approved
//                                       ↘ additional document requested (resubmit)
//                                       ↘ rejected (no retry path)

/// The member entered verification; the case now exists.
pub const KYC_APPLICATION_RECEIVED: &str = "KYC_Application_Received";
/// The case is with the provider or a compliance officer, awaiting a verdict.
pub const KYC_UNDER_REVIEW: &str = "KYC_Under_Review";
/// Verification succeeded.
pub const KYC_APPROVED: &str = "KYC_Approved";
/// Rejected with **no** retry path — the member cannot fix this by resubmitting.
pub const KYC_REJECTED: &str = "KYC_Rejected";
/// A nudge to a member who started verification and stopped, sent once per member and
/// track. Only for states where the next move is theirs — never for a case waiting on us.
pub const KYC_REMINDER: &str = "KYC_Reminder";
/// Rejected **but retryable**: the provider asked for a document again (unreadable scan,
/// bad selfie, missing proof of address). A distinct message from [`KYC_REJECTED`]
/// because the member is being asked to act, not told the answer is no.
pub const KYC_ADDITIONAL_DOCUMENT_REQUESTED: &str = "KYC_Additional_Document_Requested";

// --- Account standing ------------------------------------------------------------
//
// What the member may do, and who changed it. Distinct from KYC: verification is about
// who they are, these are about what the platform currently permits.

/// The account was disabled in Keycloak — the member cannot sign in.
///
/// Keycloak's `enabled` flag is the single source: it is what actually stops them, so a
/// freeze announced from anywhere else would promise an enforcement the platform does not
/// make. A KYC hold suspends *verification*, which is a different thing and stays silent.
pub const ACCOUNT_FROZEN: &str = "Account_Frozen";
/// The account was re-enabled in Keycloak.
pub const ACCOUNT_REACTIVATED: &str = "Account_Reactivated";
/// A specific permission was withdrawn (trading, crypto or fiat withdrawal) while the
/// account otherwise stays open. Narrower than [`ACCOUNT_FROZEN`], which suspends
/// everything gated on verification.
pub const ACCOUNT_RESTRICTED: &str = "Account_Restricted";

/// A withdrawal was requested from an unfamiliar device shortly after the credentials
/// changed — the shape of an account takeover. **Unwired:** cex-tx-risk-manager scores
/// `RECENT_PASSWORD_RESET`, `RECENT_2FA_CHANGE` and `NEW_DEVICE` independently, but nothing
/// composes them into a takeover verdict, and inventing that rule is a risk-policy decision
/// rather than a notification one.
pub const ACCOUNT_TAKEOVER: &str = "Account_Takeover";
/// The account was closed, by the member or by the platform. **Unwired:** no closure or
/// off-boarding flow exists in the platform — there is no endpoint, no status and no job
/// that closes an account.
pub const ACCOUNT_CLOSED: &str = "Account_Closed";

// --- Trading -----------------------------------------------------------------------
//
// Driven by the matching engine's own order-event stream, consumed by cex-history. Unlike
// the rest of this registry these scale with trading volume: an active trader produces
// hundreds a day and one resting order can produce dozens of partial fills. Configure the
// workflows accordingly — in-app rather than email/SMS unless the member opts in.

/// A new order passed validation and rests on the book.
pub const TRADE_ORDER_CREATED: &str = "Trade_Order_Created";
/// An order matched in part; the remainder is still working.
pub const TRADE_ORDER_PARTIALLY_FILLED: &str = "Trade_Order_Partially_Filled";
/// An order matched in full and is closed.
pub const TRADE_ORDER_FILLED: &str = "Trade_Order_Filled";
/// An order was cancelled by the member or by an admin. Distinct from expiry, which is an
/// order dying by its own time-in-force rules rather than anyone cancelling it.
pub const TRADE_ORDER_CANCELLED: &str = "Trade_Order_Cancelled";

/// A stop or trigger order reached its trigger price. **Unwired:** the matching engine has
/// no stop orders — `OrderType` is `LIMIT` or `MARKET` only, and there is no trigger price
/// anywhere in it. Wiring this needs the engine feature first.
pub const TRADE_ORDER_TRIGGERED: &str = "Trade_Order_Triggered";
/// Suspected wash trading, spoofing or market manipulation. **Unwired:** the engine
/// *prevents* self-trades per order (`RejectReason::SelfTradePrevented`) but nothing looks
/// for the pattern, and cex-trade-risk-manager is a pre-trade filter chain (balance, KYC,
/// notional, open orders) with no abuse detection. Deciding what counts as abuse is a
/// surveillance rule, not a notification one.
pub const TRADE_ABUSE: &str = "Trade_Abuse";

// --- Fiat withdrawal ------------------------------------------------------------
//
// The lifecycle, and which service owns each transition:
//   requested (ledger) → approved (ledger) → sent to bank (ledger)
//     → completed (history)                       … the bank settled it
//     → failed (history) → failed+refunded (ledger) … the bank returned it

/// The withdrawal request was accepted into the system and the balance reserved.
pub const FIAT_WITHDRAWAL_REQUESTED: &str = "Fiat_Withdrawal_Requested";
/// An admin (or the automated decision) approved the request; the payout is next.
pub const FIAT_WITHDRAWAL_APPROVED: &str = "Fiat_Withdrawal_Approved";
/// The bank accepted the payout instruction; settlement is now the bank's move.
pub const FIAT_WITHDRAWAL_SENT_TO_BANK: &str = "Fiat_Withdrawal_Sent_To_Bank";
/// The bank settled the transfer — the withdrawal is closed.
pub const FIAT_WITHDRAWAL_COMPLETED: &str = "Fiat_Withdrawal_Completed";
/// The bank reported the payout as failed. The balance is **still reserved** at this
/// point — it goes back to admin review, so this is the "we are on it" message, not a
/// refund confirmation. The refund, if it happens, is a separate event below.
pub const FIAT_WITHDRAWAL_FAILED: &str = "Fiat_Withdrawal_Failed";
/// A fiat withdrawal the bank returned or an admin rejected; the reserved balance
/// has been released back to available. Triggered from the ledger's withdraw
/// decision — the point where the release event is actually written — so the
/// message is never sent for a refund that did not happen.
pub const FIAT_WITHDRAWAL_FAILED_REFUNDED: &str = "Fiat_Withdrawal_Failed_Refunded";

// --- Fiat deposit ---------------------------------------------------------------

/// A bank movement was matched to the depositor and the amount is now on their
/// available balance.
pub const FIAT_DEPOSIT_CREDITED: &str = "Fiat_Deposit_Credited";
/// A deposit an admin rejected or returned to the sender. Only reaches a customer when
/// the movement was matched to one — an unmatched rejection has no recipient and is
/// [`ADMIN_FIAT_UNMATCHED_DEPOSIT`]'s story instead.
pub const FIAT_DEPOSIT_REJECTED: &str = "Fiat_Deposit_Rejected";

// --- Operations channels ---------------------------------------------------------
//
// Addressed to an `AdminChannel`, not to a person: routed to a topic whose membership
// lives in the notification center. The `Admin_` prefix in the identifier is the reminder
// that no customer ever receives these — which is also why they can fire at moments where
// no member has been identified yet.

/// A bank movement was ingested from the statement, before anything is known about who
/// sent it. Operations sees the platform's incoming flow as it lands.
pub const ADMIN_FIAT_DEPOSIT_DETECTED: &str = "Admin_Fiat_Deposit_Detected";
/// An incoming amount held because the sender's name/IBAN did not match a member.
/// By definition no member was matched, so there is no subscriber to send to.
pub const ADMIN_FIAT_UNMATCHED_DEPOSIT: &str = "Admin_Fiat_Unmatched_Deposit";

// --- Registered in the notification center, no producer in the platform yet ------
//
// These exist as workflows but nothing triggers them, because the code has no point
// that means what they describe. Listed so the registry mirrors the notification
// center rather than silently diverging from it; delete or wire them deliberately.

/// A corporate onboarding application was received. **Unwired:** there is no KYB flow.
/// The only corporate concept in the codebase is the MKK investor *kind*, which is a
/// registry filing for an already-verified member, not an application to onboard.
pub const KYB_APPLICATION_RECEIVED: &str = "KYB_Application_Received";
/// A deposit held for risk/limit review. **Unwired:** the fiat deposit path has no
/// risk or limit gate — everything that reaches `PENDING_ADMIN` gets there through
/// identity matching, which is [`ADMIN_FIAT_UNMATCHED_DEPOSIT`].
pub const FIAT_DEPOSIT_MANUAL_REVIEW: &str = "Fiat_Deposit_Manual_Review";

// --- User bank accounts --------------------------------------------------------

/// A member added a bank account as a payout destination.
pub const BANK_ACCOUNT_ADDED: &str = "Bank_Account_Added";

/// Several consecutive authentication failures were detected for a member.
pub const FAILED_LOGIN_WARNING: &str = "Failed_Login_Warning";

/// An official legal or regulatory information request was recorded.
pub const LEGAL_REQUEST_RECEIVED: &str = "Legal_Request_Received";

/// A member's password was changed successfully.
pub const PASSWORD_CHANGE: &str = "Password_Change";
/// A member signed in successfully.
pub const SUCCESSFUL_LOGIN: &str = "Successful_Login";

/// A member's withdrawal address whitelist was changed.
pub const WHITELIST_ADDRESS_CHANGE: &str = "Whitelist_Address_Change";

pub const CRYPTO_ADDRESS_ADDED: &str = "Crypto_Address_Added";
pub const CRYPTO_ADDRESS_DELETED: &str = "Crypto_Address_Deleted";
pub const CRYPTO_DEPOSIT_CONFIRMED: &str = "Crypto_Deposit_Confirmed";
pub const CRYPTO_DEPOSIT_CREDITED: &str = "Crypto_Deposit_Credited";
pub const CRYPTO_DEPOSIT_DETECTED: &str = "Crypto_Deposit_Detected";
pub const CRYPTO_DEPOSIT_MANUAL_REVIEW: &str = "Crypto_Deposit_Manual_Review";
pub const CRYPTO_DEPOSIT_UNCREDITED: &str = "Crypto_Deposit_Uncredited";
pub const CRYPTO_WITHDRAWAL_APPROVED: &str = "Crypto_Withdrawal_Approved";
pub const CRYPTO_WITHDRAWAL_BROADCASTED: &str = "Crypto_Withdrawal_Broadcasted";
pub const CRYPTO_WITHDRAWAL_COMPLETED: &str = "Crypto_Withdrawal_Completed";
pub const CRYPTO_WITHDRAWAL_FAILED_REJECTED: &str = "Crypto_Withdrawal_Failed_Rejected";
pub const CRYPTO_WITHDRAWAL_NO_TX: &str = "Crypto_Withdrawal_No_Tx";
pub const CRYPTO_WITHDRAWAL_REQUESTED: &str = "Crypto_Withdrawal_Requested";