@tokenoftrust/cli 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Token of Trust",
@@ -18,6 +18,13 @@ export const STRUCTURED_DATA_NODE_TOKENS: readonly string[];
18
18
  export const STRUCTURED_DATA_NAMED_TYPES: readonly string[];
19
19
  export const STRUCTURED_DATA_NAME_SOURCES: readonly string[];
20
20
  export function seoDeclarationProblems(seo: unknown): string[];
21
+ export const DEFAULT_CURRENCY: "USD";
22
+ export const CURRENCY_CODES: readonly string[];
23
+ export function resolveStoreCurrency(declared: unknown): string;
24
+ export function currencyDeclarationProblems(currency: unknown): string[];
25
+ export const SHIPPING_KEYS: readonly string[];
26
+ export const MAX_SHIPPING_AMOUNT: number;
27
+ export function shippingDeclarationProblems(shipping: unknown): string[];
21
28
  export const COMPLIANCE_NOTICES: Record<string, string>;
22
29
  export const MANDATED_NOTICE_KEYS: string[];
23
30
  export function nearestKey(key: string, known: readonly string[]): string | null;
@@ -19,7 +19,7 @@
19
19
 
20
20
  export const TOT_CONFIG_KEYS = [
21
21
  "$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
22
- "displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo",
22
+ "displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo", "currency", "shipping",
23
23
  ];
24
24
  // Keys other tooling has written that the platform does NOT read, with the key it does.
25
25
  const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
@@ -32,6 +32,44 @@ export const FEATURE_KEYS = [
32
32
  ];
33
33
  export const SITE_TYPES = ["commerce", "marketing"];
34
34
 
35
+ /** The currency a store sells in when `.tot/config.json` declares none. */
36
+ export const DEFAULT_CURRENCY = "USD";
37
+
38
+ /** ISO 4217 codes a store may declare as its `currency`. */
39
+ export const CURRENCY_CODES = Object.freeze(
40
+ ("AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BRL BSD BTN BWP BYN BZD CAD CDF CHF CLP CNY COP " +
41
+ "CRC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HTG HUF IDR ILS INR IQD IRR " +
42
+ "ISK JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA MKD MMK MNT MOP MRU MUR MVR MWK MXN " +
43
+ "MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SOS SRD " +
44
+ "SSP STN SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VES VND VUV WST XAF XCD XCG XOF XPF YER ZAR ZMW ZWG").split(" "),
45
+ );
46
+
47
+ /**
48
+ * The ONE place a store's currency is resolved. A declared ISO 4217 code wins; anything
49
+ * else (absent, wrong shape, lowercase, unknown code) is USD. Every surface that prices,
50
+ * signs or formats an amount takes the currency from here, never from a literal. Only the
51
+ * store owner's `.tot/config.json` can change it: a shopper's request never reaches this.
52
+ *
53
+ * @param {unknown} declared
54
+ * @returns {string}
55
+ */
56
+ export function resolveStoreCurrency(declared) {
57
+ return typeof declared === "string" && CURRENCY_CODES.includes(declared) ? declared : DEFAULT_CURRENCY;
58
+ }
59
+
60
+ /**
61
+ * What is wrong with a declared `currency`, one sentence; `[]` when it is valid.
62
+ *
63
+ * @param {unknown} currency
64
+ * @returns {string[]}
65
+ */
66
+ export function currencyDeclarationProblems(currency) {
67
+ return typeof currency === "string" && CURRENCY_CODES.includes(currency)
68
+ ? []
69
+ : [`\`currency\` is ${JSON.stringify(currency)} — must be an uppercase ISO 4217 code such as "USD" or "CAD"`];
70
+ }
71
+
72
+
35
73
  /** Every `seo` key (mirrors packages/public-runtime/src/declared-tenant-config.mjs). */
36
74
  export const SEO_KEYS = Object.freeze(["trailingSlash", "verbatimSeoTitles", "structuredData"]);
37
75
 
@@ -158,6 +196,46 @@ export function seoDeclarationProblems(seo) {
158
196
  return problems;
159
197
  }
160
198
 
199
+ /** Every `shipping` key (mirrors packages/public-runtime/src/declared-tenant-config.mjs). */
200
+ export const SHIPPING_KEYS = Object.freeze(["flatRate", "freeAtOrAbove"]);
201
+
202
+ /** Largest shipping amount a declaration may state, in the cart's currency. */
203
+ export const MAX_SHIPPING_AMOUNT = 100000;
204
+
205
+ /** @param {unknown} value */
206
+ const isShippingAmount = (value) =>
207
+ typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= MAX_SHIPPING_AMOUNT
208
+ && Math.abs(value * 100 - Math.round(value * 100)) < 1e-6;
209
+
210
+ /**
211
+ * What is wrong with a declared `shipping` value, one sentence per problem; `[]` when it is
212
+ * valid. A policy with any problem is refused whole, so checkout never charges half of one.
213
+ *
214
+ * @param {unknown} shipping
215
+ * @returns {string[]}
216
+ */
217
+ export function shippingDeclarationProblems(shipping) {
218
+ if (!isPlainObject(shipping)) return ["`shipping` must be an object"];
219
+ const problems = [];
220
+ const source = /** @type {Record<string, unknown>} */ (shipping);
221
+ for (const key of Object.keys(source)) {
222
+ if (!SHIPPING_KEYS.includes(key)) problems.push(`\`shipping.${key}\` is not a shipping key (${SHIPPING_KEYS.join(", ")})`);
223
+ }
224
+ if (!isShippingAmount(source.flatRate)) {
225
+ problems.push(`\`shipping.flatRate\` must be an amount from 0 to ${MAX_SHIPPING_AMOUNT} with at most two decimal places`);
226
+ }
227
+ if (source.freeAtOrAbove !== undefined && !(isShippingAmount(source.freeAtOrAbove) && /** @type {number} */ (source.freeAtOrAbove) > 0)) {
228
+ problems.push(`\`shipping.freeAtOrAbove\` must be an amount above 0 and at most ${MAX_SHIPPING_AMOUNT} with at most two decimal places`);
229
+ }
230
+ return problems;
231
+ }
232
+
233
+ /** The owner-facing sentence for an invalid `shipping` declaration. */
234
+ function shippingProblemMessage(problems) {
235
+ return `\`shipping\` is not a valid shipping policy (${problems.join("; ")}). ` +
236
+ "Checkout charges no shipping until it is fixed.";
237
+ }
238
+
161
239
 
162
240
  /** What each compliance key puts on the page — the thing that is missing when it is. */
163
241
  export const COMPLIANCE_NOTICES = {
@@ -530,6 +608,8 @@ export function declarationFindings(current, previous) {
530
608
  if (warning.enabled && warning.problems.length > 0) {
531
609
  out.push(`CONFIG: ${nicotineWarningProblemMessage(warning.problems)}`);
532
610
  }
611
+ const shipping = current.shipping === undefined ? [] : shippingDeclarationProblems(current.shipping);
612
+ if (shipping.length > 0) out.push(`CONFIG: ${shippingProblemMessage(shipping)}`);
533
613
  }
534
614
  for (const { key, notice } of droppedMandatedNotices(previous, current)) {
535
615
  out.push(
@@ -608,11 +688,23 @@ export function validateDeclaredConfig(config, file = ".tot/config.json") {
608
688
  && !(Array.isArray(config.embeds) && config.embeds.every((/** @type {unknown} */ s) => typeof s === "string"))) {
609
689
  out.push(mk(ERROR, "config-embeds", file, '`embeds` must be an array of embed slugs, e.g. ["youtube"]'));
610
690
  }
691
+ if (config.currency !== undefined) {
692
+ for (const problem of currencyDeclarationProblems(config.currency)) {
693
+ out.push(mk(ERROR, "config-currency", file, `${problem} — the store sells in ${DEFAULT_CURRENCY} until it is fixed`));
694
+ }
695
+ }
611
696
  if (config.seo !== undefined) {
612
697
  for (const problem of seoDeclarationProblems(config.seo)) {
613
698
  out.push(mk(ERROR, "config-seo", `${file} seo`, `${problem} — the whole \`seo\` declaration is ignored until it is fixed`));
614
699
  }
615
700
  }
701
+ if (config.shipping !== undefined) {
702
+ const problems = shippingDeclarationProblems(config.shipping);
703
+ if (problems.length > 0) {
704
+ out.push(mk(ERROR, "config-shipping", `${file} shipping`, shippingProblemMessage(problems),
705
+ 'declare e.g. `"shipping": { "flatRate": 5.99, "freeAtOrAbove": 100 }`, or remove `shipping` to charge none'));
706
+ }
707
+ }
616
708
  return out;
617
709
  }
618
710