fix(core): price compliance totals through the exchange-rate pivot - #221
Merged
Merged
Conversation
…ency sums Re-KYC's cumulative-deposit trigger only ever summed deposits already in the player's own currency field, which reads $0 forever on a platform with no matching base-currency wallet rail. The high_roller tag's lifetime-deposit total and the withdrawal queue's large_amount tag both summed or compared raw wallet_transaction.amount across whatever currencies a player used. All three now price through EXCHANGE_RATE_READER before comparing. A new sumInPivot helper (@openora/core/server) returns a large sentinel instead of a partial total when a currency can't be priced, so a missing rate never silently undercounts a compliance total - it forces the threshold to read as crossed instead.
Review fix on the pivot-pricing change: PIVOT_SUM_UNPRICED_SENTINEL poisoned the re-KYC watermark (a fired evaluation wrote triggerDeposits as the sentinel, so no later deposit could ever cross a fresh band) and made tag rules assign off a missing rate rather than a real value. sumInPivot now returns string | null instead of a sentinel - null means at least one currency could not be priced, never a guessed number standing in for a real one. - KycVerificationService.handleDeposit: on null, logs (logger.error + reportError, naming the userId and the unpriced currencies) and returns without writing a kyc_verification row or touching the watermark. The next deposit re-evaluates once rates are back. - WalletReader.getLifetimeDeposit port changed to Promise<string | null>. Every caller now handles null explicitly, in the direction that never loses the compliance signal: high_roller skips and warns (a sticky tag should not be assigned or removed off a guess); basic_kyc_needed still assigns (null only happens once a deposit exists); bonus eligibility's isFirstDeposit paths (offerFacts and the isFirstDeposit-less fallback) treat null as not-first. - The large_amount queue tag has no running total to lose, so an unpriced withdrawal is flagged rather than skipped - never a silent pass. - WalletReaderService takes a deps object; pivotCurrency is required and wallet/plugin.ts wires it from resolveExchangeRatePivot instead of a hardcoded default. Tests: handleDeposit crossing a threshold summed across currencies, and writing no row (then firing normally once rates return) when a currency is unpriced; getLifetimeDeposit pricing into the pivot vs returning null; large_amount on a small-face/high-value coin, a large-face/low-value coin, and an unpriced one.
zaxovaiko
requested review from
damianrzepka,
jakubfilinger-b,
klaudia-blazyczek-blurify,
marek-chmielowski-blurify,
mp-blurify and
okapitula
as code owners
September 24, 2026 22:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Compliance totals on a platform with no base currency are now priced through the exchange-rate pivot instead of summing raw amounts across currencies: the re-KYC cumulative deposit check, the lifetime deposit behind tag rules and first-deposit checks, and the
large_amountwithdrawal queue tag. BF-0Why
player.currency. On a platform where players deposit in several currencies, a player who never deposits in that currency reads as 0 forever, so re-KYC never fires.getLifetimeDepositsummed every currency together (1 BTC + 20000 DOGE = 20001), so a threshold rule compared a meaningless number.large_amounttag compared a raw amount in any currency against one flat threshold.A missing rate never produces a guessed total:
sumInPivotreturnsnullwhen any currency cannot be priced.WalletReader.getLifetimeDepositnow returnsstring | null(priced into the pivot). high_roller skips on null, basic_kyc_needed still assigns, first-deposit checks read null as not-first.large_amountflags an unpriced withdrawal rather than letting it pass.Alternatives considered
Risks
WalletReader.getLifetimeDepositreturn type widened tostring | null- a breaking change for any external implementer or caller (minor bump, documented in the changeset).WalletReaderServiceconstructor takes a deps object with a requiredpivotCurrency.