Finance

RemittanceQuote

Sending money abroad: you send, the fee, the rate against the mid-market rate, what they get and when it arrives, worked out as the amount changes.

#finance#forex#banking#calculator#forms#interactive

Demo

Preview
USD
Fee
+ $2.99
Our rate
1 USD = 57.4226 PHP
mid-market 58.1200, 1.20% markup
Total to pay
$502.99
₱28,711.30PHP
How they get it

Arrives: Within 1 hour

Against the mid-market rate this costs $8.99 in all: the fee, $2.99, and $6.00 in the rate.

GBP
Fee, taken from the amount
− £1.00
Our rate
1 GBP = 105.1000 INR
mid-market 106.4200, 1.24% markup
Total to pay
£200.00
₹20,914.90INR

Arrives: Within 2 working days

Against the mid-market rate this costs £3.47 in all: the fee, £1.00, and £2.47 in the rate.

Change the amount.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/remittance_quote.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/remittance_quote.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/remittance_quote.qwe
component RemittanceQuoteDemoLive() {    state note = "Change the amount."    <div.demo-stack>        <RemittanceQuote name="uk" send_currency="GBP" receive_currency="INR" mid={106.42m} rate={105.10m} fee_percent={0.5m}                         fee_deducted={true} amount={200} least={10} locale="en-GB" delivery="Within 2 working days" cta={null}                         on_change={(q) => { note = `${q.send} GBP: fee ${q.fee}, they get ${q.receive} INR at ${q.rate} (${q.markup}% under the mid).` }}/>        <p.demo-note id="remittance-quote-demo-note">{note}</p>    </div>}view RemittanceQuoteDemo() =    <div class="demo-row" style="align-items: start">        <RemittanceQuote send_currency="USD" receive_currency="PHP" mid={58.12m} markup={1.2m} fee={2.99m} amount={500} most={2999}                         methods={[{ id: "bank", label: "Bank deposit", delivery: "Within 1 hour", note: "Banco Lumina, Kalayaan Savings" },                                   { id: "cash", label: "Cash pickup", fee: 4.99m, delivery: "In minutes", note: "4,200 partner outlets" },                                   { id: "wallet", label: "Mobile wallet", fee: 0, markup: 1.5m, delivery: "In minutes" }]}/>        <RemittanceQuoteDemoLive/>    </div>

Source

