Skip to content

feat(cashout): price JMD payout at live NCB rate from ERPNext - #445

Merged
bobodread876 merged 3 commits into
mainfrom
feat/cashout-live-ncb-rate
Jul 14, 2026
Merged

feat(cashout): price JMD payout at live NCB rate from ERPNext#445
bobodread876 merged 3 commits into
mainfrom
feat/cashout-live-ncb-rate

Conversation

@islandbitcoin

Copy link
Copy Markdown
Contributor

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 ERPNext Currency Exchange records; for money-out, the correct side is for_buying (USD→JMD ≈ 152.7). Users were quoted 160 while the real bank buy rate was ~152.7.

Change

  • ErpNext.getCashoutExchangeRate() — reads the latest for_buying USD→JMD Currency Exchange record (same REST pattern as getBankAccountsByCustomer).
  • 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.
  • Retires the static ExchangeRates config 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 + receiveJmd on the settle screen and the ERP payable JE all use the same value.

Validation

  • tsc --noEmit + eslint clean.
  • Ran the exact query as the flash API user (flash_sa@getflash.io, permissions enforced) against prod ERPCurrency Exchange returns 152.7 (2026-07-13), confirming the query and read permission.
  • No unit tests added — these ERPNext service methods (getBankAccountsByCustomer, listBanks, …) aren't unit-tested in this repo; validated against live ERP instead.

Notes

  • Fail-closed = a JMD cashout errors if ERPNext is unreachable at offer time (deliberate; chosen over transacting at a stale rate). ERPNext serves the most-recent rate ≤ today, so a missed weekly scrape does not trigger this.
  • Topup intentionally left as-is (prices differently; no analogous backend rate step).

🤖 Generated with Claude Code

bobodread876 and others added 3 commits July 14, 2026 03:14
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
bobodread876 merged commit ae075f8 into main Jul 14, 2026
15 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants