@asteby/storefront 0.17.0 → 0.18.1
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/README.md +24 -0
- package/dist/{account-settings-DNcJKOQl.d.ts → account-settings-C3OFjz-H.d.ts} +109 -1
- package/dist/{account-settings-H6zuI7iG.d.cts → account-settings-x0TZ7BTv.d.cts} +109 -1
- package/dist/{chunk-IPUAZ3LQ.js → chunk-5BJRZIS3.js} +196 -3
- package/dist/chunk-5BJRZIS3.js.map +1 -0
- package/dist/cli.js +25 -2
- package/dist/fiscal-document.d.cts +2 -2
- package/dist/fiscal-document.d.ts +2 -2
- package/dist/index.cjs +232 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +26 -3
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +247 -1
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +18 -2
- package/dist/mock.d.ts +18 -2
- package/dist/mock.js +143 -3
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IPUAZ3LQ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -124,9 +124,14 @@ function appendParams(search, params) {
|
|
|
124
124
|
return search;
|
|
125
125
|
}
|
|
126
126
|
function productListQuery(params = {}) {
|
|
127
|
-
const { filters, handles, ...rest } = params;
|
|
127
|
+
const { filters, handles, tags_any, ...rest } = params;
|
|
128
128
|
const search = appendParams(new URLSearchParams(), rest);
|
|
129
129
|
if (handles && handles.length > 0) search.set("handles", handles.join(","));
|
|
130
|
+
if (tags_any && tags_any.length > 0) {
|
|
131
|
+
if (tags_any.length > 5) throw new RangeError("tags_any takes at most 5 tags");
|
|
132
|
+
if (tags_any.some((t) => t.includes(",") || t.length > 120)) throw new TypeError("tags_any: a tag has at most 120 characters and no comma");
|
|
133
|
+
search.set("tags_any", tags_any.join(","));
|
|
134
|
+
}
|
|
130
135
|
if (filters) {
|
|
131
136
|
for (const [key, raw] of Object.entries(filters)) {
|
|
132
137
|
const name = key.startsWith("filter.") ? key : `filter.${key}`;
|
|
@@ -573,6 +578,19 @@ function createStorefrontClient(config) {
|
|
|
573
578
|
/** Physical delete (a profile leaves nothing behind). A saved default passes to another profile. */
|
|
574
579
|
delete: (id, options) => write("DELETE", `/storefront/customer/fiscal-profiles/${enc(id)}`, void 0, options)
|
|
575
580
|
},
|
|
581
|
+
/**
|
|
582
|
+
* Saved items of the buyer («My items», «My garage»; contract 1.9, `capabilities().subjects`). Personal data:
|
|
583
|
+
* private, never cached, never logged. `422` with `field: 'attributes.<field>'` when the kind refuses a value
|
|
584
|
+
* (it never echoes it); `409` for an item already saved or past the limit; `429` for too many saves; `404` for
|
|
585
|
+
* an item that is not the buyer's. The server builds `label` and `fit_key`; private fields come back masked.
|
|
586
|
+
*/
|
|
587
|
+
subjects: {
|
|
588
|
+
list: async (params, options) => (await get("/storefront/customer/subjects", params, { ...options, cache: "no-store" })).nodes,
|
|
589
|
+
create: (input, options) => write("POST", "/storefront/customer/subjects", input, options),
|
|
590
|
+
update: (id, patch, options) => write("PATCH", `/storefront/customer/subjects/${enc(id)}`, patch, options),
|
|
591
|
+
/** Physical delete (the item leaves nothing behind). The default of its kind passes to another item. */
|
|
592
|
+
delete: (id, options) => write("DELETE", `/storefront/customer/subjects/${enc(id)}`, void 0, options)
|
|
593
|
+
},
|
|
576
594
|
addresses: {
|
|
577
595
|
list: (options) => get("/storefront/customer/addresses", void 0, options),
|
|
578
596
|
create: (address, options) => write("POST", "/storefront/customer/addresses", address, options),
|
|
@@ -581,6 +599,11 @@ function createStorefrontClient(config) {
|
|
|
581
599
|
}
|
|
582
600
|
},
|
|
583
601
|
content: {
|
|
602
|
+
/**
|
|
603
|
+
* The kinds of buyer item the organization declares («My garage») and their catalogs (contract 1.9; public,
|
|
604
|
+
* cacheable). `[]` when it declares none or the server predates the route: the store has no such section.
|
|
605
|
+
*/
|
|
606
|
+
subjectKinds: async (options) => (await getOrNull("/storefront/subject-kinds", void 0, options))?.nodes ?? [],
|
|
584
607
|
theme: (options) => get("/storefront/content/theme", void 0, options),
|
|
585
608
|
/** `null` when the page does not exist. */
|
|
586
609
|
page: (handle, options) => getOrNull(`/storefront/content/pages/${enc(handle)}`, previewQuery(options), options),
|
|
@@ -1538,6 +1561,199 @@ function safeInvoiceFilename(name, fallback = "factura") {
|
|
|
1538
1561
|
return clean || fallback;
|
|
1539
1562
|
}
|
|
1540
1563
|
|
|
1564
|
+
// src/subjects.ts
|
|
1565
|
+
var MIN_SUBJECT_YEAR = 1900;
|
|
1566
|
+
var MAX_SUBJECT_YEAR = 2100;
|
|
1567
|
+
var MAX_VALUE_LENGTH = 80;
|
|
1568
|
+
var MAX_LABEL_LENGTH = 120;
|
|
1569
|
+
var MAX_FIT_KEY_LENGTH = 100;
|
|
1570
|
+
var isPresent = (v) => v !== null && v !== void 0 && v !== "";
|
|
1571
|
+
var asYear = (v) => {
|
|
1572
|
+
const n = typeof v === "number" ? v : typeof v === "string" && /^\s*\d+\s*$/.test(v) ? Number(v) : NaN;
|
|
1573
|
+
return Number.isInteger(n) && n >= MIN_SUBJECT_YEAR && n <= MAX_SUBJECT_YEAR ? n : null;
|
|
1574
|
+
};
|
|
1575
|
+
var cleanText = (v, max) => {
|
|
1576
|
+
if (typeof v !== "string") return null;
|
|
1577
|
+
const s = v.split(/\s+/).filter(Boolean).join(" ");
|
|
1578
|
+
return [...s].length > max || /[\u0000-\u001f\u007f-\u009f]/.test(s) ? null : s;
|
|
1579
|
+
};
|
|
1580
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9._+-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1581
|
+
function coversYear(node, year) {
|
|
1582
|
+
if (node.ranges && node.ranges.length > 0) return node.ranges.some(([a, b]) => year >= a && year <= b);
|
|
1583
|
+
if (node.from !== void 0 && year < node.from) return false;
|
|
1584
|
+
if (node.to !== void 0 && year > node.to) return false;
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
var declaresYears = (n) => n.from !== void 0 || n.to !== void 0 || (n.ranges?.length ?? 0) > 0;
|
|
1588
|
+
function subjectLevels(kind) {
|
|
1589
|
+
return kind.fields.filter((f) => f.source === "catalog" && f.type !== "year");
|
|
1590
|
+
}
|
|
1591
|
+
function subjectFields(kind) {
|
|
1592
|
+
return kind.fields;
|
|
1593
|
+
}
|
|
1594
|
+
function walk(kind, values) {
|
|
1595
|
+
const path = [];
|
|
1596
|
+
const ids = {};
|
|
1597
|
+
let tree = kind.catalog?.tree;
|
|
1598
|
+
for (const f of subjectLevels(kind)) {
|
|
1599
|
+
const raw = values[f.key];
|
|
1600
|
+
const s = isPresent(raw) ? cleanText(raw, MAX_VALUE_LENGTH) : null;
|
|
1601
|
+
const node = s && tree ? tree[s] ?? tree[s.toLowerCase()] : void 0;
|
|
1602
|
+
if (!s || !node) return { path, ids, next: tree };
|
|
1603
|
+
ids[f.key] = tree && tree[s] ? s : s.toLowerCase();
|
|
1604
|
+
path.push(node);
|
|
1605
|
+
tree = node.children;
|
|
1606
|
+
if (!tree) return { path, ids, next: void 0 };
|
|
1607
|
+
}
|
|
1608
|
+
return { path, ids, next: tree };
|
|
1609
|
+
}
|
|
1610
|
+
var byLabel = (a, b) => a.label.localeCompare(b.label, void 0, { sensitivity: "base", numeric: true });
|
|
1611
|
+
function subjectOptions(kind, fieldKey, values) {
|
|
1612
|
+
const field = kind.fields.find((f) => f.key === fieldKey);
|
|
1613
|
+
if (!field || field.source !== "catalog" || !kind.catalog) return [];
|
|
1614
|
+
if (field.type === "year") {
|
|
1615
|
+
const upto = kind.fields.slice(0, kind.fields.indexOf(field));
|
|
1616
|
+
const { path: path2 } = walk({ ...kind, fields: upto }, values);
|
|
1617
|
+
const declared = path2.filter(declaresYears);
|
|
1618
|
+
if (declared.length === 0) return [];
|
|
1619
|
+
let lo = Infinity;
|
|
1620
|
+
let hi = -Infinity;
|
|
1621
|
+
for (const n of declared) {
|
|
1622
|
+
for (const [a, b] of n.ranges?.length ? n.ranges : [[n.from ?? MIN_SUBJECT_YEAR, n.to ?? MAX_SUBJECT_YEAR]]) {
|
|
1623
|
+
lo = Math.min(lo, a);
|
|
1624
|
+
hi = Math.max(hi, b);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const out = [];
|
|
1628
|
+
for (let y = Math.min(hi, MAX_SUBJECT_YEAR); y >= Math.max(lo, MIN_SUBJECT_YEAR); y--) if (declared.every((n) => coversYear(n, y))) out.push({ value: String(y), label: String(y) });
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
const levels = subjectLevels(kind);
|
|
1632
|
+
const at = levels.findIndex((f) => f.key === fieldKey);
|
|
1633
|
+
if (at < 0) return [];
|
|
1634
|
+
const before = Object.fromEntries(levels.slice(0, at).map((f) => [f.key, values[f.key]]));
|
|
1635
|
+
const { path, next } = walk({ ...kind, fields: kind.fields.filter((f) => levels.slice(0, at).includes(f) || f.type === "year") }, before);
|
|
1636
|
+
if (path.length < at || !next) return [];
|
|
1637
|
+
const year = asYear(values[kind.fields.find((f) => f.type === "year" && f.source === "catalog")?.key ?? ""]);
|
|
1638
|
+
return Object.entries(next).filter(([, n]) => year === null || coversYear(n, year)).map(([value, n]) => ({ value, label: n.label })).sort(byLabel);
|
|
1639
|
+
}
|
|
1640
|
+
function reconcileSubjectValues(kind, values) {
|
|
1641
|
+
const out = { ...values };
|
|
1642
|
+
for (const f of kind.fields) {
|
|
1643
|
+
if (f.source !== "catalog" || !isPresent(out[f.key])) continue;
|
|
1644
|
+
const options = subjectOptions(kind, f.key, out);
|
|
1645
|
+
const v = String(out[f.key]);
|
|
1646
|
+
const ok = f.type === "year" ? options.length === 0 || options.some((o) => o.value === v) : options.some((o) => o.value === v || o.value === v.toLowerCase());
|
|
1647
|
+
if (!ok) out[f.key] = "";
|
|
1648
|
+
}
|
|
1649
|
+
return out;
|
|
1650
|
+
}
|
|
1651
|
+
function resolveSubject(kind, values) {
|
|
1652
|
+
const problems = {};
|
|
1653
|
+
const known = new Set(kind.fields.map((f) => f.key));
|
|
1654
|
+
for (const key2 of Object.keys(values)) if (isPresent(values[key2]) && !known.has(key2)) problems[key2] = "unknown";
|
|
1655
|
+
const attributes = {};
|
|
1656
|
+
const priv = {};
|
|
1657
|
+
const display = {};
|
|
1658
|
+
const ids = {};
|
|
1659
|
+
const path = [];
|
|
1660
|
+
let tree = kind.catalog?.tree;
|
|
1661
|
+
const years = [];
|
|
1662
|
+
const put = (f, v) => {
|
|
1663
|
+
if (f.private) priv[f.key] = String(v);
|
|
1664
|
+
else attributes[f.key] = v;
|
|
1665
|
+
};
|
|
1666
|
+
for (const f of kind.fields) {
|
|
1667
|
+
const raw = values[f.key];
|
|
1668
|
+
if (!isPresent(raw)) {
|
|
1669
|
+
if (f.required) problems[f.key] = "required";
|
|
1670
|
+
if (f.source === "catalog" && f.type !== "year") tree = void 0;
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
const max = f.max ?? MAX_VALUE_LENGTH;
|
|
1674
|
+
if (f.type === "year") {
|
|
1675
|
+
const y = asYear(raw);
|
|
1676
|
+
if (y === null) {
|
|
1677
|
+
problems[f.key] = "invalid";
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
put(f, y);
|
|
1681
|
+
display[f.key] = ids[f.key] = String(y);
|
|
1682
|
+
if (f.source === "catalog") years.push(f.key);
|
|
1683
|
+
} else if (f.source === "catalog") {
|
|
1684
|
+
if (!kind.catalog) {
|
|
1685
|
+
problems[f.key] = "catalog_unavailable";
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
const s = cleanText(raw, max);
|
|
1689
|
+
if (s === null) {
|
|
1690
|
+
problems[f.key] = "invalid";
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const node = tree?.[s] ?? tree?.[s.toLowerCase()];
|
|
1694
|
+
if (!node) {
|
|
1695
|
+
problems[f.key] = "not_in_catalog";
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
const id = tree?.[s] ? s : s.toLowerCase();
|
|
1699
|
+
path.push(node);
|
|
1700
|
+
tree = node.children;
|
|
1701
|
+
put(f, id);
|
|
1702
|
+
display[f.key] = node.label;
|
|
1703
|
+
ids[f.key] = id;
|
|
1704
|
+
} else {
|
|
1705
|
+
const s = cleanText(raw, max);
|
|
1706
|
+
if (s === null) {
|
|
1707
|
+
problems[f.key] = "invalid";
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
put(f, s);
|
|
1711
|
+
display[f.key] = s;
|
|
1712
|
+
ids[f.key] = slug(s);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const key2 of years) {
|
|
1716
|
+
const y = attributes[key2];
|
|
1717
|
+
if (y !== void 0 && !problems[key2] && !path.every((n) => coversYear(n, y))) problems[key2] = "year_range";
|
|
1718
|
+
}
|
|
1719
|
+
if (Object.keys(problems).length > 0) return { ok: false, problems };
|
|
1720
|
+
const label = [...kind.fields.filter((f) => !f.private).map((f) => display[f.key]).filter(Boolean).join(" ")].slice(0, MAX_LABEL_LENGTH).join("").trim();
|
|
1721
|
+
if (!label) return { ok: false, problems: { _: "required" } };
|
|
1722
|
+
let complete = true;
|
|
1723
|
+
const key = kind.fit_key_template.replace(/\{([a-z][a-z0-9_]*)\}/g, (_m, name) => {
|
|
1724
|
+
const id = ids[name];
|
|
1725
|
+
if (!id) complete = false;
|
|
1726
|
+
return id ?? "";
|
|
1727
|
+
});
|
|
1728
|
+
return { ok: true, subject: { label, fit_key: complete && key.length <= MAX_FIT_KEY_LENGTH ? key.toLowerCase() : "", attributes, private: priv } };
|
|
1729
|
+
}
|
|
1730
|
+
var PRIVATE_MASK = "\u2022\u2022";
|
|
1731
|
+
var isMasked = (v) => typeof v === "string" && v.startsWith(PRIVATE_MASK);
|
|
1732
|
+
var DEFAULT_FIT_TAG_PREFIX = "fit:";
|
|
1733
|
+
var fitTag = (fitKey, prefix = DEFAULT_FIT_TAG_PREFIX) => `${prefix}${fitKey}`;
|
|
1734
|
+
function fitTagsAny(fitKey, opts = {}) {
|
|
1735
|
+
if (!fitKey) return [];
|
|
1736
|
+
const prefix = opts.prefix ?? DEFAULT_FIT_TAG_PREFIX;
|
|
1737
|
+
return opts.universal === false ? [fitTag(fitKey, prefix)] : [fitTag(fitKey, prefix), `${prefix}universal`];
|
|
1738
|
+
}
|
|
1739
|
+
var SUBJECT_CATALOG_PAGE_BYTES = 28 * 1024;
|
|
1740
|
+
var utf8Bytes = (s) => new TextEncoder().encode(s).length;
|
|
1741
|
+
function splitSubjectTree(tree, maxBytes = SUBJECT_CATALOG_PAGE_BYTES) {
|
|
1742
|
+
const size = (part) => utf8Bytes(JSON.stringify(JSON.stringify(part)));
|
|
1743
|
+
const parts = [];
|
|
1744
|
+
let current = {};
|
|
1745
|
+
for (const [id, node] of Object.entries(tree)) {
|
|
1746
|
+
if (size({ [id]: node }) > maxBytes) throw new RangeError(`splitSubjectTree: option ${id} alone exceeds ${maxBytes} bytes`);
|
|
1747
|
+
const next = { ...current, [id]: node };
|
|
1748
|
+
if (Object.keys(current).length > 0 && size(next) > maxBytes) {
|
|
1749
|
+
parts.push(current);
|
|
1750
|
+
current = { [id]: node };
|
|
1751
|
+
} else current = next;
|
|
1752
|
+
}
|
|
1753
|
+
if (Object.keys(current).length > 0) parts.push(current);
|
|
1754
|
+
return parts;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1541
1757
|
// src/delivery.ts
|
|
1542
1758
|
function parseDelivery(value) {
|
|
1543
1759
|
const raw = (value ?? "").trim();
|
|
@@ -1566,11 +1782,16 @@ function deliveryNotices(delivery) {
|
|
|
1566
1782
|
|
|
1567
1783
|
exports.CLIENT_IP_HEADER = CLIENT_IP_HEADER;
|
|
1568
1784
|
exports.COUNTRY_HEADERS = COUNTRY_HEADERS;
|
|
1785
|
+
exports.DEFAULT_FIT_TAG_PREFIX = DEFAULT_FIT_TAG_PREFIX;
|
|
1786
|
+
exports.MAX_SUBJECT_YEAR = MAX_SUBJECT_YEAR;
|
|
1787
|
+
exports.MIN_SUBJECT_YEAR = MIN_SUBJECT_YEAR;
|
|
1569
1788
|
exports.MISSING_WEIGHT = MISSING_WEIGHT;
|
|
1570
1789
|
exports.NOTE_MAX_LENGTH = NOTE_MAX_LENGTH;
|
|
1571
1790
|
exports.PAYMENT_CHOICE_REFUSED_FIELD = PAYMENT_CHOICE_REFUSED_FIELD;
|
|
1572
1791
|
exports.PICKUP_STEPS = PICKUP_STEPS;
|
|
1792
|
+
exports.PRIVATE_MASK = PRIVATE_MASK;
|
|
1573
1793
|
exports.SHIPPING_STEPS = SHIPPING_STEPS;
|
|
1794
|
+
exports.SUBJECT_CATALOG_PAGE_BYTES = SUBJECT_CATALOG_PAGE_BYTES;
|
|
1574
1795
|
exports.StorefrontError = StorefrontError;
|
|
1575
1796
|
exports.appendParams = appendParams;
|
|
1576
1797
|
exports.availabilityLocale = availabilityLocale;
|
|
@@ -1579,6 +1800,7 @@ exports.availabilityView = availabilityView;
|
|
|
1579
1800
|
exports.cartLineProblemView = cartLineProblemView;
|
|
1580
1801
|
exports.clampPercent = clampPercent;
|
|
1581
1802
|
exports.classifyPaymentError = classifyPaymentError;
|
|
1803
|
+
exports.coversYear = coversYear;
|
|
1582
1804
|
exports.createStorefrontClient = createStorefrontClient;
|
|
1583
1805
|
exports.dataExportBytes = dataExportBytes;
|
|
1584
1806
|
exports.declineMessage = declineMessage;
|
|
@@ -1586,6 +1808,8 @@ exports.declineMessageKey = declineMessageKey;
|
|
|
1586
1808
|
exports.deliveryInput = deliveryInput;
|
|
1587
1809
|
exports.deliveryNotices = deliveryNotices;
|
|
1588
1810
|
exports.diagnose = diagnose;
|
|
1811
|
+
exports.fitTag = fitTag;
|
|
1812
|
+
exports.fitTagsAny = fitTagsAny;
|
|
1589
1813
|
exports.formatAvailabilityMessage = formatAvailabilityMessage;
|
|
1590
1814
|
exports.formatMoney = formatMoney;
|
|
1591
1815
|
exports.formatVoucherExpiry = formatVoucherExpiry;
|
|
@@ -1594,6 +1818,7 @@ exports.groupInvoiceDocs = groupInvoiceDocs;
|
|
|
1594
1818
|
exports.hreflangLinks = hreflangLinks;
|
|
1595
1819
|
exports.invoiceFileBytes = invoiceFileBytes;
|
|
1596
1820
|
exports.isAccountDeleteBlocked = isAccountDeleteBlocked;
|
|
1821
|
+
exports.isMasked = isMasked;
|
|
1597
1822
|
exports.isReauthRequired = isReauthRequired;
|
|
1598
1823
|
exports.isStorefrontError = isStorefrontError;
|
|
1599
1824
|
exports.isTestInvoice = isTestInvoice;
|
|
@@ -1619,8 +1844,10 @@ exports.productAvailability = productAvailability;
|
|
|
1619
1844
|
exports.productListQuery = productListQuery;
|
|
1620
1845
|
exports.promoProgressView = promoProgressView;
|
|
1621
1846
|
exports.promotionMessages = promotionMessages;
|
|
1847
|
+
exports.reconcileSubjectValues = reconcileSubjectValues;
|
|
1622
1848
|
exports.repairPath = repairPath;
|
|
1623
1849
|
exports.resolveAvailability = resolveAvailability;
|
|
1850
|
+
exports.resolveSubject = resolveSubject;
|
|
1624
1851
|
exports.rewardLabel = rewardLabel;
|
|
1625
1852
|
exports.safeExportFilename = safeExportFilename;
|
|
1626
1853
|
exports.safeHttpsUrl = safeHttpsUrl;
|
|
@@ -1629,7 +1856,11 @@ exports.safeTrackingUrl = safeTrackingUrl;
|
|
|
1629
1856
|
exports.schemaAvailability = schemaAvailability;
|
|
1630
1857
|
exports.shipmentsStatus = shipmentsStatus;
|
|
1631
1858
|
exports.sortTrackingEvents = sortTrackingEvents;
|
|
1859
|
+
exports.splitSubjectTree = splitSubjectTree;
|
|
1632
1860
|
exports.storefrontDomainHeader = storefrontDomainHeader;
|
|
1861
|
+
exports.subjectFields = subjectFields;
|
|
1862
|
+
exports.subjectLevels = subjectLevels;
|
|
1863
|
+
exports.subjectOptions = subjectOptions;
|
|
1633
1864
|
exports.suggestMarket = suggestMarket;
|
|
1634
1865
|
exports.toMoney = toMoney;
|
|
1635
1866
|
exports.visitorCountry = visitorCountry;
|