feat(cashout): price JMD payout at live NCB rate from ERPNext - #445
Merged
Conversation
The cashout offer rate was a hardcoded config value (exchangeRates.USD.JMD.ask = 160), flagged `// todo: get from price server`. A weekly CronJob scrapes NCB's board into ERPNext `Currency Exchange` records; for money-out the correct side is `for_buying` (~152.7). Read that live rate at offer time instead of the static constant. - ErpNext.getCashoutExchangeRate(): latest for_buying USD->JMD rate - CashoutManager: fail closed on JMD payout when the live rate is unavailable (never build an offer at a guessed rate); USD payout unaffected - retire the static ExchangeRates config usage in the cashout path Validated against prod ERP as flash_sa@getflash.io -> 152.7 (2026-07-13). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createOffer now sources the JMD rate from ErpNext.getCashoutExchangeRate instead of static config; the integration mock lacked the method (TypeError: not a function). Mock it to J$160 so the existing offer assertions (serviceFee 2, payout 15680 JMD cents, rate 16000) hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second offers integration spec exercises createOffer -> same missing mock (TypeError: not a function). Mirror the make-cashout-offer fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bobodread876
approved these changes
Jul 14, 2026
islandbitcoin
added a commit
that referenced
this pull request
Jul 15, 2026
#449) `JMDAmount.dollars(d)` did `new JMDAmount(BigInt(d) * 100n)`, and `BigInt(d)` throws on any non-integer `d`. The cashout offer wraps the live NCB rate (essentially always fractional — 152.7, 155.5, ...) via this method, so `getCashoutExchangeRate` returned `ExchangeRateQueryError` for every real rate and `requestCashout` failed. Rewrote it to mirror `USDAmount.dollars` (Money-lib, HALF_TO_EVEN) so fractional rates convert correctly. Compounding it, `error-map.ts` had no `ExchangeRateQueryError` case, so the failure fell through to the exhaustiveness fallback ("This should never compile ...") and returned a `data: null` top-level GraphQL error — the app therefore showed no message, just a spinner. Added a mapped case. Only surfaces post-#444/#445 (live NCB rate); a deploy would break JMD cashout in every env until this lands. Co-authored-by: Dread <bobodread@bobodread.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
JMD cashout offers now price at the live NCB buy rate read from ERPNext, instead of the hardcoded
exchangeRates.USD.JMD.ask(160).Why
The offer rate was static config with a standing
// todo: get from price server(CashoutManager.ts). A weekly CronJob scrapes NCB's board into ERPNextCurrency Exchangerecords; for money-out, the correct side isfor_buying(USD→JMD ≈ 152.7). Users were quoted 160 while the real bank buy rate was ~152.7.Change
ErpNext.getCashoutExchangeRate()— reads the latestfor_buyingUSD→JMDCurrency Exchangerecord (same REST pattern asgetBankAccountsByCustomer).CashoutManager.createOffer— uses that rate for JMD payouts; fails closed if the live rate can't be read (never builds a JMD offer at a guessed rate). USD payouts unchanged.ExchangeRatesconfig wiring in the cashout path.Behaviour change
JMD payouts settle at the real bank buy rate (~152.7) rather than 160 — users receive ~4.5% less JMD, matching what Flash actually realizes on the FX. The rate +
receiveJmdon the settle screen and the ERP payable JE all use the same value.Validation
tsc --noEmit+ eslint clean.flash_sa@getflash.io, permissions enforced) against prod ERP →Currency Exchangereturns152.7 (2026-07-13), confirming the query and read permission.getBankAccountsByCustomer,listBanks, …) aren't unit-tested in this repo; validated against live ERP instead.Notes
🤖 Generated with Claude Code