ui/remittance_quote.qwe
// RemittanceQuote: what sending money abroad costs, worked out as the amount is// typed: you send, the fee, the total to pay, the rate offered against the// mid-market rate (the markup, said as a percent and as money), what they get, and// when it arrives. Ways to deliver (bank deposit, cash pickup, a mobile wallet) can// each have their own fee, rate and time, picked from cards. The fields are real// (`send_amount`, `method`, and a hidden `receive_amount`), so a plain <form> sends them.////   <RemittanceQuote send_currency="USD" receive_currency="PHP" mid={58.12m} markup={1.2m} fee={2.99m} amount={500}/>//   <RemittanceQuote send_currency="GBP" receive_currency="INR" mid={106.42m} rate={105.10m} fee_percent={0.5m}//                    methods={[{ id: "bank", label: "Bank deposit", delivery: "Within 1 hour" },//                              { id: "cash", label: "Cash pickup", fee: 3.50m, delivery: "In minutes" }]}//                    on_change={(q) => { quote = q }}/>//// send_currency and receive_currency: ISO codes ("USD", "PHP"). mid: the mid-market// rate, the receive currency one unit of the send currency buys. rate: the rate you// offer; or markup: how far under the mid it is, in percent (rate wins when both are// given; neither means the mid). fee: a fixed fee in the send currency (0);// fee_percent: and a percent of the amount (0). fee_deducted: the fee comes out of the// amount sent instead of on top (false). methods: a list of { id, label, delivery,// fee, fee_percent, rate, markup, note }, each overriding the prices above; without// it there is one way, with `delivery` ("Usually within 1 hour"). method: the id// picked to start with (the first). amount: to start with (500). least and most: the// amounts allowed (1; none). locale: how amounts are written ("en-US"). cta: the// submit button's text ("Continue"; null for none). name: put before the fields'// names ("r" makes r_send_amount, ...) and their ids. on_change(q): inside one of your// components, given { send, fee, total, rate, mid, markup, receive, method } (decimals)// when the amount or the way changes and the amount is allowed.import ui/theme.qwe use ui_clslet remittance_quote_css = css`.ui-remitq { display: grid; gap: 14px; min-width: 0; max-width: 440px; padding: 18px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.4 var(--ui-font); color: var(--ui-fg);  font-variant-numeric: tabular-nums; }.ui-remitq-row { display: grid; gap: 6px; }.ui-remitq-label { font-size: 13px; font-weight: 500; color: var(--ui-muted); }.ui-remitq-box { display: flex; align-items: center; gap: 10px; height: 50px; padding: 0 14px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); transition: border-color .15s, box-shadow .15s; }.ui-remitq-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-remitq-box input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent; color: var(--ui-fg);  font: 650 20px var(--ui-font); font-variant-numeric: tabular-nums; }.ui-remitq-box b { flex: none; padding: 3px 8px; border-radius: 999px; background: var(--ui-subtle); font-size: 13px; font-weight: 650; }.ui-remitq-out { background: var(--ui-subtle); }.ui-remitq-out output { flex: 1; min-width: 0; font-size: 20px; font-weight: 650; overflow-wrap: anywhere; }.ui-remitq-out b { background: var(--ui-surface); }.ui-remitq-invalid .ui-remitq-box:first-of-type { border-color: var(--ui-danger); }.ui-remitq-error { margin: 0; color: var(--ui-danger); font-size: 13px; }.ui-remitq-lines { display: grid; gap: 6px; margin: 0 0 0 10px; padding: 2px 0 2px 16px; border-left: 2px solid var(--ui-border); font-size: 13px; }.ui-remitq-lines div { display: flex; justify-content: space-between; gap: 12px; }.ui-remitq-lines dt { color: var(--ui-muted); }.ui-remitq-lines dd { margin: 0; text-align: right; font-weight: 500; }.ui-remitq-lines .ui-remitq-total dd, .ui-remitq-lines .ui-remitq-total dt { color: var(--ui-fg); font-weight: 650; }.ui-remitq-markup { color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-remitq-methods { display: grid; gap: 6px; margin: 0; padding: 0; border: 0; }.ui-remitq-methods legend { margin-bottom: 6px; padding: 0; font-size: 13px; font-weight: 500; color: var(--ui-muted); }.ui-remitq-method { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 2px 10px; padding: 10px 12px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer; transition: border-color .15s, background .15s; }.ui-remitq-method:hover { border-color: var(--ui-strong); }.ui-remitq-method input { grid-row: span 2; margin: 0; accent-color: var(--ui-accent); }.ui-remitq-method b { font-weight: 600; }.ui-remitq-method small { grid-column: 2; color: var(--ui-muted); font-size: 12px; }.ui-remitq-method span { grid-row: span 2; color: var(--ui-muted); font-size: 12px; text-align: right; }.ui-remitq-method:has(input:checked) { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-remitq-method:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-remitq-eta { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 13px; }.ui-remitq-eta svg { flex: none; width: 16px; height: 16px; color: var(--ui-success); }.ui-remitq-cost { margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-remitq-cta { height: 44px; border: 0; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg);  font: 600 15px var(--ui-font); cursor: pointer; }.ui-remitq-cta:hover:not(:disabled) { filter: brightness(1.08); }.ui-remitq-cta:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-remitq-cta:disabled { opacity: .45; cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-remitq-box, .ui-remitq-method { transition: none; } }`// An amount in its currency, rounded by `mode` to its minor units: "$502.99", "₱28,711.30".fn remittance_quote_money(amount: dec, code: str, locale: str, mode: str = "half_up") -> str {    let m = money.of(amount, code, mode)    type_of(m) == "error" ? `${format_number(round(amount, 2, mode), locale, 2)} ${code}` : money.format(m, locale)}// An amount rounded to the currency's minor units (2 when it is not known).fn remittance_quote_round(amount: dec, code: str, mode: str = "half_up") -> dec {    let m = money.of(amount, code, mode)    type_of(m) == "error" ? round(amount, 2, mode) : m.amount}// What was typed as a decimal ("1,250.5" is 1250.5m), or null.fn remittance_quote_read(text: str) {    let t = re.replace(trim(text), "[,\\s_]", "")    if t == "" { return null }    let d = dec(t)    is_err(d) || d < 0 ? null : d}// The quote for an amount by one way: { send, fee, total, converted, rate, mid, markup, receive }.fn remittance_quote_work(send: dec, way, mid: dec, rate, markup, fee, fee_percent, fee_deducted: bool, from: str, to: str) {    let r = get(way, "rate") != null ? dec(way.rate)        : (get(way, "markup") != null ? mid * (100 - dec(way.markup)) / 100        : (rate != null ? dec(rate) : (markup != null ? mid * (100 - dec(markup)) / 100 : mid)))    let charge = remittance_quote_round(dec(get(way, "fee") ?? fee) + send * dec(get(way, "fee_percent") ?? fee_percent) / 100, from)    let converted = fee_deducted ? (send > charge ? send - charge : 0m) : send    let shown = round(r, 4, "half_up")    {        send, fee: charge, total: fee_deducted ? send : send + charge, converted, rate: shown, mid,        markup: mid > 0 ? round((mid - shown) * 100 / mid, 2, "half_up") : 0m,        receive: remittance_quote_round(converted * shown, to, "down"),        lost: remittance_quote_round(mid > 0 ? converted * (mid - shown) / mid : 0m, from),    }}component RemittanceQuote(send_currency: str = "USD", receive_currency: str, mid, rate = null, markup = null, fee = 0,                          fee_percent = 0, fee_deducted: bool = false, methods: list? = null, method: str? = null,                          delivery: str = "Usually within 1 hour", amount = 500, least = 1, most = null,                          locale: str = "en-US", cta: str? = "Continue", name: str? = null, on_change = null,                          class: str? = null) {    style global remittance_quote_css    let from = upper(send_currency)    let to = upper(receive_currency)    let ways = methods ?? [{ id: "standard", label: "Standard", delivery }]    let field = (f: str) => name != null ? `${name}_${f}` : f    let id = `remittance-quote-${name ?? lower(from + to)}`    let m = dec(mid)    state text = str(amount)    state way_id = method ?? ways[0].id    let way = find(ways, (w) => w.id == way_id) ?? ways[0]    let value = remittance_quote_read(text)    let low = dec(least)    let high = most != null ? dec(most) : null    let problem = value == null ? "Enter the amount to send."        : (value < low ? `The least you can send is ${remittance_quote_money(low, from, locale)}.`        : (high != null && value > high ? `The most you can send at once is ${remittance_quote_money(high, from, locale)}.` : null))    let q = problem == null ? remittance_quote_work(value, way, m, rate, markup, fee, fee_percent, fee_deducted, from, to) : null    let tell = (t: str, wid: str) => {        let v = remittance_quote_read(t)        let w = find(ways, (x) => x.id == wid) ?? ways[0]        if on_change != null && v != null && v >= low && (high == null || v <= high) {            let r = remittance_quote_work(v, w, m, rate, markup, fee, fee_percent, fee_deducted, from, to)            on_change({ send: r.send, fee: r.fee, total: r.total, rate: r.rate, mid: r.mid, markup: r.markup, receive: r.receive, method: w.id })        }    }    let fee_of = (w) => {        let f = remittance_quote_round(dec(get(w, "fee") ?? fee) + (value ?? 0m) * dec(get(w, "fee_percent") ?? fee_percent) / 100, from)        f == 0 ? "No fee" : `${remittance_quote_money(f, from, locale)} fee`    }    <div class={ui_cls(["ui-remitq", problem != null && "ui-remitq-invalid", class])} role="group" aria-label={`Send ${from} to ${to}`}>        <div.ui-remitq-row>            <label.ui-remitq-label for={`${id}-send`}>You send</label>            <div.ui-remitq-box>                <input type="text" id={`${id}-send`} name={field("send_amount")} value={text} inputmode="decimal" autocomplete="off"                       aria-invalid={problem != null ? "true" : null} aria-describedby={problem != null ? `${id}-error` : `${id}-lines`}                       @input={(e) => {                           text = e.value                           tell(e.value, way_id)                       }}/>                <b>{from}</b>            </div>            {problem != null && <p.ui-remitq-error id={`${id}-error`} role="alert">{problem}</p>}        </div>        <dl.ui-remitq-lines id={`${id}-lines`}>            <div data-line="fee"><dt>{fee_deducted ? "Fee, taken from the amount" : "Fee"}</dt><dd>{q != null ? (q.fee == 0 ? "No fee" : `${fee_deducted ? "−" : "+"} ${remittance_quote_money(q.fee, from, locale)}`) : "—"}</dd></div>            <div data-line="rate"><dt>Our rate</dt><dd>                {q != null ? `1 ${from} = ${format_number(q.rate, locale, 4)} ${to}` : "—"}                {q != null && <span.ui-remitq-markup><br/>mid-market {format_number(round(m, 4, "half_up"), locale, 4)}, {q.markup == 0 ? "no markup" : `${format_number(q.markup, locale, 2)}% markup`}</span>}            </dd></div>            <div.ui-remitq-total data-line="total"><dt>Total to pay</dt><dd>{q != null ? remittance_quote_money(q.total, from, locale) : "—"}</dd></div>        </dl>        <div.ui-remitq-row>            <label.ui-remitq-label for={`${id}-receive`}>They get</label>            <div class="ui-remitq-box ui-remitq-out">                <output id={`${id}-receive`} for={`${id}-send`} aria-live="polite">{q != null ? remittance_quote_money(q.receive, to, locale, "down") : "—"}</output>                <b>{to}</b>            </div>            <input type="hidden" name={field("receive_amount")} value={q != null ? str(q.receive) : ""}/>        </div>        {len(ways) > 1            ? <fieldset.ui-remitq-methods>                  <legend>How they get it</legend>                  {for w in ways => <label.ui-remitq-method key={w.id} data-method={w.id}>                      <input type="radio" name={field("method")} value={w.id} checked={w.id == way_id}                             @change={() => {                                 way_id = w.id                                 tell(text, w.id)                             }}/>                      <b>{w.label}</b>                      <span>{fee_of(w)}</span>                      <small>{get(w, "delivery") ?? delivery}{get(w, "note") != null ? ` · ${w.note}` : ""}</small>                  </label>}              </fieldset>            : <input type="hidden" name={field("method")} value={way.id}/>}        <p.ui-remitq-eta>            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6.5"/><path d="M8 4.5V8l2.5 1.5"/></svg>            <span>Arrives: <b>{get(way, "delivery") ?? delivery}</b></span>        </p>        {q != null && <p.ui-remitq-cost data-cost={str(q.fee + q.lost)}>            Against the mid-market rate this costs {remittance_quote_money(q.fee + q.lost, from, locale)} in all:            {q.fee == 0 ? " no fee" : ` the fee, ${remittance_quote_money(q.fee, from, locale)}`}, and {remittance_quote_money(q.lost, from, locale)} in the rate.        </p>}        {cta != null && <button.ui-remitq-cta type="submit" disabled={problem != null}>{cta}</button>}    </div>}
Thread
85Useful? Like it, or add to its thread below.

Thread 0 posts

On RemittanceQuote: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.