Commerce

BestCoupon

Works out the lowest total from several coupons, trying each combination and order under their rules, and explains the winner and why each other coupon lost.

#smart#interactive#commerce#coupon#discount#checkout

Demo

Preview

Best coupon

  • The code SAVE10 is given twice: the second is left out.

Best: Save 10 then Take 15 saves $32.00.

  • Cart$170.00
  • Save 10minus $17.00
  • Take 15minus $15.00
To pay$138.00

In this order it saves $1.50 more than the other way round.

  • Used: takes off $17.00.
  • Used: takes off $15.00.
  • Cannot be combined. Alone it saves $30.00, $2.00 less than the winner.
  • Needs $25.00 of books, and this cart has $20.00: $5.00 short.
  • Expired on Oct 1, 2026.
  • Needs $300.00, and this cart has $170.00: $130.00 short.
  • Nothing in toys in this cart.
  • Cannot be combined. Alone it saves $20.00, $12.00 less than the winner.
  • HUGECannot be used: it takes more than 100%.

The shop's order, minimum after discounts

Best: TEN saves $10.00.

  • Cart$100.00
  • TENminus $10.00
To pay$90.00
  • Used: takes off $10.00.
  • Needs $95.00 when its turn comes, and only $90.00 is left by then.

Switch a coupon off to see on_change.

A coupon larger than the cart, in pennies across categories

  • Bad has no price or quantity it can read, so it is left out.

Best: ALL saves £0.10.

  • Cart£0.10
  • ALLminus £0.10
To pay£0.00
  • Used: takes off £0.10.
  • Adds nothing on top of the winner.

More stacking coupons than are tried together

Best: C3 then C6 then C7 then C8 then C4 then C5 saves $288.53.

  • Cart$1,000.00
  • C3minus $30.00
  • C6minus $58.20
  • C7minus $63.83
  • C8minus $67.84
  • C4minus $31.21
  • C5minus $37.45
To pay$711.47

In this order it saves $0.02 more than the other way round.

  • Used: takes off $30.00.
  • Used: takes off $58.20.
  • Used: takes off $63.83.
  • Used: takes off $67.84.
  • Used: takes off $31.21.
  • Used: takes off $37.45.
  • Not tried: only the 6 stacking coupons that save the most alone go together.
  • Not tried: only the 6 stacking coupons that save the most alone go together.

An empty cart

The cart is empty, so there is nothing to save on.

  • Cart$0.00
To pay$0.00
  • Nothing in the cart to take it off.

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/best_coupon.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/best_coupon.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/best_coupon.qwe
let best_coupon_demo_cart = [    { name: "Trail shoes", category: "Shoes", price: "120.00" },    { name: "Wool socks", category: "Apparel", price: "10.00", qty: 3 },    { name: "Route guide", category: "Books", price: "20.00" }]let best_coupon_demo_coupons = [    { code: "TAKE15", label: "Take 15", kind: "fixed", value: "15", stackable: true, min_spend: "100" },    { code: "save10", label: "Save 10", kind: "percent", value: "10", stackable: true },    { code: "SHOES25", label: "Shoe sale", kind: "percent", value: "25", category: "shoes" },    { code: "BOOK5", label: "Book token", kind: "fixed", value: "5", stackable: true, category: "books", min_spend: "25" },    { code: "OLD20", label: "Last season", kind: "percent", value: "20", expires: "2026-10-01" },    { code: "BIG50", label: "Big spender", kind: "fixed", value: "50", min_spend: "300" },    { code: "TOYS", label: "Toy sale", kind: "percent", value: "50", category: "toys" },    { code: "FLAT20", label: "Flat 20", kind: "fixed", value: "20" },    { code: "SAVE10", label: "Again", kind: "percent", value: "99" },    { code: "HUGE", kind: "percent", value: "120" }]component BestCouponDemoLive() {    state seen = null    <div.demo-stack>        <BestCoupon id="best-coupon-demo-live" title="The shop's order, minimum after discounts" order="percent_first" basis="after" name="codes"                    cart={[{ name: "Jacket", price: "100.00" }]}                    coupons={[{ code: "FIVE", kind: "fixed", value: "5", stackable: true, min_spend: "95" }, { code: "TEN", kind: "percent", value: "10", stackable: true }]}                    on_change={(f) => { seen = f }}/>        <p.demo-note>{seen == null ? "Switch a coupon off to see on_change." : `on_change: ${join(seen.codes, ", ")}, to pay ${seen.total}, saving ${seen.saving}.`}</p>    </div>}view BestCouponDemo() =    <div.demo-stack>        <BestCoupon id="best-coupon-demo" today="2026-11-01" cart={best_coupon_demo_cart} coupons={best_coupon_demo_coupons}/>        <BestCouponDemoLive/>        <BestCoupon id="best-coupon-demo-free" title="A coupon larger than the cart, in pennies across categories" currency="GBP"                    cart={[{ name: "A", category: "x", price: "0.05" }, { name: "B", category: "y", price: "0.05" }, { name: "Bad", price: "soon" }]}                    coupons={[{ code: "THIRD", kind: "percent", value: "33.3", cap: "0.02", stackable: true }, { code: "ALL", kind: "fixed", value: "5", stackable: true }]}/>        <BestCoupon id="best-coupon-demo-many" title="More stacking coupons than are tried together" cart={[{ name: "Sofa", price: "1000.00" }]}                    coupons={range(1, 9) | map((n) => { code: `C${n}`, kind: "percent", value: n, stackable: true })}/>        <BestCoupon id="best-coupon-demo-empty" title="An empty cart" cart={[]} coupons={[{ code: "X", kind: "percent", value: "10" }]}/>    </div>

