carlos / carloku Public

Pricing page: the billing country sets the band shown, and the signup links carry it

Each country option carries its band from the price book. Choosing a
country prices the plan cards at that band in the chosen currency (a
band D country with GBP chosen stays in pounds at band D) and scales the
per-member inclusions by the platform's own rule, exact figures only;
the overage rates and the band table do not change. The signup links
carry ?country= beside ?currency=, and a link back preselects both.
Without the script the page is the default currency, USD, at band A,
which is what the controls show selected. The approved strings are the
band A EUR texts; every other text is the same words with the book's
figure, and the build stops if band A EUR differs from the approval.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 15317d272b74ada70e39cca4454b159a6cba4be6 parent b4e5f20
5 files changed, +256 −72
  • copy-review/pricing/build.mjs +64 −13
  • src/pricing/index.html +23 −23
  • src/site.js +41 −5
  • test/pricing-currency.test.mjs +66 −9
  • test/pricing-pricebook.test.mjs +62 −22
diff --git a/copy-review/pricing/build.mjs b/copy-review/pricing/build.mjs
index b016868..310a37e 100644
--- a/copy-review/pricing/build.mjs
+++ b/copy-review/pricing/build.mjs
@@ -42,16 +42,61 @@ const swapAmount = (text, cur, amount) => {
if (!/€[0-9][0-9.,]*/.test(text)) throw new Error("no euro amount in " + JSON.stringify(text));
return text.replace(/€[0-9][0-9.,]*/, amount);
};
-// priced renders an approved string whose euro amount varies by currency:
-// the approved text as written (EUR), with the other currencies' texts in
-// data-currency-values for the selector.
+// The static text of every priced element is the book's default currency
+// (USD) at band A: what a visitor with no script sees, and what the
+// currency control shows selected before the script runs. The approved
+// strings are the EUR texts; every other currency's text is the same
+// words with the book's amount.
+const BANDS = ["A", "B", "C", "D"];
+const DEFAULT = book.default_currency.toUpperCase();
+// priced renders an approved string whose euro amount varies by currency
+// alone (an overage rate, the floor, the free plan's price): every
+// currency's text in data-currency-values for the selector.
const priced = (id, amountFor, tag = "span", cls = "") => {
if (!(id in S)) throw new Error("missing string " + id);
const by = Object.fromEntries(CURRENCIES.map((cur) => [cur, swapAmount(S[id], cur, amountFor(cur))]));
if (by.EUR !== S[id]) throw new Error("the book's EUR amount differs from the approved string " + id + ": " + by.EUR + " vs " + S[id]);
- return `<${tag}${cls ? ` class="${cls}"` : ""} data-copy="${id}" data-currency-values="${values(by)}">${esc(S[id])}</${tag}>`;
+ return `<${tag}${cls ? ` class="${cls}"` : ""} data-copy="${id}" data-currency-values="${values(by)}">${esc(by[DEFAULT])}</${tag}>`;
+};
+// banded renders a string whose amount varies by band and currency (a
+// paid plan's price line): every band's texts, each by currency, in
+// data-band-values. The selector picks the band from the billing country.
+const banded = (id, textFor, tag = "span", cls = "") => {
+ if (!(id in S)) throw new Error("missing string " + id);
+ const by = Object.fromEntries(BANDS.map((band) => [band, Object.fromEntries(CURRENCIES.map((cur) => [cur, textFor(band, cur)]))]));
+ if (by.A.EUR !== S[id]) throw new Error("the book's band A EUR amount differs from the approved string " + id + ": " + by.A.EUR + " vs " + S[id]);
+ return `<${tag}${cls ? ` class="${cls}"` : ""} data-copy="${id}" data-band-values="${values(by)}">${esc(by.A[DEFAULT])}</${tag}>`;
+};
+const bandCell = (plan, band) => `<td data-currency-values="${values(Object.fromEntries(CURRENCIES.map((cur) => [cur, money(cur, unitMinor(plan, cur, band))])))}">${esc(money(DEFAULT, unitMinor(plan, DEFAULT, band)))}</td>`;
+// A band-scaled plan's per-member inclusions at a band are the plan's
+// figure times the band factor, divided by 10000 and rounded down: the
+// platform's own pooling rule (public-pricebook.json), at one member. The
+// approved string is band A's; the other bands' texts are the same words
+// with that band's figure, in data-band-values. A figure that is not exact
+// to three decimals stops the build rather than rounding on the page.
+const allowanceOf = {
+ personal: { i2: "awake_seconds", i3: "storage_byte_months", i4: "transfer_bytes" },
+ community: { i1: "awake_seconds", i2: "storage_byte_months", i3: "transfer_bytes" },
+ business: { i1: "awake_seconds", i2: "storage_byte_months", i3: "transfer_bytes", i4: "writes" },
+};
+const figure = (resource, amount) => {
+ const n = resource === "awake_seconds" ? amount / 3600 : resource === "writes" ? amount : amount / 1e9;
+ const text = n.toFixed(3).replace(/\.?0+$/, "");
+ if (Number(text) !== n) throw new Error("not an exact figure: " + resource + " " + amount);
+ const [int, frac] = text.split(".");
+ return int.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (frac ? "." + frac : "");
+};
+const swapFigure = (text, fig) => {
+ if (!/^[0-9][0-9,]*(\.[0-9]+)?/.test(text)) throw new Error("no leading figure in " + JSON.stringify(text));
+ return text.replace(/^[0-9][0-9,]*(\.[0-9]+)?/, fig);
+};
+const allowanceLi = (p, i) => {
+ const id = `plan.${p}.${i}`, plan = plansByKey[p], resource = (allowanceOf[p] || {})[i];
+ if (!resource || !plan.band_scaled) return li(id);
+ const by = Object.fromEntries(BANDS.map((band) => [band, swapFigure(S[id], figure(resource, Math.floor(plan.resources_per_member[resource] * book.band_factors_bps[band] / 10000)))]));
+ if (by.A !== S[id]) throw new Error("band A's figure differs from the approved string " + id + ": " + by.A + " vs " + S[id]);
+ return ` <li data-copy="${id}" data-band-values="${values(by)}">${esc(S[id])}</li>`;
};
-const bandCell = (plan, band) => `<td data-currency-values="${values(Object.fromEntries(CURRENCIES.map((cur) => [cur, money(cur, unitMinor(plan, cur, band))])))}">${esc(money("EUR", unitMinor(plan, "EUR", band)))}</td>`;
const overageRate = (meter) => (cur) => rate(cur, book.overage[cur.toLowerCase()][meter]);
const defaultCurrencyFor = (iso2) => (book.country_currency[iso2] || book.default_currency).toUpperCase();
const esc = (t) => t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -78,13 +123,13 @@ const planCardLists = {
business: { allowances: ["i1", "i2", "i3", "i4"], features: ["i5", "i6"] },
};
const priceLine = (p) => {
- if (p === "free") return t(`plan.${p}.price`);
- const plan = plansByKey[p];
- return priced(`plan.${p}.price`, (cur) => money(cur, plan.annual_unit_minor[cur.toLowerCase()].A));
+ const id = `plan.${p}.price`;
+ if (p === "free") return priced(id, (cur) => money(cur, unitMinor(p, cur, "A")));
+ return banded(id, (band, cur) => swapAmount(S[id], cur, money(cur, unitMinor(p, cur, band))));
};
const card = (p, featured) => {
const lists = planCardLists[p];
- const items = (ids) => ids.map((i) => li(`plan.${p}.${i}`)).join("\n");
+ const items = (ids) => ids.map((i) => allowanceLi(p, i)).join("\n");
const seen = new Set([...(lists.plain || []), ...(lists.allowances || []), ...(lists.features || [])].map((i) => `plan.${p}.${i}`));
for (const k of Object.keys(S).filter((k) => k.startsWith(`plan.${p}.i`))) if (!seen.has(k)) throw new Error("unplaced string " + k);
return ` <article class="plan${featured ? " featured" : ""}">
@@ -97,10 +142,16 @@ ${lists.plain ? ` <ul>\n${items(lists.plain)}\n </ul>\n` : ""}${li
// The currency and billing-country controls, in the approved words. The
// country list is every country the book sells to, with its default
-// currency; the selector script (site.js) applies the choice to every
-// priced string on the page and to the signup links. No price is charged
-// from this page: checkout prices from the same book.
-const countryOptions = recs.map((r) => `<option value="${r.iso2}" data-currency="${defaultCurrencyFor(r.iso2)}">${esc(r.name)}</option>`).join("");
+// currency and its band; the selector script (site.js) applies the
+// currency to every priced string on the page, the country's band to every
+// banded one, and both to the signup links. No price is charged from this
+// page: checkout prices from the same book, for the country the buyer
+// gives there.
+const bandOf = (iso2) => {
+ if (!BANDS.includes(book.country_band[iso2])) throw new Error("the book has no band for " + iso2);
+ return book.country_band[iso2];
+};
+const countryOptions = recs.map((r) => `<option value="${r.iso2}" data-currency="${defaultCurrencyFor(r.iso2)}" data-band="${bandOf(r.iso2)}">${esc(r.name)}</option>`).join("");
const controls = ` <section class="pricing-controls">
<div class="wrap narrow">
<form class="currency-form" onsubmit="return false">
diff --git a/src/pricing/index.html b/src/pricing/index.html
index 84cc146..dd34b68 100644
--- a/src/pricing/index.html
+++ b/src/pricing/index.html
@@ -48,7 +48,7 @@
</div>
<div class="control">
<label for="pricing-country">Billing country</label>
- <select id="pricing-country" data-pricing-country><option value=""></option><option value="AF" data-currency="USD">Afghanistan</option><option value="AL" data-currency="USD">Albania</option><option value="DZ" data-currency="USD">Algeria</option><option value="AD" data-currency="USD">Andorra</option><option value="AO" data-currency="USD">Angola</option><option value="AG" data-currency="USD">Antigua and Barbuda</option><option value="AR" data-currency="USD">Argentina</option><option value="AM" data-currency="USD">Armenia</option><option value="AW" data-currency="USD">Aruba</option><option value="AU" data-currency="USD">Australia</option><option value="AT" data-currency="EUR">Austria</option><option value="AZ" data-currency="USD">Azerbaijan</option><option value="BS" data-currency="USD">Bahamas, The</option><option value="BH" data-currency="USD">Bahrain</option><option value="BD" data-currency="USD">Bangladesh</option><option value="BB" data-currency="USD">Barbados</option><option value="BY" data-currency="USD">Belarus</option><option value="BE" data-currency="EUR">Belgium</option><option value="BZ" data-currency="USD">Belize</option><option value="BJ" data-currency="USD">Benin</option><option value="BM" data-currency="USD">Bermuda</option><option value="BT" data-currency="USD">Bhutan</option><option value="BO" data-currency="USD">Bolivia</option><option value="BA" data-currency="USD">Bosnia and Herzegovina</option><option value="BW" data-currency="USD">Botswana</option><option value="BR" data-currency="USD">Brazil</option><option value="BN" data-currency="USD">Brunei Darussalam</option><option value="BG" data-currency="EUR">Bulgaria</option><option value="BF" data-currency="USD">Burkina Faso</option><option value="BI" data-currency="USD">Burundi</option><option value="CV" data-currency="USD">Cabo Verde</option><option value="KH" data-currency="USD">Cambodia</option><option value="CM" data-currency="USD">Cameroon</option><option value="CA" data-currency="USD">Canada</option><option value="KY" data-currency="USD">Cayman Islands</option><option value="CF" data-currency="USD">Central African Republic</option><option value="TD" data-currency="USD">Chad</option><option value="CL" data-currency="USD">Chile</option><option value="CN" data-currency="USD">China</option><option value="CO" data-currency="USD">Colombia</option><option value="KM" data-currency="USD">Comoros</option><option value="CD" data-currency="USD">Congo, Dem. Rep.</option><option value="CG" data-currency="USD">Congo, Rep.</option><option value="CR" data-currency="USD">Costa Rica</option><option value="CI" data-currency="USD">Cote d'Ivoire</option><option value="HR" data-currency="EUR">Croatia</option><option value="CW" data-currency="USD">Curacao</option><option value="CY" data-currency="EUR">Cyprus</option><option value="CZ" data-currency="EUR">Czechia</option><option value="DK" data-currency="EUR">Denmark</option><option value="DJ" data-currency="USD">Djibouti</option><option value="DM" data-currency="USD">Dominica</option><option value="DO" data-currency="USD">Dominican Republic</option><option value="EC" data-currency="USD">Ecuador</option><option value="EG" data-currency="USD">Egypt, Arab Rep.</option><option value="SV" data-currency="USD">El Salvador</option><option value="GQ" data-currency="USD">Equatorial Guinea</option><option value="EE" data-currency="EUR">Estonia</option><option value="SZ" data-currency="USD">Eswatini</option><option value="ET" data-currency="USD">Ethiopia</option><option value="FO" data-currency="USD">Faroe Islands</option><option value="FJ" data-currency="USD">Fiji</option><option value="FI" data-currency="EUR">Finland</option><option value="FR" data-currency="EUR">France</option><option value="GA" data-currency="USD">Gabon</option><option value="GM" data-currency="USD">Gambia, The</option><option value="GE" data-currency="USD">Georgia</option><option value="DE" data-currency="EUR">Germany</option><option value="GH" data-currency="USD">Ghana</option><option value="GR" data-currency="EUR">Greece</option><option value="GL" data-currency="USD">Greenland</option><option value="GD" data-currency="USD">Grenada</option><option value="GT" data-currency="USD">Guatemala</option><option value="GN" data-currency="USD">Guinea</option><option value="GW" data-currency="USD">Guinea-Bissau</option><option value="GY" data-currency="USD">Guyana</option><option value="HT" data-currency="USD">Haiti</option><option value="HN" data-currency="USD">Honduras</option><option value="HK" data-currency="USD">Hong Kong SAR, China</option><option value="HU" data-currency="EUR">Hungary</option><option value="IS" data-currency="USD">Iceland</option><option value="IN" data-currency="USD">India</option><option value="ID" data-currency="USD">Indonesia</option><option value="IR" data-currency="USD">Iran, Islamic Rep.</option><option value="IQ" data-currency="USD">Iraq</option><option value="IE" data-currency="EUR">Ireland</option><option value="IL" data-currency="USD">Israel</option><option value="IT" data-currency="EUR">Italy</option><option value="JM" data-currency="USD">Jamaica</option><option value="JP" data-currency="USD">Japan</option><option value="JO" data-currency="USD">Jordan</option><option value="KZ" data-currency="USD">Kazakhstan</option><option value="KE" data-currency="USD">Kenya</option><option value="KI" data-currency="USD">Kiribati</option><option value="KR" data-currency="USD">Korea, Rep.</option><option value="XK" data-currency="USD">Kosovo</option><option value="KW" data-currency="USD">Kuwait</option><option value="KG" data-currency="USD">Kyrgyz Republic</option><option value="LA" data-currency="USD">Lao PDR</option><option value="LV" data-currency="EUR">Latvia</option><option value="LB" data-currency="USD">Lebanon</option><option value="LS" data-currency="USD">Lesotho</option><option value="LR" data-currency="USD">Liberia</option><option value="LY" data-currency="USD">Libya</option><option value="LT" data-currency="EUR">Lithuania</option><option value="LU" data-currency="EUR">Luxembourg</option><option value="MO" data-currency="USD">Macao SAR, China</option><option value="MG" data-currency="USD">Madagascar</option><option value="MW" data-currency="USD">Malawi</option><option value="MY" data-currency="USD">Malaysia</option><option value="MV" data-currency="USD">Maldives</option><option value="ML" data-currency="USD">Mali</option><option value="MT" data-currency="EUR">Malta</option><option value="MH" data-currency="USD">Marshall Islands</option><option value="MR" data-currency="USD">Mauritania</option><option value="MU" data-currency="USD">Mauritius</option><option value="MX" data-currency="USD">Mexico</option><option value="FM" data-currency="USD">Micronesia, Fed. Sts.</option><option value="MD" data-currency="USD">Moldova</option><option value="MN" data-currency="USD">Mongolia</option><option value="ME" data-currency="USD">Montenegro</option><option value="MA" data-currency="USD">Morocco</option><option value="MZ" data-currency="USD">Mozambique</option><option value="MM" data-currency="USD">Myanmar</option><option value="NA" data-currency="USD">Namibia</option><option value="NR" data-currency="USD">Naoero</option><option value="NP" data-currency="USD">Nepal</option><option value="NL" data-currency="EUR">Netherlands</option><option value="NZ" data-currency="USD">New Zealand</option><option value="NI" data-currency="USD">Nicaragua</option><option value="NE" data-currency="USD">Niger</option><option value="NG" data-currency="USD">Nigeria</option><option value="MK" data-currency="USD">North Macedonia</option><option value="NO" data-currency="USD">Norway</option><option value="OM" data-currency="USD">Oman</option><option value="PK" data-currency="USD">Pakistan</option><option value="PW" data-currency="USD">Palau</option><option value="PA" data-currency="USD">Panama</option><option value="PG" data-currency="USD">Papua New Guinea</option><option value="PY" data-currency="USD">Paraguay</option><option value="PE" data-currency="USD">Peru</option><option value="PH" data-currency="USD">Philippines</option><option value="PL" data-currency="EUR">Poland</option><option value="PT" data-currency="EUR">Portugal</option><option value="PR" data-currency="USD">Puerto Rico (US)</option><option value="QA" data-currency="USD">Qatar</option><option value="RO" data-currency="EUR">Romania</option><option value="RU" data-currency="USD">Russian Federation</option><option value="RW" data-currency="USD">Rwanda</option><option value="WS" data-currency="USD">Samoa</option><option value="SM" data-currency="USD">San Marino</option><option value="ST" data-currency="USD">Sao Tome and Principe</option><option value="SA" data-currency="USD">Saudi Arabia</option><option value="SN" data-currency="USD">Senegal</option><option value="RS" data-currency="USD">Serbia</option><option value="SC" data-currency="USD">Seychelles</option><option value="SL" data-currency="USD">Sierra Leone</option><option value="SG" data-currency="USD">Singapore</option><option value="SX" data-currency="USD">Sint Maarten (Dutch part)</option><option value="SK" data-currency="EUR">Slovak Republic</option><option value="SI" data-currency="EUR">Slovenia</option><option value="SB" data-currency="USD">Solomon Islands</option><option value="SO" data-currency="USD">Somalia, Fed. Rep.</option><option value="ZA" data-currency="USD">South Africa</option><option value="ES" data-currency="EUR">Spain</option><option value="LK" data-currency="USD">Sri Lanka</option><option value="KN" data-currency="USD">St. Kitts and Nevis</option><option value="LC" data-currency="USD">St. Lucia</option><option value="VC" data-currency="USD">St. Vincent and the Grenadines</option><option value="SD" data-currency="USD">Sudan</option><option value="SR" data-currency="USD">Suriname</option><option value="SE" data-currency="EUR">Sweden</option><option value="CH" data-currency="USD">Switzerland</option><option value="SY" data-currency="USD">Syrian Arab Republic</option><option value="TJ" data-currency="USD">Tajikistan</option><option value="TZ" data-currency="USD">Tanzania</option><option value="TH" data-currency="USD">Thailand</option><option value="TL" data-currency="USD">Timor-Leste</option><option value="TG" data-currency="USD">Togo</option><option value="TO" data-currency="USD">Tonga</option><option value="TT" data-currency="USD">Trinidad and Tobago</option><option value="TN" data-currency="USD">Tunisia</option><option value="TR" data-currency="USD">Turkiye</option><option value="TM" data-currency="USD">Turkmenistan</option><option value="TC" data-currency="USD">Turks and Caicos Islands</option><option value="TV" data-currency="USD">Tuvalu</option><option value="UG" data-currency="USD">Uganda</option><option value="UA" data-currency="USD">Ukraine</option><option value="AE" data-currency="USD">United Arab Emirates</option><option value="GB" data-currency="GBP">United Kingdom</option><option value="US" data-currency="USD">United States</option><option value="UY" data-currency="USD">Uruguay</option><option value="UZ" data-currency="USD">Uzbekistan</option><option value="VU" data-currency="USD">Vanuatu</option><option value="VN" data-currency="USD">Viet Nam</option><option value="VI" data-currency="USD">Virgin Islands (U.S.)</option><option value="PS" data-currency="USD">West Bank and Gaza</option><option value="ZM" data-currency="USD">Zambia</option><option value="ZW" data-currency="USD">Zimbabwe</option></select>
+ <select id="pricing-country" data-pricing-country><option value=""></option><option value="AF" data-currency="USD" data-band="D">Afghanistan</option><option value="AL" data-currency="USD" data-band="C">Albania</option><option value="DZ" data-currency="USD" data-band="D">Algeria</option><option value="AD" data-currency="USD" data-band="B">Andorra</option><option value="AO" data-currency="USD" data-band="D">Angola</option><option value="AG" data-currency="USD" data-band="B">Antigua and Barbuda</option><option value="AR" data-currency="USD" data-band="C">Argentina</option><option value="AM" data-currency="USD" data-band="D">Armenia</option><option value="AW" data-currency="USD" data-band="B">Aruba</option><option value="AU" data-currency="USD" data-band="A">Australia</option><option value="AT" data-currency="EUR" data-band="A">Austria</option><option value="AZ" data-currency="USD" data-band="D">Azerbaijan</option><option value="BS" data-currency="USD" data-band="A">Bahamas, The</option><option value="BH" data-currency="USD" data-band="C">Bahrain</option><option value="BD" data-currency="USD" data-band="D">Bangladesh</option><option value="BB" data-currency="USD" data-band="A">Barbados</option><option value="BY" data-currency="USD" data-band="D">Belarus</option><option value="BE" data-currency="EUR" data-band="A">Belgium</option><option value="BZ" data-currency="USD" data-band="C">Belize</option><option value="BJ" data-currency="USD" data-band="D">Benin</option><option value="BM" data-currency="USD" data-band="A">Bermuda</option><option value="BT" data-currency="USD" data-band="D">Bhutan</option><option value="BO" data-currency="USD" data-band="D">Bolivia</option><option value="BA" data-currency="USD" data-band="D">Bosnia and Herzegovina</option><option value="BW" data-currency="USD" data-band="D">Botswana</option><option value="BR" data-currency="USD" data-band="C">Brazil</option><option value="BN" data-currency="USD" data-band="D">Brunei Darussalam</option><option value="BG" data-currency="EUR" data-band="D">Bulgaria</option><option value="BF" data-currency="USD" data-band="D">Burkina Faso</option><option value="BI" data-currency="USD" data-band="D">Burundi</option><option value="CV" data-currency="USD" data-band="C">Cabo Verde</option><option value="KH" data-currency="USD" data-band="D">Cambodia</option><option value="CM" data-currency="USD" data-band="D">Cameroon</option><option value="CA" data-currency="USD" data-band="A">Canada</option><option value="KY" data-currency="USD" data-band="A">Cayman Islands</option><option value="CF" data-currency="USD" data-band="D">Central African Republic</option><option value="TD" data-currency="USD" data-band="D">Chad</option><option value="CL" data-currency="USD" data-band="C">Chile</option><option value="CN" data-currency="USD" data-band="C">China</option><option value="CO" data-currency="USD" data-band="D">Colombia</option><option value="KM" data-currency="USD" data-band="C">Comoros</option><option value="CD" data-currency="USD" data-band="D">Congo, Dem. Rep.</option><option value="CG" data-currency="USD" data-band="D">Congo, Rep.</option><option value="CR" data-currency="USD" data-band="C">Costa Rica</option><option value="CI" data-currency="USD" data-band="D">Cote d'Ivoire</option><option value="HR" data-currency="EUR" data-band="C">Croatia</option><option value="CW" data-currency="USD" data-band="B">Curacao</option><option value="CY" data-currency="EUR" data-band="C">Cyprus</option><option value="CZ" data-currency="EUR" data-band="C">Czechia</option><option value="DK" data-currency="EUR" data-band="A">Denmark</option><option value="DJ" data-currency="USD" data-band="C">Djibouti</option><option value="DM" data-currency="USD" data-band="C">Dominica</option><option value="DO" data-currency="USD" data-band="D">Dominican Republic</option><option value="EC" data-currency="USD" data-band="C">Ecuador</option><option value="EG" data-currency="USD" data-band="D">Egypt, Arab Rep.</option><option value="SV" data-currency="USD" data-band="D">El Salvador</option><option value="GQ" data-currency="USD" data-band="D">Equatorial Guinea</option><option value="EE" data-currency="EUR" data-band="C">Estonia</option><option value="SZ" data-currency="USD" data-band="D">Eswatini</option><option value="ET" data-currency="USD" data-band="D">Ethiopia</option><option value="FO" data-currency="USD" data-band="A">Faroe Islands</option><option value="FJ" data-currency="USD" data-band="D">Fiji</option><option value="FI" data-currency="EUR" data-band="A">Finland</option><option value="FR" data-currency="EUR" data-band="A">France</option><option value="GA" data-currency="USD" data-band="D">Gabon</option><option value="GM" data-currency="USD" data-band="D">Gambia, The</option><option value="GE" data-currency="USD" data-band="D">Georgia</option><option value="DE" data-currency="EUR" data-band="A">Germany</option><option value="GH" data-currency="USD" data-band="D">Ghana</option><option value="GR" data-currency="EUR" data-band="A">Greece</option><option value="GL" data-currency="USD" data-band="B">Greenland</option><option value="GD" data-currency="USD" data-band="C">Grenada</option><option value="GT" data-currency="USD" data-band="C">Guatemala</option><option value="GN" data-currency="USD" data-band="D">Guinea</option><option value="GW" data-currency="USD" data-band="D">Guinea-Bissau</option><option value="GY" data-currency="USD" data-band="D">Guyana</option><option value="HT" data-currency="USD" data-band="B">Haiti</option><option value="HN" data-currency="USD" data-band="C">Honduras</option><option value="HK" data-currency="USD" data-band="B">Hong Kong SAR, China</option><option value="HU" data-currency="EUR" data-band="C">Hungary</option><option value="IS" data-currency="USD" data-band="A">Iceland</option><option value="IN" data-currency="USD" data-band="D">India</option><option value="ID" data-currency="USD" data-band="D">Indonesia</option><option value="IR" data-currency="USD" data-band="D">Iran, Islamic Rep.</option><option value="IQ" data-currency="USD" data-band="D">Iraq</option><option value="IE" data-currency="EUR" data-band="A">Ireland</option><option value="IL" data-currency="USD" data-band="A">Israel</option><option value="IT" data-currency="EUR" data-band="A">Italy</option><option value="JM" data-currency="USD" data-band="C">Jamaica</option><option value="JP" data-currency="USD" data-band="C">Japan</option><option value="JO" data-currency="USD" data-band="C">Jordan</option><option value="KZ" data-currency="USD" data-band="D">Kazakhstan</option><option value="KE" data-currency="USD" data-band="D">Kenya</option><option value="KI" data-currency="USD" data-band="B">Kiribati</option><option value="KR" data-currency="USD" data-band="C">Korea, Rep.</option><option value="XK" data-currency="USD" data-band="D">Kosovo</option><option value="KW" data-currency="USD" data-band="C">Kuwait</option><option value="KG" data-currency="USD" data-band="D">Kyrgyz Republic</option><option value="LA" data-currency="USD" data-band="D">Lao PDR</option><option value="LV" data-currency="EUR" data-band="C">Latvia</option><option value="LB" data-currency="USD" data-band="D">Lebanon</option><option value="LS" data-currency="USD" data-band="D">Lesotho</option><option value="LR" data-currency="USD" data-band="C">Liberia</option><option value="LY" data-currency="USD" data-band="C">Libya</option><option value="LT" data-currency="EUR" data-band="C">Lithuania</option><option value="LU" data-currency="EUR" data-band="A">Luxembourg</option><option value="MO" data-currency="USD" data-band="C">Macao SAR, China</option><option value="MG" data-currency="USD" data-band="D">Madagascar</option><option value="MW" data-currency="USD" data-band="D">Malawi</option><option value="MY" data-currency="USD" data-band="D">Malaysia</option><option value="MV" data-currency="USD" data-band="C">Maldives</option><option value="ML" data-currency="USD" data-band="D">Mali</option><option value="MT" data-currency="EUR" data-band="B">Malta</option><option value="MH" data-currency="USD" data-band="A">Marshall Islands</option><option value="MR" data-currency="USD" data-band="D">Mauritania</option><option value="MU" data-currency="USD" data-band="D">Mauritius</option><option value="MX" data-currency="USD" data-band="C">Mexico</option><option value="FM" data-currency="USD" data-band="A">Micronesia, Fed. Sts.</option><option value="MD" data-currency="USD" data-band="D">Moldova</option><option value="MN" data-currency="USD" data-band="D">Mongolia</option><option value="ME" data-currency="USD" data-band="D">Montenegro</option><option value="MA" data-currency="USD" data-band="D">Morocco</option><option value="MZ" data-currency="USD" data-band="D">Mozambique</option><option value="MM" data-currency="USD" data-band="D">Myanmar</option><option value="NA" data-currency="USD" data-band="D">Namibia</option><option value="NR" data-currency="USD" data-band="A">Naoero</option><option value="NP" data-currency="USD" data-band="D">Nepal</option><option value="NL" data-currency="EUR" data-band="A">Netherlands</option><option value="NZ" data-currency="USD" data-band="A">New Zealand</option><option value="NI" data-currency="USD" data-band="D">Nicaragua</option><option value="NE" data-currency="USD" data-band="D">Niger</option><option value="NG" data-currency="USD" data-band="D">Nigeria</option><option value="MK" data-currency="USD" data-band="D">North Macedonia</option><option value="NO" data-currency="USD" data-band="B">Norway</option><option value="OM" data-currency="USD" data-band="C">Oman</option><option value="PK" data-currency="USD" data-band="D">Pakistan</option><option value="PW" data-currency="USD" data-band="B">Palau</option><option value="PA" data-currency="USD" data-band="C">Panama</option><option value="PG" data-currency="USD" data-band="C">Papua New Guinea</option><option value="PY" data-currency="USD" data-band="D">Paraguay</option><option value="PE" data-currency="USD" data-band="C">Peru</option><option value="PH" data-currency="USD" data-band="D">Philippines</option><option value="PL" data-currency="EUR" data-band="C">Poland</option><option value="PT" data-currency="EUR" data-band="A">Portugal</option><option value="PR" data-currency="USD" data-band="B">Puerto Rico (US)</option><option value="QA" data-currency="USD" data-band="C">Qatar</option><option value="RO" data-currency="EUR" data-band="D">Romania</option><option value="RU" data-currency="USD" data-band="D">Russian Federation</option><option value="RW" data-currency="USD" data-band="D">Rwanda</option><option value="WS" data-currency="USD" data-band="C">Samoa</option><option value="SM" data-currency="USD" data-band="B">San Marino</option><option value="ST" data-currency="USD" data-band="C">Sao Tome and Principe</option><option value="SA" data-currency="USD" data-band="C">Saudi Arabia</option><option value="SN" data-currency="USD" data-band="D">Senegal</option><option value="RS" data-currency="USD" data-band="D">Serbia</option><option value="SC" data-currency="USD" data-band="C">Seychelles</option><option value="SL" data-currency="USD" data-band="D">Sierra Leone</option><option value="SG" data-currency="USD" data-band="C">Singapore</option><option value="SX" data-currency="USD" data-band="B">Sint Maarten (Dutch part)</option><option value="SK" data-currency="EUR" data-band="C">Slovak Republic</option><option value="SI" data-currency="EUR" data-band="C">Slovenia</option><option value="SB" data-currency="USD" data-band="B">Solomon Islands</option><option value="SO" data-currency="USD" data-band="D">Somalia, Fed. Rep.</option><option value="ZA" data-currency="USD" data-band="D">South Africa</option><option value="ES" data-currency="EUR" data-band="A">Spain</option><option value="LK" data-currency="USD" data-band="D">Sri Lanka</option><option value="KN" data-currency="USD" data-band="B">St. Kitts and Nevis</option><option value="LC" data-currency="USD" data-band="C">St. Lucia</option><option value="VC" data-currency="USD" data-band="C">St. Vincent and the Grenadines</option><option value="SD" data-currency="USD" data-band="C">Sudan</option><option value="SR" data-currency="USD" data-band="D">Suriname</option><option value="SE" data-currency="EUR" data-band="A">Sweden</option><option value="CH" data-currency="USD" data-band="A">Switzerland</option><option value="SY" data-currency="USD" data-band="D">Syrian Arab Republic</option><option value="TJ" data-currency="USD" data-band="D">Tajikistan</option><option value="TZ" data-currency="USD" data-band="D">Tanzania</option><option value="TH" data-currency="USD" data-band="D">Thailand</option><option value="TL" data-currency="USD" data-band="D">Timor-Leste</option><option value="TG" data-currency="USD" data-band="D">Togo</option><option value="TO" data-currency="USD" data-band="B">Tonga</option><option value="TT" data-currency="USD" data-band="C">Trinidad and Tobago</option><option value="TN" data-currency="USD" data-band="D">Tunisia</option><option value="TR" data-currency="USD" data-band="D">Turkiye</option><option value="TM" data-currency="USD" data-band="D">Turkmenistan</option><option value="TC" data-currency="USD" data-band="A">Turks and Caicos Islands</option><option value="TV" data-currency="USD" data-band="A">Tuvalu</option><option value="UG" data-currency="USD" data-band="D">Uganda</option><option value="UA" data-currency="USD" data-band="D">Ukraine</option><option value="AE" data-currency="USD" data-band="B">United Arab Emirates</option><option value="GB" data-currency="GBP" data-band="A">United Kingdom</option><option value="US" data-currency="USD" data-band="A">United States</option><option value="UY" data-currency="USD" data-band="B">Uruguay</option><option value="UZ" data-currency="USD" data-band="D">Uzbekistan</option><option value="VU" data-currency="USD" data-band="A">Vanuatu</option><option value="VN" data-currency="USD" data-band="D">Viet Nam</option><option value="VI" data-currency="USD" data-band="A">Virgin Islands (U.S.)</option><option value="PS" data-currency="USD" data-band="B">West Bank and Gaza</option><option value="ZM" data-currency="USD" data-band="D">Zambia</option><option value="ZW" data-currency="USD" data-band="D">Zimbabwe</option></select>
<p class="control-note">Your billing country sets your pricing band. Changing currency does not change your inclusions.</p>
</div>
</form>
@@ -61,7 +61,7 @@
<div class="plans">
<article class="plan">
<h3>Free</h3>
- <p class="price"><span>€0</span></p>
+ <p class="price"><span data-currency-values="{&quot;USD&quot;:&quot;$0&quot;,&quot;EUR&quot;:&quot;€0&quot;,&quot;GBP&quot;:&quot;£0&quot;}">$0</span></p>
<p class="for"><span>Free to try, great for tiny apps.</span></p>
<ul>
<li>One member</li>
@@ -77,40 +77,40 @@
</article>
<article class="plan featured">
<h3>Personal</h3>
- <p class="price"><span data-currency-values="{&quot;USD&quot;:&quot;$28/y&quot;,&quot;EUR&quot;:&quot;€24/y&quot;,&quot;GBP&quot;:&quot;£22/y&quot;}">€24/y</span></p>
+ <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$28/y&quot;,&quot;EUR&quot;:&quot;€24/y&quot;,&quot;GBP&quot;:&quot;£22/y&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$21/y&quot;,&quot;EUR&quot;:&quot;€18/y&quot;,&quot;GBP&quot;:&quot;£16.50/y&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$14/y&quot;,&quot;EUR&quot;:&quot;€12/y&quot;,&quot;GBP&quot;:&quot;£11/y&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$9.80/y&quot;,&quot;EUR&quot;:&quot;€8.40/y&quot;,&quot;GBP&quot;:&quot;£7.70/y&quot;}}">$28/y</span></p>
<p class="for"><span>For individuals running a few apps.</span></p>
<ul>
<li>One member</li>
- <li>85 awake hours a month</li>
- <li>2 GB of storage</li>
- <li>2 GB of transfer a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;85 awake hours a month&quot;,&quot;B&quot;:&quot;63.75 awake hours a month&quot;,&quot;C&quot;:&quot;42.5 awake hours a month&quot;,&quot;D&quot;:&quot;29.75 awake hours a month&quot;}">85 awake hours a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;2 GB of storage&quot;,&quot;B&quot;:&quot;1.5 GB of storage&quot;,&quot;C&quot;:&quot;1 GB of storage&quot;,&quot;D&quot;:&quot;0.7 GB of storage&quot;}">2 GB of storage</li>
+ <li data-band-values="{&quot;A&quot;:&quot;2 GB of transfer a month&quot;,&quot;B&quot;:&quot;1.5 GB of transfer a month&quot;,&quot;C&quot;:&quot;1 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.7 GB of transfer a month&quot;}">2 GB of transfer a month</li>
</ul>
<a class="btn btn-primary" href="https://console.carloku.com/?plan=personal" data-pricing-signup>Choose Personal</a>
</article>
<article class="plan">
<h3>Community</h3>
- <p class="price"><span data-currency-values="{&quot;USD&quot;:&quot;$6/y per member&quot;,&quot;EUR&quot;:&quot;€5/y per member&quot;,&quot;GBP&quot;:&quot;£4.50/y per member&quot;}">€5/y per member</span></p>
+ <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$6/y per member&quot;,&quot;EUR&quot;:&quot;€5/y per member&quot;,&quot;GBP&quot;:&quot;£4.50/y per member&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$4.50/y per member&quot;,&quot;EUR&quot;:&quot;€3.75/y per member&quot;,&quot;GBP&quot;:&quot;£3.38/y per member&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$3/y per member&quot;,&quot;EUR&quot;:&quot;€2.50/y per member&quot;,&quot;GBP&quot;:&quot;£2.25/y per member&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$2.10/y per member&quot;,&quot;EUR&quot;:&quot;€1.75/y per member&quot;,&quot;GBP&quot;:&quot;£1.58/y per member&quot;}}">$6/y per member</span></p>
<p class="min"><span>Minimum 10 members.</span></p>
<p class="for"><span>For a groups, clubs, classes, an open-source projects.</span></p>
<p class="allowance-heading">Each member adds</p>
<ul class="allowances">
- <li>17 awake hours a month</li>
- <li>0.5 GB of storage</li>
- <li>0.4 GB of transfer a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;17 awake hours a month&quot;,&quot;B&quot;:&quot;12.75 awake hours a month&quot;,&quot;C&quot;:&quot;8.5 awake hours a month&quot;,&quot;D&quot;:&quot;5.95 awake hours a month&quot;}">17 awake hours a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;0.5 GB of storage&quot;,&quot;B&quot;:&quot;0.375 GB of storage&quot;,&quot;C&quot;:&quot;0.25 GB of storage&quot;,&quot;D&quot;:&quot;0.175 GB of storage&quot;}">0.5 GB of storage</li>
+ <li data-band-values="{&quot;A&quot;:&quot;0.4 GB of transfer a month&quot;,&quot;B&quot;:&quot;0.3 GB of transfer a month&quot;,&quot;C&quot;:&quot;0.2 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.14 GB of transfer a month&quot;}">0.4 GB of transfer a month</li>
</ul>
<a class="btn btn-quiet" href="https://console.carloku.com/?plan=community" data-pricing-signup>Choose Community</a>
</article>
<article class="plan">
<h3>Business</h3>
- <p class="price"><span data-currency-values="{&quot;USD&quot;:&quot;$60/y per member&quot;,&quot;EUR&quot;:&quot;€50/y per member&quot;,&quot;GBP&quot;:&quot;£45/y per member&quot;}">€50/y per member</span></p>
+ <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$60/y per member&quot;,&quot;EUR&quot;:&quot;€50/y per member&quot;,&quot;GBP&quot;:&quot;£45/y per member&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$45/y per member&quot;,&quot;EUR&quot;:&quot;€37.50/y per member&quot;,&quot;GBP&quot;:&quot;£33.75/y per member&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$30/y per member&quot;,&quot;EUR&quot;:&quot;€25/y per member&quot;,&quot;GBP&quot;:&quot;£22.50/y per member&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$21/y per member&quot;,&quot;EUR&quot;:&quot;€17.50/y per member&quot;,&quot;GBP&quot;:&quot;£15.75/y per member&quot;}}">$60/y per member</span></p>
<p class="min"><span>Minimum 10 members.</span></p>
<p class="for"><span>For teams that need continuous replication and restore.</span></p>
<p class="allowance-heading">Each member adds</p>
<ul class="allowances">
- <li>170 extra awake hours/m</li>
- <li>5 GB/m extra of storage</li>
- <li>3 GB of transfer a month</li>
- <li>30,000 replication writes a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;170 extra awake hours/m&quot;,&quot;B&quot;:&quot;127.5 extra awake hours/m&quot;,&quot;C&quot;:&quot;85 extra awake hours/m&quot;,&quot;D&quot;:&quot;59.5 extra awake hours/m&quot;}">170 extra awake hours/m</li>
+ <li data-band-values="{&quot;A&quot;:&quot;5 GB/m extra of storage&quot;,&quot;B&quot;:&quot;3.75 GB/m extra of storage&quot;,&quot;C&quot;:&quot;2.5 GB/m extra of storage&quot;,&quot;D&quot;:&quot;1.75 GB/m extra of storage&quot;}">5 GB/m extra of storage</li>
+ <li data-band-values="{&quot;A&quot;:&quot;3 GB of transfer a month&quot;,&quot;B&quot;:&quot;2.25 GB of transfer a month&quot;,&quot;C&quot;:&quot;1.5 GB of transfer a month&quot;,&quot;D&quot;:&quot;1.05 GB of transfer a month&quot;}">3 GB of transfer a month</li>
+ <li data-band-values="{&quot;A&quot;:&quot;30,000 replication writes a month&quot;,&quot;B&quot;:&quot;22,500 replication writes a month&quot;,&quot;C&quot;:&quot;15,000 replication writes a month&quot;,&quot;D&quot;:&quot;10,500 replication writes a month&quot;}">30,000 replication writes a month</li>
</ul>
<ul class="plan-features">
<li>Continuous replication, with restore to an earlier moment</li>
@@ -142,9 +142,9 @@
<table class="price-table">
<thead><tr><th scope="col"><span class="visually-hidden">Plan</span></th><th scope="col">A</th><th scope="col">B</th><th scope="col">C</th><th scope="col">D</th></tr></thead>
<tbody>
- <tr><th scope="row">Personal, a year</th><td data-currency-values="{&quot;USD&quot;:&quot;$28&quot;,&quot;EUR&quot;:&quot;€24&quot;,&quot;GBP&quot;:&quot;£22&quot;}">€24</td><td data-currency-values="{&quot;USD&quot;:&quot;$21&quot;,&quot;EUR&quot;:&quot;€18&quot;,&quot;GBP&quot;:&quot;£16.50&quot;}">€18</td><td data-currency-values="{&quot;USD&quot;:&quot;$14&quot;,&quot;EUR&quot;:&quot;€12&quot;,&quot;GBP&quot;:&quot;£11&quot;}">€12</td><td data-currency-values="{&quot;USD&quot;:&quot;$9.80&quot;,&quot;EUR&quot;:&quot;€8.40&quot;,&quot;GBP&quot;:&quot;£7.70&quot;}">€8.40</td></tr>
- <tr><th scope="row">Community, per member a year</th><td data-currency-values="{&quot;USD&quot;:&quot;$6&quot;,&quot;EUR&quot;:&quot;€5&quot;,&quot;GBP&quot;:&quot;£4.50&quot;}">€5</td><td data-currency-values="{&quot;USD&quot;:&quot;$4.50&quot;,&quot;EUR&quot;:&quot;€3.75&quot;,&quot;GBP&quot;:&quot;£3.38&quot;}">€3.75</td><td data-currency-values="{&quot;USD&quot;:&quot;$3&quot;,&quot;EUR&quot;:&quot;€2.50&quot;,&quot;GBP&quot;:&quot;£2.25&quot;}">€2.50</td><td data-currency-values="{&quot;USD&quot;:&quot;$2.10&quot;,&quot;EUR&quot;:&quot;€1.75&quot;,&quot;GBP&quot;:&quot;£1.58&quot;}">€1.75</td></tr>
- <tr><th scope="row">Business, per member a year</th><td data-currency-values="{&quot;USD&quot;:&quot;$60&quot;,&quot;EUR&quot;:&quot;€50&quot;,&quot;GBP&quot;:&quot;£45&quot;}">€50</td><td data-currency-values="{&quot;USD&quot;:&quot;$45&quot;,&quot;EUR&quot;:&quot;€37.50&quot;,&quot;GBP&quot;:&quot;£33.75&quot;}">€37.50</td><td data-currency-values="{&quot;USD&quot;:&quot;$30&quot;,&quot;EUR&quot;:&quot;€25&quot;,&quot;GBP&quot;:&quot;£22.50&quot;}">€25</td><td data-currency-values="{&quot;USD&quot;:&quot;$21&quot;,&quot;EUR&quot;:&quot;€17.50&quot;,&quot;GBP&quot;:&quot;£15.75&quot;}">€17.50</td></tr>
+ <tr><th scope="row">Personal, a year</th><td data-currency-values="{&quot;USD&quot;:&quot;$28&quot;,&quot;EUR&quot;:&quot;€24&quot;,&quot;GBP&quot;:&quot;£22&quot;}">$28</td><td data-currency-values="{&quot;USD&quot;:&quot;$21&quot;,&quot;EUR&quot;:&quot;€18&quot;,&quot;GBP&quot;:&quot;£16.50&quot;}">$21</td><td data-currency-values="{&quot;USD&quot;:&quot;$14&quot;,&quot;EUR&quot;:&quot;€12&quot;,&quot;GBP&quot;:&quot;£11&quot;}">$14</td><td data-currency-values="{&quot;USD&quot;:&quot;$9.80&quot;,&quot;EUR&quot;:&quot;€8.40&quot;,&quot;GBP&quot;:&quot;£7.70&quot;}">$9.80</td></tr>
+ <tr><th scope="row">Community, per member a year</th><td data-currency-values="{&quot;USD&quot;:&quot;$6&quot;,&quot;EUR&quot;:&quot;€5&quot;,&quot;GBP&quot;:&quot;£4.50&quot;}">$6</td><td data-currency-values="{&quot;USD&quot;:&quot;$4.50&quot;,&quot;EUR&quot;:&quot;€3.75&quot;,&quot;GBP&quot;:&quot;£3.38&quot;}">$4.50</td><td data-currency-values="{&quot;USD&quot;:&quot;$3&quot;,&quot;EUR&quot;:&quot;€2.50&quot;,&quot;GBP&quot;:&quot;£2.25&quot;}">$3</td><td data-currency-values="{&quot;USD&quot;:&quot;$2.10&quot;,&quot;EUR&quot;:&quot;€1.75&quot;,&quot;GBP&quot;:&quot;£1.58&quot;}">$2.10</td></tr>
+ <tr><th scope="row">Business, per member a year</th><td data-currency-values="{&quot;USD&quot;:&quot;$60&quot;,&quot;EUR&quot;:&quot;€50&quot;,&quot;GBP&quot;:&quot;£45&quot;}">$60</td><td data-currency-values="{&quot;USD&quot;:&quot;$45&quot;,&quot;EUR&quot;:&quot;€37.50&quot;,&quot;GBP&quot;:&quot;£33.75&quot;}">$45</td><td data-currency-values="{&quot;USD&quot;:&quot;$30&quot;,&quot;EUR&quot;:&quot;€25&quot;,&quot;GBP&quot;:&quot;£22.50&quot;}">$30</td><td data-currency-values="{&quot;USD&quot;:&quot;$21&quot;,&quot;EUR&quot;:&quot;€17.50&quot;,&quot;GBP&quot;:&quot;£15.75&quot;}">$21</td></tr>
</tbody>
</table>
<p>Ireland, the EU-15, the US, the UK, Australia and Canada are band A. Your billing country sets the band at checkout. Usage beyond the inclusions costs the same in every band.</p>
@@ -164,13 +164,13 @@
<p>Extra usage is billed monthly, from our own usage statements.</p>
<table class="price-table rates">
<tbody>
- <tr><th scope="row">Awake time</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.012 per hour&quot;,&quot;EUR&quot;:&quot;€0.010 per hour&quot;,&quot;GBP&quot;:&quot;£0.009 per hour&quot;}">€0.010 per hour</td></tr>
- <tr><th scope="row">Storage</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.127 per GB per month&quot;,&quot;EUR&quot;:&quot;€0.106 per GB per month&quot;,&quot;GBP&quot;:&quot;£0.095 per GB per month&quot;}">€0.106 per GB per month</td></tr>
- <tr><th scope="row">Transfer</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.497 per GB&quot;,&quot;EUR&quot;:&quot;€0.414 per GB&quot;,&quot;GBP&quot;:&quot;£0.373 per GB&quot;}">€0.414 per GB</td></tr>
- <tr><th scope="row">Replication writes</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.028 per 1,000&quot;,&quot;EUR&quot;:&quot;€0.023 per 1,000&quot;,&quot;GBP&quot;:&quot;£0.021 per 1,000&quot;}">€0.023 per 1,000</td></tr>
+ <tr><th scope="row">Awake time</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.012 per hour&quot;,&quot;EUR&quot;:&quot;€0.010 per hour&quot;,&quot;GBP&quot;:&quot;£0.009 per hour&quot;}">$0.012 per hour</td></tr>
+ <tr><th scope="row">Storage</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.127 per GB per month&quot;,&quot;EUR&quot;:&quot;€0.106 per GB per month&quot;,&quot;GBP&quot;:&quot;£0.095 per GB per month&quot;}">$0.127 per GB per month</td></tr>
+ <tr><th scope="row">Transfer</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.497 per GB&quot;,&quot;EUR&quot;:&quot;€0.414 per GB&quot;,&quot;GBP&quot;:&quot;£0.373 per GB&quot;}">$0.497 per GB</td></tr>
+ <tr><th scope="row">Replication writes</th><td data-currency-values="{&quot;USD&quot;:&quot;$0.028 per 1,000&quot;,&quot;EUR&quot;:&quot;€0.023 per 1,000&quot;,&quot;GBP&quot;:&quot;£0.021 per 1,000&quot;}">$0.028 per 1,000</td></tr>
</tbody>
</table>
- <p data-currency-values="{&quot;USD&quot;:&quot;Minimum $1.20 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;,&quot;EUR&quot;:&quot;Minimum €1 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;,&quot;GBP&quot;:&quot;Minimum £0.90 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;}">Minimum €1 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.</p>
+ <p data-currency-values="{&quot;USD&quot;:&quot;Minimum $1.20 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;,&quot;EUR&quot;:&quot;Minimum €1 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;,&quot;GBP&quot;:&quot;Minimum £0.90 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.&quot;}">Minimum $1.20 bill based on our own measures. If usage doesn't meet our metered amount, it's not billed for that month.</p>
<p>Carloku is hosting (currently) on AWS. For files served directly from S3, we simply charge the AWS transfer fee.</p>
</div>
</section>
diff --git a/src/site.js b/src/site.js
index 01d230d..29d12ef 100644
--- a/src/site.js
+++ b/src/site.js
@@ -11,6 +11,14 @@
onScroll();
}
+ // The pricing page's currency and billing-country controls. The currency
+ // picks the text of every element carrying data-currency-values; the
+ // country picks the band (its option's data-band) for every element
+ // carrying data-band-values, whose texts are by band and then, where the
+ // amount is money, by currency. Both are forwarded to the console on
+ // the signup links (?currency=, ?country=), where they are suggestions:
+ // checkout prices from the country the buyer gives there, in the
+ // currency the buyer confirms there. Nothing here is a price.
var currencySelect = document.querySelector("[data-pricing-currency]");
if (currencySelect) {
var supported = ["USD", "EUR", "GBP"];
@@ -20,24 +28,52 @@
function countryCurrency(country) {
return country === "GB" ? "GBP" : eu.indexOf(country) !== -1 ? "EUR" : "USD";
}
- function renderCurrency(currency) {
+ // countryOption is the selected country's option, or null when none
+ // is selected or the value matches no option.
+ function countryOption() {
+ var value = countrySelect && countrySelect.value;
+ if (!value || !/^[A-Z]{2}$/.test(value)) return null;
+ return countrySelect.querySelector('option[value="' + value + '"]');
+ }
+ function selectedBand() {
+ var opt = countryOption();
+ var band = opt && opt.getAttribute("data-band");
+ return band === "B" || band === "C" || band === "D" ? band : "A";
+ }
+ function render(currency) {
if (supported.indexOf(currency) === -1) return;
currencySelect.value = currency;
+ var band = selectedBand();
document.querySelectorAll("[data-currency-values]").forEach(function (el) {
try {
var values = JSON.parse(el.getAttribute("data-currency-values"));
if (typeof values[currency] === "string") el.textContent = values[currency];
} catch (_) {}
});
+ document.querySelectorAll("[data-band-values]").forEach(function (el) {
+ try {
+ var text = JSON.parse(el.getAttribute("data-band-values"))[band];
+ if (text && typeof text === "object") text = text[currency];
+ if (typeof text === "string") el.textContent = text;
+ } catch (_) {}
+ });
+ var country = countryOption() ? countrySelect.value : "";
document.querySelectorAll("[data-pricing-signup]").forEach(function (el) {
var url = new URL(el.href, window.location.href);
url.searchParams.set("currency", currency.toLowerCase());
+ if (country) url.searchParams.set("country", country);
+ else url.searchParams.delete("country");
el.href = url.href;
});
}
var initial = "USD";
try {
- var requested = new URL(window.location.href).searchParams.get("currency");
+ var params = new URL(window.location.href).searchParams;
+ var wanted = params.get("country");
+ if (countrySelect && wanted && /^[A-Za-z]{2}$/.test(wanted) && countrySelect.querySelector('option[value="' + wanted.toUpperCase() + '"]')) {
+ countrySelect.value = wanted.toUpperCase();
+ }
+ var requested = params.get("currency");
requested = requested && requested.toUpperCase();
if (supported.indexOf(requested) !== -1) {
initial = requested;
@@ -49,13 +85,13 @@
initial = countryCurrency(region);
}
} catch (_) {}
- renderCurrency(initial);
+ render(initial);
currencySelect.addEventListener("change", function () {
explicit = true;
- renderCurrency(currencySelect.value);
+ render(currencySelect.value);
});
if (countrySelect) countrySelect.addEventListener("change", function () {
- if (!explicit) renderCurrency(countryCurrency(countrySelect.value));
+ render(explicit ? currencySelect.value : countryCurrency(countrySelect.value));
});
}
diff --git a/test/pricing-currency.test.mjs b/test/pricing-currency.test.mjs
index 92b7af9..9da7d5c 100644
--- a/test/pricing-currency.test.mjs
+++ b/test/pricing-currency.test.mjs
@@ -3,21 +3,38 @@ import { runInNewContext } from 'node:vm';
import assert from 'node:assert/strict';
import { test } from 'node:test';
const source = readFileSync(new URL('../src/site.js', import.meta.url), 'utf8');
+// A page with the two controls, one currency-priced element (an overage
+// rate), one band-and-currency-priced element (a plan price), one
+// band-scaled inclusion and one signup link. The country options are
+// the four the cases need, each with its default currency and its band.
+const options = { IE: ['EUR', 'A'], GB: ['GBP', 'A'], US: ['USD', 'A'], IN: ['USD', 'D'], PL: ['EUR', 'C'] };
function page(language, query = '', country = '') {
const listeners = {};
const selector = { value: 'USD', addEventListener: (name, fn) => listeners.currency = fn };
- const countrySelector = { value: country, addEventListener: (name, fn) => listeners.country = fn };
- const amount = { textContent: '$28', getAttribute: () => JSON.stringify({ USD: '$28', EUR: '€24', GBP: '£22' }) };
+ const countrySelector = {
+ value: country,
+ addEventListener: (name, fn) => listeners.country = fn,
+ querySelector: (sel) => {
+ const iso = sel.match(/^option\[value="([A-Z]{2})"\]$/)[1];
+ return options[iso] ? { getAttribute: (a) => a === 'data-band' ? options[iso][1] : a === 'data-currency' ? options[iso][0] : null } : null;
+ },
+ };
+ const rate = { textContent: '$0.012 per hour', getAttribute: () => JSON.stringify({ USD: '$0.012 per hour', EUR: '€0.010 per hour', GBP: '£0.009 per hour' }) };
+ const price = { textContent: '$28/y', getAttribute: () => JSON.stringify({
+ A: { USD: '$28/y', EUR: '€24/y', GBP: '£22/y' }, B: { USD: '$21/y', EUR: '€18/y', GBP: '£16.50/y' },
+ C: { USD: '$14/y', EUR: '€12/y', GBP: '£11/y' }, D: { USD: '$9.80/y', EUR: '€8.40/y', GBP: '£7.70/y' } }) };
+ const hours = { textContent: '85 awake hours a month', getAttribute: () => JSON.stringify({ A: '85 awake hours a month', B: '63.75 awake hours a month', C: '42.5 awake hours a month', D: '29.75 awake hours a month' }) };
const signup = { href: 'https://console.carloku.com/?plan=personal' };
runInNewContext(source, {
document: {
querySelector: key => key === '[data-pricing-currency]' ? selector : key === '[data-pricing-country]' ? countrySelector : null,
- querySelectorAll: key => key === '[data-currency-values]' ? [amount] : key === '[data-pricing-signup]' ? [signup] : []
+ querySelectorAll: key => key === '[data-currency-values]' ? [rate] : key === '[data-band-values]' ? [price, hours] : key === '[data-pricing-signup]' ? [signup] : []
},
window: { location: { href: 'https://carloku.com/pricing/' + query } },
navigator: { language }, Intl, URL
});
- return { selector, countrySelector, amount, signup, listeners };
+ const link = () => new URL(signup.href).searchParams;
+ return { selector, countrySelector, rate, price, hours, signup, link, listeners };
}
test('currency defaults from explicit locale region; language alone is not a country', () => {
for (const [locale, expected] of [['en-US','USD'],['en-GB','GBP'],['en-IE','EUR'],['pl-PL','EUR'],['fr-CA','USD'],['en','USD'],['bad_locale','USD']]) {
@@ -26,13 +43,16 @@ test('currency defaults from explicit locale region; language alone is not a cou
});
test('explicit choice overrides region and persists when billing country changes', () => {
const p = page('en-IE', '?currency=gbp', 'IE');
- assert.equal(p.amount.textContent, '£22');
- assert.equal(new URL(p.signup.href).searchParams.get('plan'), 'personal');
- assert.equal(new URL(p.signup.href).searchParams.get('currency'), 'gbp');
+ assert.equal(p.price.textContent, '£22/y');
+ assert.equal(p.rate.textContent, '£0.009 per hour');
+ assert.equal(p.link().get('plan'), 'personal');
+ assert.equal(p.link().get('currency'), 'gbp');
+ assert.equal(p.link().get('country'), 'IE');
p.countrySelector.value = 'US'; p.listeners.country();
assert.equal(p.selector.value, 'GBP');
+ assert.equal(p.link().get('country'), 'US');
p.selector.value = 'USD'; p.listeners.currency();
- assert.equal(p.amount.textContent, '$28');
+ assert.equal(p.price.textContent, '$28/y');
});
test('country sets default until a currency is explicitly chosen', () => {
const p = page('en-US', '', 'IE');
@@ -40,8 +60,45 @@ test('country sets default until a currency is explicitly chosen', () => {
p.countrySelector.value = 'GB'; p.listeners.country();
assert.equal(p.selector.value, 'GBP');
});
+test('a band D country with an explicit currency keeps the currency and changes the band: price, inclusions and the signup link', () => {
+ const p = page('en-US', '?currency=gbp', '');
+ assert.equal(p.price.textContent, '£22/y');
+ assert.equal(p.hours.textContent, '85 awake hours a month');
+ assert.equal(p.link().get('country'), null);
+ p.countrySelector.value = 'IN'; p.listeners.country();
+ assert.equal(p.selector.value, 'GBP', 'the explicit currency stays');
+ assert.equal(p.price.textContent, '£7.70/y', 'the price is band D in pounds');
+ assert.equal(p.hours.textContent, '29.75 awake hours a month', 'the inclusions are band D');
+ assert.equal(p.rate.textContent, '£0.009 per hour', 'overage rates do not change with the band');
+ assert.deepEqual([p.link().get('currency'), p.link().get('country'), p.link().get('plan')], ['gbp', 'IN', 'personal']);
+ // Back to a band A country: full price, full inclusions, same currency.
+ p.countrySelector.value = 'US'; p.listeners.country();
+ assert.equal(p.price.textContent, '£22/y');
+ assert.equal(p.hours.textContent, '85 awake hours a month');
+ // No country: band A, and the link carries no country.
+ p.countrySelector.value = ''; p.listeners.country();
+ assert.equal(p.price.textContent, '£22/y');
+ assert.equal(p.link().get('country'), null);
+});
+test('a band C country without an explicit currency takes its default currency and its band', () => {
+ const p = page('en-US', '', '');
+ p.countrySelector.value = 'PL'; p.listeners.country();
+ assert.equal(p.selector.value, 'EUR');
+ assert.equal(p.price.textContent, '€12/y');
+ assert.equal(p.hours.textContent, '42.5 awake hours a month');
+ assert.deepEqual([p.link().get('currency'), p.link().get('country')], ['eur', 'PL']);
+});
+test('the link back from the console preselects the country and currency it carries', () => {
+ const p = page('en-US', '?country=in&currency=gbp', '');
+ assert.equal(p.countrySelector.value, 'IN');
+ assert.equal(p.selector.value, 'GBP');
+ assert.equal(p.price.textContent, '£7.70/y');
+ const q = page('en-US', '?country=XX', '');
+ assert.equal(q.countrySelector.value, '', 'an unknown country is ignored');
+ assert.equal(q.link().get('country'), null);
+});
test('unsupported URL currency is ignored and never forwarded', () => {
const p = page('en-US', '?currency=xyz');
assert.equal(p.selector.value, 'USD');
- assert.equal(new URL(p.signup.href).searchParams.get('currency'), 'usd');
+ assert.equal(p.link().get('currency'), 'usd');
});
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs
index a9db5ce..7795de3 100644
--- a/test/pricing-pricebook.test.mjs
+++ b/test/pricing-pricebook.test.mjs
@@ -1,7 +1,8 @@
// 🤖 The pricing page shows no price that is not in the platform's price
// book. Every amount on the page is generated from copy-review/pricing/
-// pricebook.json; the approved EUR strings are the book's EUR; and the
-// other currencies' texts are the same words with the book's amounts.
+// pricebook.json; the approved EUR strings are the book's EUR at band A;
+// the other currencies' and bands' texts are the same words with the
+// book's amounts.
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
@@ -16,8 +17,17 @@ const page = readFileSync(new URL('src/pricing/index.html', root), 'utf8');
const preview = readFileSync(new URL('copy-review/pricing/preview/index.html', root), 'utf8');
const unesc = (t) => t.replace(/&quot;/g, '"').replace(/&amp;/g, '&');
const valuesOf = (html) => [...html.matchAll(/data-currency-values="([^"]*)"/g)].map((m) => JSON.parse(unesc(m[1])));
+const bandValuesOf = (html) => [...html.matchAll(/data-band-values="([^"]*)"/g)].map((m) => JSON.parse(unesc(m[1])));
+const re = (id) => id.replace(/\./g, '\\.');
+const attr = (id, name) => {
+ const m = preview.match(new RegExp(`data-copy="${re(id)}" ${name}="([^"]*)"`));
+ assert.ok(m, id + ' carries ' + name);
+ return JSON.parse(unesc(m[1]));
+};
+const text = (id) => preview.match(new RegExp(`data-copy="${re(id)}"[^>]*>([^<]*)<`))[1];
const money = (sym, minor) => sym + Math.floor(minor / 100) + (minor % 100 ? '.' + String(minor % 100).padStart(2, '0') : '');
const sym = Object.fromEntries(book.currencies.map((c) => [c.code.toUpperCase(), c.symbol]));
+const planOf = (k) => book.plans.find((p) => p.key === k);
test('the book is the platform export, at the version the console sells from', () => {
assert.equal(book.schema, 1);
@@ -25,26 +35,44 @@ test('the book is the platform export, at the version the console sells from', (
assert.ok(/public-pricebook\.json at commit [0-9a-f]{8}/.test(readFileSync(new URL('copy-review/pricing/pricebook.source.txt', root), 'utf8')));
});
-test('every priced string on the page carries all three currencies, and EUR is the approved text', () => {
+test('every priced string carries all three currencies, every banded one all four bands, and band A EUR is the approved text', () => {
const all = valuesOf(page);
- assert.ok(all.length >= 3 + 12 + 4 + 1, `found ${all.length} priced strings`);
+ assert.equal(all.length, 1 + 12 + 4 + 1, `found ${all.length} currency-priced strings`);
for (const v of all) assert.deepEqual(Object.keys(v).sort(), ['EUR', 'GBP', 'USD']);
- for (const id of ['plan.personal.price', 'plan.community.price', 'plan.business.price', 'over.rate.awake', 'over.rate.storage', 'over.rate.transfer', 'over.rate.writes', 'over.p2']) {
- const m = preview.match(new RegExp(`data-copy="${id.replace(/\./g, '\\.')}" data-currency-values="([^"]*)"`));
- assert.ok(m, id + ' is priced from the book');
- assert.equal(JSON.parse(unesc(m[1])).EUR, S[id], id + ' EUR is the approved string');
+ for (const id of ['plan.free.price', 'over.rate.awake', 'over.rate.storage', 'over.rate.transfer', 'over.rate.writes', 'over.p2']) {
+ assert.equal(attr(id, 'data-currency-values').EUR, S[id], id + ' EUR is the approved string');
}
+ const banded = bandValuesOf(page);
+ assert.equal(banded.length, 3 + 3 + 3 + 4, `found ${banded.length} banded strings`);
+ for (const v of banded) assert.deepEqual(Object.keys(v), ['A', 'B', 'C', 'D']);
+ for (const id of ['plan.personal.price', 'plan.community.price', 'plan.business.price']) {
+ const v = attr(id, 'data-band-values');
+ for (const band of ['A', 'B', 'C', 'D']) assert.deepEqual(Object.keys(v[band]).sort(), ['EUR', 'GBP', 'USD']);
+ assert.equal(v.A.EUR, S[id], id + ' band A EUR is the approved string');
+ }
+});
+
+test('the static page, before any script runs, is the default currency at band A: what the controls show selected', () => {
+ assert.equal(book.default_currency, 'usd');
+ assert.ok(page.includes('<option value="USD">USD</option><option value="EUR">EUR</option><option value="GBP">GBP</option>'));
+ for (const id of ['plan.personal.price', 'plan.community.price', 'plan.business.price']) assert.equal(text(id), attr(id, 'data-band-values').A.USD, id);
+ for (const id of ['plan.free.price', 'over.rate.awake', 'over.p2']) assert.equal(text(id), attr(id, 'data-currency-values').USD, id);
+ for (const id of ['plan.personal.i2', 'plan.business.i4']) assert.equal(text(id), attr(id, 'data-band-values').A, id);
+ assert.equal(text('plan.personal.price'), '$28/y');
+ assert.equal(text('plan.free.price'), '$0');
+ const stripped = page.replace(/data-(currency|band)-values="[^"]*"/g, '');
+ assert.equal((stripped.match(/€/g) || []).length, 0, 'no euro amount is in the static text');
});
-test('plan prices and the band table are the book, per currency', () => {
- const planOf = (k) => book.plans.find((p) => p.key === k);
+test('plan prices and the band table are the book, per band and currency', () => {
for (const [k, id] of [['personal', 'plan.personal.price'], ['community', 'plan.community.price'], ['business', 'plan.business.price']]) {
- const m = preview.match(new RegExp(`data-copy="${id.replace(/\./g, '\\.')}" data-currency-values="([^"]*)"`));
- const v = JSON.parse(unesc(m[1]));
- for (const cur of ['USD', 'EUR', 'GBP']) {
- assert.ok(v[cur].startsWith(money(sym[cur], planOf(k).annual_unit_minor[cur.toLowerCase()].A)), `${k} ${cur}: ${v[cur]}`);
+ const v = attr(id, 'data-band-values');
+ for (const band of ['A', 'B', 'C', 'D']) for (const cur of ['USD', 'EUR', 'GBP']) {
+ assert.ok(v[band][cur].startsWith(money(sym[cur], planOf(k).annual_unit_minor[cur.toLowerCase()][band])), `${k} ${band} ${cur}: ${v[band][cur]}`);
}
}
+ assert.equal(attr('plan.personal.price', 'data-band-values').D.GBP, '£7.70/y');
+ assert.equal(attr('plan.business.price', 'data-band-values').D.GBP, '£15.75/y per member');
const cells = valuesOf(page.slice(page.indexOf('class="price-table"'), page.indexOf('class="price-table rates"')));
assert.equal(cells.length, 12);
let i = 0;
@@ -57,6 +85,17 @@ test('plan prices and the band table are the book, per currency', () => {
assert.equal(cells[7].GBP, '£1.58');
});
+test('a band-scaled inclusion carries each band\'s figure by the platform\'s rule, and band A is the approved string', () => {
+ const per = (k, resource, band) => Math.floor(planOf(k).resources_per_member[resource] * book.band_factors_bps[band] / 10000);
+ assert.deepEqual(attr('plan.personal.i2', 'data-band-values'), { A: S['plan.personal.i2'], B: '63.75 awake hours a month', C: '42.5 awake hours a month', D: '29.75 awake hours a month' });
+ assert.equal(per('personal', 'awake_seconds', 'D') / 3600, 29.75);
+ assert.deepEqual(attr('plan.community.i2', 'data-band-values'), { A: S['plan.community.i2'], B: '0.375 GB of storage', C: '0.25 GB of storage', D: '0.175 GB of storage' });
+ assert.deepEqual(attr('plan.business.i4', 'data-band-values'), { A: S['plan.business.i4'], B: '22,500 replication writes a month', C: '15,000 replication writes a month', D: '10,500 replication writes a month' });
+ assert.equal(per('business', 'writes', 'D'), 10500);
+ // Strings with no figure, and the free plan's, are not banded.
+ for (const id of ['plan.personal.i1', 'plan.free.i3', 'plan.business.i5']) assert.ok(!preview.match(new RegExp(`data-copy="${re(id)}" data-band-values`)), id);
+});
+
test('overage rates and the floor are the book, per currency', () => {
const rates = valuesOf(page.slice(page.indexOf('class="price-table rates"')));
const o = book.overage;
@@ -67,22 +106,23 @@ test('overage rates and the floor are the book, per currency', () => {
assert.ok(rates[4].GBP.startsWith('Minimum ' + money('£', o.gbp.invoice_floor_micro / 10000)), rates[4].GBP);
});
-test('the controls carry the five approved strings and the country defaults from the book', () => {
+test('the controls carry the five approved strings, and every country option its default currency and band from the book', () => {
const currency = JSON.parse(readFileSync(new URL('copy-review/currency/result.json', root), 'utf8'));
assert.equal(currency.action, 'approve');
for (const s of currency.strings) assert.ok(page.includes('>' + s.text + '<'), s.id);
assert.ok(page.includes('data-pricing-currency'));
- assert.ok(page.includes('<option value="IE" data-currency="EUR">'));
- assert.ok(page.includes('<option value="GB" data-currency="GBP">'));
- assert.ok(page.includes('<option value="US" data-currency="USD">'));
- assert.ok(page.includes('<option value="CH" data-currency="USD">'));
+ for (const [iso, band] of Object.entries(book.country_band)) {
+ assert.ok(page.includes(`<option value="${iso}" data-currency="${(book.country_currency[iso] || book.default_currency).toUpperCase()}" data-band="${band}">`), iso);
+ }
+ assert.ok(page.includes('<option value="IE" data-currency="EUR" data-band="A">'));
+ assert.ok(page.includes('<option value="GB" data-currency="GBP" data-band="A">'));
+ assert.ok(page.includes('<option value="IN" data-currency="USD" data-band="D">'));
+ assert.ok(page.includes('<option value="CH" data-currency="USD" data-band="A">'));
assert.equal((page.match(/data-pricing-signup/g) || []).length, 5);
});
test('no euro amount on the page is typed in the script: each comes from a string or the book', () => {
- // Every € on the page is inside an element the book priced, or in a
- // string the review approved.
- const stripped = page.replace(/data-currency-values="[^"]*"/g, '');
+ const stripped = page.replace(/data-(currency|band)-values="[^"]*"/g, '');
for (const m of stripped.matchAll(/€[0-9][0-9.,]*/g)) {
const amount = m[0];
const inBook = Object.values(book.plans).some((p) => Object.values(p.annual_unit_minor.eur).some((minor) => money('€', minor) === amount));