Skip to content

Fix: feat(frontend): make quote freshness and currency validation explicit in send flow - #300

Open
euniceamoni wants to merge 1 commit into
RemitFlow:mainfrom
euniceamoni:simple/279
Open

euniceamoni wants to merge 1 commit into
RemitFlow:mainfrom
euniceamoni:simple/279

Conversation

@euniceamoni

Copy link
Copy Markdown

Closes #279

Issues

#279 feat(frontend): make quote freshness and currency validation explicit in send flow

Problem

Users can confirm a transfer using an expired quote or an amount/currency combination that the backend will reject.

Objective

Deliver a production-quality improvement to FX quote selection, amount entry, and send confirmation that creates measurable value for correctness, security, reliability, performance, or maintainability.

Implementation scope

  • Show quote source, timestamp, expiry, currency metadata, and canonical amount; invalidate on field/network changes; bind quote ID to payload.

Acceptance criteria

  • Expired or mismatched quotes cannot be signed; displayed and serialized amounts reconcile; refresh behavior is clear.

Required validation

  • Expiry, currency matrix, precision, changed-input, and quote-binding tests.
  • Existing tests and CI remain passing.
  • Add regression coverage for the original failure mode.
  • Do not weaken, delete, or skip unrelated tests to obtain a green build.

PR quality bar

  • Keep the PR focused and explain design tradeoffs, compatibility impact, and test evidence.
  • Avoid typo-only, documentation-only, cosmetic-only, or unrelated refactor submissions.

Out of scope

  • Broad rewrites not required by the acceptance criteria.
  • Changes to unrelated services, contracts, or user flows.

Summary

Keep the Quote and Transfer contract changes backward compatible where possible: existing fixtures and tests must keep passing, so add new fields as required only if fixtures are updated in the same change, otherwise mark them optional and enforce presence in validateQuoteForTransfer. The quoteId must be generated deterministically enough for tests (inject a clock/id factory via buildQuote options). Invalidate the active quote on every input and network change, and re-validate at confirmation time against the live clock so a quote that expires while the dialog is open cannot be submitted. Do not strip quoteId in useTransfers or api.createTransfer.