Source

ui/best_coupon.qwe
// BestCoupon: the lowest total a cart can reach with the coupons on offer, found by// trying them, not by a rule of thumb. A coupon that does not stack goes alone;// those that stack are tried in every combination and, as a percentage taken before// an amount saves more than after it, every order, or in the order the shop sets.// It shows the winner step by step, says what the order was worth, and gives each// other coupon its reason for losing: expired, short of its minimum by so much,// nothing in its category, or how much less it would have saved. Each step rounds// half up to the cent and comes off what is left, so the steps add up to the total.////   <BestCoupon today="2026-11-01" cart={[//       { name: "Trail shoes", category: "shoes", price: 120 }, { name: "Socks", category: "apparel", price: 10, qty: 3 }]}//       coupons={[{ code: "SAVE10", kind: "percent", value: 10, stackable: true },//                 { code: "TAKE15", kind: "fixed", value: 15, stackable: true, min_spend: 100 },//                 { code: "SHOES25", kind: "percent", value: 25, category: "shoes" }]}/>//// cart: a list of { name, price, qty?, category? }, price a number, decimal or text.// coupons: a list of { code, kind, value, label?, stackable?, category?, min_spend?,// cap?, expires? }: kind "percent" or "fixed"; value the percent or the amount;// stackable whether it goes with other stackable coupons (false); category the only// lines it applies to (all of them); min_spend what must be spent on those lines;// cap the most a percentage may take; expires its last day ("YYYY-MM-DD"). Codes are// matched without regard to case; the first of a repeated code counts. order: "best"// tries every order (the default), "percent_first", "fixed_first" or "listed" keep to// one. basis: what min_spend is held against: "before" the cart as it is (the default)// or "after" what is left when the coupon's turn comes. today: a prop, not the clock,// so server and browser agree; without it nothing counts as expired. currency: ISO// code ("USD"). locale: how to write amounts and dates (the program's). title: the// heading ("Best coupon"). id: a prefix for its ids ("best-coupon"), unique on the page.// name: the hidden field with the winning codes, joined by commas ("coupons"), so a// form around it sends them; each coupon's box is named `<name>_offered`.// on_change({ codes, total, saving }): inside one of your components, after a box// changes; amounts are decimals.//// At most 6 stacking coupons are tried together, those saving the most alone, which// is under 2,000 orders, and it says so past that. best_coupon_model(cart, coupons,// currency, order, basis, today) and best_coupon_solve(model, off) are the logic,// for a program to check an order the same way: off lists codes to leave out, and// the answer is { subtotal, total, saving, steps, rows, order_gain }.import ui/theme.qwe use ui_clslet best_coupon_css = css`.ui-bestcoupon { display: grid; gap: 14px; min-width: 0; max-width: 640px; padding: 20px 22px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-bestcoupon .ui-bestcoupon-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-bestcoupon-out { display: grid; gap: 8px; padding: 12px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-bestcoupon-steps { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }.ui-bestcoupon-steps li { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; }.ui-bestcoupon-steps span:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-bestcoupon-total { display: flex; justify-content: space-between; gap: 12px; padding-top: 6px; border-top: 1px solid var(--ui-border); font-size: 16px; font-weight: 700; }.ui-bestcoupon-total span:last-child { color: var(--ui-accent); font-variant-numeric: tabular-nums; }.ui-bestcoupon-say { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-bestcoupon-win { margin: 0; font-weight: 600; color: var(--ui-success); }.ui-bestcoupon-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-bestcoupon-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 10px; align-items: start; padding: 8px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); }.ui-bestcoupon-row:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-bestcoupon-row input { margin: 3px 0 0; accent-color: var(--ui-accent); }.ui-bestcoupon-used { border-color: var(--ui-success); background: var(--ui-success-soft); }.ui-bestcoupon-name { font-weight: 600; overflow-wrap: anywhere; }.ui-bestcoupon-what { color: var(--ui-muted); font-weight: 400; }.ui-bestcoupon-why { grid-column: 2; color: var(--ui-muted); font-size: 12px; }.ui-bestcoupon-used .ui-bestcoupon-why { color: var(--ui-success); }.ui-bestcoupon-problems { margin: 0; padding: 8px 12px 8px 26px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft); color: var(--ui-warning); font-size: 12px; }@media (max-width: 420px) { .ui-bestcoupon { padding: 16px; } }`let best_coupon_limit = 6// An amount of 0 or more as a decimal, or null.fn best_coupon_read(x) {    let t = trim(str(x ?? ""))    // Nine digits, by at most 10,000 of a line and 500 lines, stays inside what money holds.    if !re.test(t, "^[0-9]{1,9}(\\.[0-9]{1,8})?$") { return null }    dec(t)}fn best_coupon_key(s) -> str = lower(trim(str(s ?? "")))// One coupon as data, or the sentence saying why it cannot be used.fn best_coupon_coupon(c, minor: int, i: int) {    let code = upper(trim(str(c.code ?? "")))    if code == "" { return err("has no code") }    let value = best_coupon_read(c.value)    if c.kind != "percent" && c.kind != "fixed" { return err(`has the kind "${str(c.kind)}", not percent or fixed`) }    if value == null { return err("has no value it can read") }    if c.kind == "percent" && value > 100 { return err("takes more than 100%") }    if c.expires != null && !date.valid(str(c.expires), "date") { return err("has an expiry that is not a date") }    let needs = c.min_spend == null ? 0m : best_coupon_read(c.min_spend)    let cap = c.cap == null ? null : best_coupon_read(c.cap)    if needs == null || (c.cap != null && cap == null) { return err("has a minimum or cap it cannot read") }    { code, at: i, label: filled(str(c.label ?? "")) ?? code, kind: c.kind, value: c.kind == "fixed" ? round(value, minor, "half_up") : value, stackable: c.stackable == true,      category: c.category == null ? null : best_coupon_key(c.category), category_name: str(c.category ?? ""), needs: round(needs, minor, "half_up"), cap, expires: c.expires }}// The cart and coupons as data it can trust. Lines of one category are one bucket:// a coupon can only tell categories apart, so that is all a step has to keep.fn best_coupon_model(cart, coupons, currency: str, order: str = "best", basis: str = "before", today: str = "") {    let problems = []    let asked = upper(trim(currency))    let known = !is_err(money.minor_units(asked))    let code = known ? asked : "USD"    if !known { problems = problems | push(`The currency "${currency}" is not known, so USD is used.`) }    let minor = money.minor_units(code)    let buckets = []    for l in type_of(cart) == "list" ? take(cart, 500) : [] {        let price = type_of(l) == "map" ? best_coupon_read(l.price) : null        let qty = type_of(l) == "map" ? (l.qty ?? 1) : 1        if price == null || type_of(qty) != "int" || qty < 1 || qty > 10000 {            problems = problems | push(`${type_of(l) == "map" ? str(l.name ?? "A line") : "A line"} has no price or quantity it can read, so it is left out.`)        } else {            let cat = best_coupon_key(l.category)            let amount = round(price, minor, "half_up") * qty            let found = buckets | find((b) => b.cat == cat)            buckets = found != null ? buckets | map((b) => b.cat == cat ? merge(b, { start: b.start + amount, net: b.net + amount }) : b)                                    : buckets | push({ cat, name: str(l.category ?? ""), start: amount, net: amount })        }    }    let good = []    let wrong = []    let seen = []    for p in enumerate(type_of(coupons) == "list" ? take(coupons, 100) : []) {        let c = p[1]        if type_of(c) != "map" {            problems = problems | push("A coupon that is not a coupon is left out.")        } else {            let read = best_coupon_coupon(c, minor, p[0])            let shown = upper(trim(str(c.code ?? "")))            if !is_err(read) && contains(seen, read.code) {                problems = problems | push(`The code ${read.code} is given twice: the second is left out.`)            } else if is_err(read) {                wrong = wrong | push({ code: shown != "" ? shown : "A coupon", why: read.error })            } else {                seen = seen | push(read.code)                good = good | push(read)            }        }    }    { buckets, coupons: good, wrong, problems, code, minor, order, basis, today, subtotal: sum(buckets | map((b) => b.start)) + 0m }}// One coupon on what is left: { off, buckets, fail, spend }, with fail "none" or "min"// when it takes nothing. Shares follow what each bucket still holds, so they cannot// go below 0, and money.allocate loses no cent between them.fn best_coupon_apply(model, buckets, c) {    let hit = (b) => c.category == null || b.cat == c.category    let base = sum(buckets | filter(hit) | map((b) => b.net)) + 0m    let spend = model.basis == "after" ? base : sum(buckets | filter(hit) | map((b) => b.start)) + 0m    if base == 0 { return { off: 0m, buckets, fail: "none", spend } }    if spend < c.needs { return { off: 0m, buckets, fail: "min", spend } }    let raw = c.kind == "percent" ? round(base * c.value / 100, model.minor, "half_up") : c.value > base ? base : c.value    let off = c.cap != null && raw > c.cap ? round(c.cap, model.minor, "half_up") : raw    if off == 0 { return { off, buckets, fail: "zero", spend } }    let open = buckets | filter((b) => hit(b) && b.net > 0)    let shares = money.allocate(money.of(off, model.code), open | map((b) => b.net))    let taken = reduce(enumerate(open), {}, (m, p) => set(m, p[1].cat, shares[p[0]].amount))    { off, fail: null, spend, buckets: buckets | map((b) => has(taken, b.cat) ? merge(b, { net: b.net - taken[b.cat] }) : b) }}fn best_coupon_left(buckets) = sum(buckets | map((b) => b.net)) + 0m// The lower total, the fewer coupons on a tie, the earlier found after that.fn best_coupon_better(a, b) {    if a == null { return b }    if b == null { return a }    if b.total < a.total || (b.total == a.total && len(b.steps) < len(a.steps)) { return b }    a}// The best of what follows, trying each coupon next: in any order, or in the one// `rest` is in when the shop sets it.fn best_coupon_walk(model, buckets, steps, rest, any_order: bool) {    let best = null    for i in range(len(rest)) {        let c = rest[i]        let s = best_coupon_apply(model, buckets, c)        if s.fail == null {            let total = best_coupon_left(s.buckets)            let done = steps + [{ code: c.code, label: c.label, off: s.off, left: total }]            let tail = any_order ? slice(rest, 0, i) + slice(rest, i + 1) : slice(rest, i + 1)            best = best_coupon_better(best, best_coupon_better({ steps: done, total }, best_coupon_walk(model, s.buckets, done, tail, any_order)))        }    }    best}// The coupons in the order the shop set.fn best_coupon_ordered(model, list) {    if model.order == "percent_first" { return sort_by(list, (c) => c.kind == "percent" ? 0 : 1) }    if model.order == "fixed_first" { return sort_by(list, (c) => c.kind == "fixed" ? 0 : 1) }    list}// The total of these coupons in the order given, or null when one takes nothing.fn best_coupon_run(model, codes) {    let now = model.buckets    for code in codes {        let s = best_coupon_apply(model, now, model.coupons | find((c) => c.code == code))        if s.fail != null { return null }        now = s.buckets    }    best_coupon_left(now)}// See the top of the file.fn best_coupon_solve(model, off) {    let on = model.coupons | filter((c) => !contains(off, c.code))    let dead = (c) => model.today != "" && c.expires != null && c.expires < model.today    let live = on | filter((c) => !dead(c))    let solo = reduce(live, {}, (m, c) => set(m, c.code, best_coupon_apply(model, model.buckets, c)))    let works = live | filter((c) => solo[c.code].fail == null)    let stack_all = works | filter((c) => c.stackable)    // The most that are tried together are those saving the most alone.    let stack = take(reverse(sort_by(reverse(stack_all), (c) => solo[c.code].off)), best_coupon_limit)    let any_order = model.order != "percent_first" && model.order != "fixed_first" && model.order != "listed"    let pool = best_coupon_ordered(model, sort_by(stack, (c) => c.at))    let stacked = best_coupon_walk(model, model.buckets, [], pool, any_order)    let alone = reduce(works | filter((c) => !c.stackable), null, (b, c) => best_coupon_better(b, {        total: best_coupon_left(solo[c.code].buckets), steps: [{ code: c.code, label: c.label, off: solo[c.code].off, left: best_coupon_left(solo[c.code].buckets) }] }))    let win = best_coupon_better(stacked, alone)    let steps = win != null ? win.steps : []    let total = win != null ? win.total : model.subtotal    let used = steps | map((s) => s.code)    // What the order was worth: the same coupons the other way round.    let flipped = any_order && len(used) > 1 ? best_coupon_run(model, reverse(used)) : null    let row = (c) => {        if contains(used, c.code) { return { code: c.code, label: c.label, used: true, why: null } }        let lost = (why) => ({ code: c.code, label: c.label, used: false, why })        if contains(off, c.code) { return lost({ kind: "off" }) }        if dead(c) { return lost({ kind: "expired", on: c.expires }) }        let s = solo[c.code]        if s.fail == "none" { return lost({ kind: "none", category: c.category_name }) }        if s.fail == "min" { return lost({ kind: "min", needs: c.needs, spend: s.spend, category: c.category_name }) }        if s.fail == "zero" { return lost({ kind: "nothing" }) }        if !c.stackable { return lost({ kind: "alone", saves: s.off, behind: s.off < model.subtotal - total ? model.subtotal - total - s.off : 0m }) }        if !contains(stack | map((x) => x.code), c.code) { return lost({ kind: "past_limit" }) }        if stacked != null && win.steps == stacked.steps {            let tried = reduce(steps, model.buckets, (b, st) => best_coupon_apply(model, b, model.coupons | find((x) => x.code == st.code)).buckets)            let last_go = best_coupon_apply(model, tried, c)            return lost(last_go.fail == "min" ? { kind: "late", needs: c.needs, spend: last_go.spend, category: c.category_name } : { kind: "adds_nothing" })        }        lost({ kind: "stack", saves: stacked != null ? model.subtotal - stacked.total : s.off, behind: stacked != null ? stacked.total - total : model.subtotal - total - s.off })    }    { subtotal: model.subtotal, total, saving: model.subtotal - total, steps, order_gain: flipped != null && flipped > total ? flipped - total : 0m,      rows: (steps | map((s) => row(on | find((c) => c.code == s.code)))) + (model.coupons | filter((c) => !contains(used, c.code)) | map(row)) }}component BestCoupon(cart = [], coupons = [], order: str = "best", basis: str = "before", today: str = "", currency: str = "USD", locale: str? = null,                     title: str = "Best coupon", id: str = "best-coupon", name: str = "coupons", on_change = null, class: str? = null) {    style global best_coupon_css    state off = []    let model = best_coupon_model(cart, coupons, currency, order, basis, today)    let shown = (x) => {        let m = money.of(x, model.code, "half_up")        is_err(m) ? "a very large amount" : locale != null ? money.format(m, locale) : money.format(m)    }    let when = (d) => date.format(str(d), { locale: locale ?? "en-US", style: "medium" })    let r = best_coupon_solve(model, off)    let of_cat = (cat) => cat != "" ? ` of ${cat}` : ""    let why = (w) => {        if w.kind == "off" { return "Switched off." }        if w.kind == "expired" { return `Expired on ${when(w.on)}.` }        if w.kind == "none" { return w.category != "" ? `Nothing in ${w.category} in this cart.` : "Nothing in the cart to take it off." }        if w.kind == "min" { return `Needs ${shown(w.needs)}${of_cat(w.category)}, and this cart has ${shown(w.spend)}: ${shown(w.needs - w.spend)} short.` }        if w.kind == "late" { return `Needs ${shown(w.needs)}${of_cat(w.category)} when its turn comes, and only ${shown(w.spend)} is left by then.` }        if w.kind == "nothing" { return "Takes nothing off this cart." }        if w.kind == "past_limit" { return `Not tried: only the ${best_coupon_limit} stacking coupons that save the most alone go together.` }        if w.kind == "adds_nothing" { return "Adds nothing on top of the winner." }        if w.kind == "alone" { return w.behind > 0 ? `Cannot be combined. Alone it saves ${shown(w.saves)}, ${shown(w.behind)} less than the winner.` : `Cannot be combined. Alone it saves ${shown(w.saves)}, the same as the winner with fewer coupons kept back.` }        `Only goes with other stacking coupons, which together save ${shown(w.saves)}, ${shown(w.behind)} less than the winner.`    }    let describe = (c) => {        let x = model.coupons | find((k) => k.code == c.code)        x.kind == "percent" ? `${str(x.value)}% off` : `${shown(x.value)} off`    }    let toggle = (code: str, on: bool) => {        let next = on ? off | filter((c) => c != code) : off + [code]        off = next        if on_change != null {            let g = best_coupon_solve(model, next)            on_change({ codes: g.steps | map((s) => s.code), total: g.total, saving: g.saving })        }    }    <section class={ui_cls(["ui-bestcoupon", class])} aria-labelledby={`${id}-title`}>        <h3.ui-bestcoupon-title id={`${id}-title`}>{title}</h3>        <input type="hidden" name={name} value={r.steps | map((s) => s.code) | join(",")}/>        {len(model.problems) > 0 && <ul.ui-bestcoupon-problems>{for p in model.problems => <li>{p}</li>}</ul>}        <div.ui-bestcoupon-out aria-live="polite">            {len(model.buckets) == 0 && <p.ui-bestcoupon-say>The cart is empty, so there is nothing to save on.</p>}            {len(model.buckets) > 0 && len(r.steps) > 0 && <p.ui-bestcoupon-win>Best: {r.steps | map((s) => s.label) | join(" then ")} saves {shown(r.saving)}.</p>}            {len(model.buckets) > 0 && len(r.steps) == 0 && <p.ui-bestcoupon-say>No coupon takes anything off this cart.</p>}            <ul.ui-bestcoupon-steps>                <li><span>Cart</span><span>{shown(r.subtotal)}</span></li>                {for s in r.steps => <li key={s.code}><span>{s.label}</span><span><span aria-hidden="true">−</span><span.ui-sr>minus </span>{shown(s.off)}</span></li>}            </ul>            <div.ui-bestcoupon-total><span>To pay</span><span>{shown(r.total)}</span></div>            {r.order_gain > 0 && <p.ui-bestcoupon-say>In this order it saves {shown(r.order_gain)} more than the other way round.</p>}            {r.order_gain == 0 && len(r.steps) > 1 && order != "best" && <p.ui-bestcoupon-say>In the shop's order.</p>}        </div>        <ul.ui-bestcoupon-list aria-label="Coupons on offer">            {for c in r.rows => <li key={c.code} class={ui_cls(["ui-bestcoupon-row", c.used && "ui-bestcoupon-used"])}>                <input type="checkbox" id={`${id}-${c.code}`} name={`${name}_offered`} value={c.code} checked={!contains(off, c.code)} aria-describedby={`${id}-${c.code}-why`}                       @change={(e) => { toggle(c.code, e.checked) }}/>                <label.ui-bestcoupon-name for={`${id}-${c.code}`}>{c.label} <span.ui-bestcoupon-what>{describe(c)}</span></label>                <span.ui-bestcoupon-why id={`${id}-${c.code}-why`}>{c.used ? `Used: takes off ${shown((r.steps | find((s) => s.code == c.code)).off)}.` : why(c.why)}</span>            </li>}            {for w in model.wrong => <li class="ui-bestcoupon-row" key={`bad-${w.code}`}>                <span></span><span.ui-bestcoupon-name>{w.code}</span>                <span.ui-bestcoupon-why>Cannot be used: it {w.why}.</span>            </li>}        </ul>    </section>}
Thread
37Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.