Changes

  • Extend the Quote v1 contract to carry explicit freshness and currency metadata: add required fields source (string, e.g. the quote provider/feed identifier), quoteId (string, minLength 1), fromCurrency/toCurrency metadata objects (or reuse from/to plus add fromMinorUnits/toMinorUnits integers), and a canonicalAmount decimal string. Add a validateQuoteForTransfer(quote, { now, from, to, amount }) helper that returns { valid, reasons } covering expiry (via isQuoteExpired), currency-pair mismatch against the requested from/to, and canonical-amount/precision mismatch (compare the quote's canonicalAmount against the requested amount quantized to the source currency minor unit). Keep the existing fields and isQuoteExpired behaviour so current fixtures and tests continue to pass. (src/services/contracts/quote.js)
  • Update buildQuote to populate the new quote metadata: generate a stable quoteId (e.g. crypto.randomUUID or a deterministic id derived from inputs+timestamp for tests), set source to the FX feed identifier (e.g. 'mock-fx'), include currency metadata (from/to minor units via currencyExponent) and a canonicalAmount equal to the quantized send amount. Keep the injectable now/ttlMs options and the parseQuote round-trip so every produced quote is contract-valid. Do not change existing numeric outputs (sendAmount, fee, amountAfterFee, receiveAmount, rate) so precision tests stay green. (src/services/quote.js)
  • Expose the FX feed metadata needed by the quote: add an exported constant (e.g. FX_SOURCE = 'mock-fx') and a getRateMetadata(from, to) helper returning { source, from, to, rate, fromMinorUnits, toMinorUnits } (minor units via currencyExponent) so the quote service and QuoteCard can display where a rate came from and its precision. Keep getRateDecimal/getRate/convert unchanged. (src/services/fx.js)
  • Add a quoteId field (string, required, minLength 1) to the Transfer v1 contract so the quote that priced a transfer is bound to the stored record. Keep it optional-compatible by marking it required: false if legacy records must still parse, but document that new transfers always include it. Do not change other fields or status handling. (src/services/contracts/transfer.js)
  • Add a getCurrencyMetadata(code) helper (or extend getCurrency) returning { code, name, symbol, minorUnits } for display in QuoteCard, and export a SUPPORTED_CURRENCY_CODES list derived from CURRENCIES for the currency-matrix validation. Keep existing exports (CURRENCIES, DEFAULT_SOURCE, DEFAULT_DEST, getCurrency, getCurrencyMinorUnits) unchanged. (src/constants/currencies.js)
  • Add a validateQuoteForTransfer(quote, { now, from, to, amount, locale }) helper that returns { valid: boolean, reasons: string[] } by delegating to the quote contract's validity check and additionally verifying the quote's currency pair matches the form's from/to and that the quote's canonicalAmount equals the form amount quantized to the source currency minor unit. This centralises the expiry/currency/precision checks used by SendMoney and keeps them unit-testable. (src/utils/validate.js)
  • Render the new quote metadata in QuoteCard: show the quote source, created-at timestamp, expiry timestamp (with an 'expired' visual state when isQuoteExpired is true), the from/to currency names and minor units, and the canonical amount. Accept a now prop (default Date.now()) so tests can pin the clock, and add an invalid/reasons prop to render a warning list when validateQuoteForTransfer fails. Keep the existing rate/fee/receive rows and formatting helpers. (src/components/QuoteCard.jsx)
  • Add styles for the new QuoteCard metadata rows and the expired/invalid state: a .quote-meta block for source/timestamp/expiry/currency metadata, a .quote-expired warning banner (contrast-safe, with a [data-theme='light'] variant), and .quote-invalid-reasons list styling. Ensure prefers-contrast: more keeps borders visible. (src/components/QuoteCard.css)
  • In SendMoney: track the inputs that produced the active quote (amount, from, to, recipient) and the network/online state; invalidate the active quote whenever any of those change (clear pendingQuote and the live quote, or mark it stale) so a stale quote cannot be confirmed. On 'Review & Send', build the quote and run validateQuoteForTransfer; block opening the confirmation dialog when the quote is expired or mismatched, surfacing the reasons. In handleConfirmTransfer, re-validate the pending quote against the current inputs and clock before submitting, and include quoteId: finalQuote.quoteId in the addTransfer payload. Disable the confirm button when the quote is invalid. Pass now/invalid/reasons through to QuoteCard. (src/pages/SendMoney.jsx)
  • Add styles for the new SendMoney states: a .send-quote-invalid notice shown when the active quote is expired or mismatched, and any layout tweaks needed for the expanded QuoteCard metadata. Keep existing offline/reconnected notice styles and the prefers-contrast: more overrides. (src/pages/SendMoney.css)
  • Ensure useTransfers.addTransfer forwards the full payload including quoteId to createTransfer unchanged (no field stripping), and that a ContractViolationError from a missing/mismatched quoteId is surfaced through the existing error path. Add a brief comment documenting that the quote ID binds the transfer to the quote that priced it. (src/hooks/useTransfers.js)
  • Add unit tests for the new quote metadata and validity helper: buildQuote populates quoteId/source/canonicalAmount/currency metadata; validateQuoteForTransfer returns invalid for an expired quote (using injected now), for a mismatched currency pair, and for a precision/canonical-amount mismatch; and returns valid for a fresh matching quote. Keep existing buildQuote assertions intact. (test/unit/quote.test.js)
  • Extend quote-contract tests: assert the new required fields (quoteId, source, canonicalAmount, currency metadata) are present and typed in v1 fixtures, add breaking fixtures for a missing quoteId and a wrong-typed canonicalAmount with expected diffs, and cover validateQuoteForTransfer expiry/currency/precision cases. Update the EXPECTED_BREAKING map accordingly so the fixture-coverage assertion stays green. (test/unit/quote-contract.test.js)
  • Add money-level tests for the precision reconciliation used by quote validation: quantize/canonicalAmount equality across the currency matrix (including zero- and three-decimal currencies) and compareDecimal behaviour at the canonical-amount boundary, so the precision-mismatch check is covered at the arithmetic layer. (test/unit/money.test.js)
  • Add integration tests for the send flow: (1) regression — an expired quote blocks confirmation and shows the expiry reason instead of submitting; (2) changing amount/from/to/recipient after a quote invalidates it and requires a fresh review; (3) a currency-matrix case (e.g. USD->INR and EUR->NGN) confirms successfully and the createTransfer payload includes a quoteId matching the active quote; (4) precision — the canonical amount shown matches the quoted amount. Keep existing form-flow tests passing. (test/integration/send-money-form.test.jsx)
  • Extend the precision integration tests to assert the transfer payload includes quoteId and that the QuoteCard displays the canonical amount, source, timestamp and expiry, and that a precision mismatch between the form amount and the quote's canonicalAmount blocks confirmation. Keep the existing fee/rate/receipt assertions unchanged. (test/integration/send-money-precision.test.jsx)

Co-authored-by: Simple Agent <agent@simple.dev>
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.

feat(frontend): make quote freshness and currency validation explicit in send flow

1 participant