@blamejs/blamejs-shop 0.0.53 → 0.0.56
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/CHANGELOG.md +6 -0
- package/lib/addresses.js +430 -0
- package/lib/analytics.js +400 -0
- package/lib/cart-abandonment.js +664 -0
- package/lib/currency-display.js +432 -0
- package/lib/email-suppressions.js +579 -0
- package/lib/email.js +264 -0
- package/lib/index.js +14 -0
- package/lib/inventory-receive.js +494 -0
- package/lib/loyalty.js +496 -0
- package/lib/newsletter.js +176 -12
- package/lib/notifications.js +474 -0
- package/lib/order-tracking.js +456 -0
- package/lib/payment.js +193 -13
- package/lib/referrals.js +649 -0
- package/lib/returns.js +627 -0
- package/lib/reviews.js +412 -0
- package/lib/search-suggestions.js +528 -0
- package/lib/tax-exempt.js +519 -0
- package/lib/tax.js +391 -3
- package/lib/vendor/MANIFEST.json +1 -1
- package/lib/webhooks.js +293 -16
- package/lib/wishlist.js +269 -0
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.currencyDisplay
|
|
4
|
+
* @title Multi-currency display conversion + FX rate cache
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* The catalog's stored prices stay in their native currency. This
|
|
8
|
+
* primitive layers display-side conversion on top so a customer in
|
|
9
|
+
* Berlin sees the operator's USD catalog rendered in EUR (or JPY,
|
|
10
|
+
* GBP, etc.) without the operator re-pricing every SKU.
|
|
11
|
+
*
|
|
12
|
+
* Rates land in `fx_rates` (one row per (base, quote) ISO 4217
|
|
13
|
+
* pair) with an integer basis-points encoding (`rate * 10000`) and
|
|
14
|
+
* an explicit `expires_at` TTL. Operators wire a feed -- ECB,
|
|
15
|
+
* openexchangerates, fixer.io, their treasury system -- against
|
|
16
|
+
* `b.httpClient` outside this primitive and call `.setRate` or
|
|
17
|
+
* `.bulkSetRates` with the result. The framework never reaches out
|
|
18
|
+
* to a rate feed itself; that's an operator choice with operator
|
|
19
|
+
* credentials.
|
|
20
|
+
*
|
|
21
|
+
* `.convert` refuses if the source or destination currency isn't in
|
|
22
|
+
* the operator's `supportedCurrencies` allow-list -- a guard against
|
|
23
|
+
* typos rendering `"USD" -> "USS"` and silently returning whatever
|
|
24
|
+
* stale row a previous typo left behind. It also refuses (returns
|
|
25
|
+
* `null` with `stale: true`) when no row exists OR the row has
|
|
26
|
+
* expired -- staleness is never hidden from the caller, so the
|
|
27
|
+
* storefront can fall back to the catalog currency rather than
|
|
28
|
+
* showing an out-of-date converted price.
|
|
29
|
+
*
|
|
30
|
+
* Conversion math composes `b.money.convert(money, toCurrency,
|
|
31
|
+
* rateProvider)` so rounding is half-to-even (banker's), consistent
|
|
32
|
+
* with `pricing.format` + the framework Money primitive. JPY (zero-
|
|
33
|
+
* decimal) and KRW (zero-decimal) round through the same path
|
|
34
|
+
* without any per-currency special-casing.
|
|
35
|
+
*
|
|
36
|
+
* Display formatting (`.format`) wraps `Intl.NumberFormat` with the
|
|
37
|
+
* operator-supplied locale -- mirrors `pricing.format` but accepts
|
|
38
|
+
* `locale` explicitly so storefront i18n can pass the request's
|
|
39
|
+
* `Accept-Language` choice through.
|
|
40
|
+
*
|
|
41
|
+
* Composition:
|
|
42
|
+
* var fx = bShop.currencyDisplay.create({ query: q });
|
|
43
|
+
* await fx.setRate({ base: "USD", quote: "EUR", rate: 0.92, source: "ecb" });
|
|
44
|
+
* var out = await fx.convert({ amount_minor: 2999, from: "USD", to: "EUR" });
|
|
45
|
+
* // { converted_minor: 2759, rate_bps: 9200, stale: false }
|
|
46
|
+
* var html = await fx.convertAndFormat({
|
|
47
|
+
* amount_minor: 2999, from: "USD", to: "EUR", locale: "de-DE",
|
|
48
|
+
* });
|
|
49
|
+
* // { display: "27,59 €", rate_bps: 9200, stale: false }
|
|
50
|
+
*
|
|
51
|
+
* @related b.money.convert, pricing.format
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
var bShop;
|
|
55
|
+
function _b() {
|
|
56
|
+
if (!bShop) bShop = require("./index");
|
|
57
|
+
return bShop.framework;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
61
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
62
|
+
var DEFAULT_SUPPORTED = [
|
|
63
|
+
"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF",
|
|
64
|
+
"SEK", "NOK", "DKK", "NZD", "INR", "BRL", "MXN",
|
|
65
|
+
"SGD", "HKD", "KRW",
|
|
66
|
+
];
|
|
67
|
+
var BPS_SCALE = 10000;
|
|
68
|
+
var KNOWN_SOURCES_RE = /^[a-z0-9][a-z0-9._-]{0,62}[a-z0-9]$/;
|
|
69
|
+
|
|
70
|
+
function _assertCurrency(c, label) {
|
|
71
|
+
if (typeof c !== "string" || !CURRENCY_RE.test(c)) {
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
"currencyDisplay: " + label +
|
|
74
|
+
" must be a 3-letter uppercase ISO 4217 code, got " +
|
|
75
|
+
JSON.stringify(c)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _assertNonNegInt(n, label) {
|
|
81
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
"currencyDisplay: " + label +
|
|
84
|
+
" must be a non-negative integer (minor units), got " +
|
|
85
|
+
JSON.stringify(n)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _assertPositiveInt(n, label) {
|
|
91
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
"currencyDisplay: " + label +
|
|
94
|
+
" must be a positive integer, got " + JSON.stringify(n)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _assertSupported(c, supported, label) {
|
|
100
|
+
if (supported.indexOf(c) === -1) {
|
|
101
|
+
throw new TypeError(
|
|
102
|
+
"currencyDisplay: " + label + " " + JSON.stringify(c) +
|
|
103
|
+
" is not in supportedCurrencies (" + supported.join(",") + ")"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _assertSource(s) {
|
|
109
|
+
if (typeof s !== "string" || !KNOWN_SOURCES_RE.test(s)) {
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
"currencyDisplay: source must match /[a-z0-9][a-z0-9._-]*[a-z0-9]/, got " +
|
|
112
|
+
JSON.stringify(s)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Float-rate -> integer basis-points. Refuses NaN / Infinity / sub-bps
|
|
118
|
+
// positives (a rate of 1e-5 collapses to zero and loses all precision).
|
|
119
|
+
function _rateToBps(rate) {
|
|
120
|
+
if (typeof rate !== "number" || !isFinite(rate) || rate <= 0) {
|
|
121
|
+
throw new TypeError(
|
|
122
|
+
"currencyDisplay: rate must be a positive finite number, got " +
|
|
123
|
+
JSON.stringify(rate)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
var bps = Math.round(rate * BPS_SCALE);
|
|
127
|
+
if (bps <= 0) {
|
|
128
|
+
throw new TypeError(
|
|
129
|
+
"currencyDisplay: rate " + rate +
|
|
130
|
+
" is below the basis-point resolution (1/10000); refusing"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return bps;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Render an integer bps back to the decimal-shaped string `b.money.convert`
|
|
137
|
+
// expects from its rate provider. 9200 -> "0.9200" -- four fraction digits
|
|
138
|
+
// always so the rationalizer downstream sees a stable shape.
|
|
139
|
+
function _bpsToDecimal(bps) {
|
|
140
|
+
var whole = Math.floor(bps / BPS_SCALE);
|
|
141
|
+
var frac = bps % BPS_SCALE;
|
|
142
|
+
var fracStr = String(frac);
|
|
143
|
+
while (fracStr.length < 4) fracStr = "0" + fracStr;
|
|
144
|
+
return whole + "." + fracStr;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _normalizeSupported(list) {
|
|
148
|
+
if (list == null) return DEFAULT_SUPPORTED.slice();
|
|
149
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
"currencyDisplay: supportedCurrencies must be a non-empty array"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
var out = [];
|
|
155
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
156
|
+
_assertCurrency(list[i], "supportedCurrencies[" + i + "]");
|
|
157
|
+
if (out.indexOf(list[i]) === -1) out.push(list[i]);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function create(opts) {
|
|
163
|
+
opts = opts || {};
|
|
164
|
+
var query = opts.query;
|
|
165
|
+
if (!query) {
|
|
166
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
167
|
+
}
|
|
168
|
+
var defaultTtlMs = opts.defaultTtlMs == null ? DEFAULT_TTL_MS : opts.defaultTtlMs;
|
|
169
|
+
if (typeof defaultTtlMs !== "number" || !isFinite(defaultTtlMs) || defaultTtlMs <= 0) {
|
|
170
|
+
throw new TypeError(
|
|
171
|
+
"currencyDisplay.create: defaultTtlMs must be a positive number, got " +
|
|
172
|
+
JSON.stringify(opts.defaultTtlMs)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
var supported = _normalizeSupported(opts.supportedCurrencies);
|
|
176
|
+
|
|
177
|
+
function _now() { return Date.now(); }
|
|
178
|
+
|
|
179
|
+
async function setRate(input) {
|
|
180
|
+
if (!input || typeof input !== "object") {
|
|
181
|
+
throw new TypeError("currencyDisplay.setRate: input object required");
|
|
182
|
+
}
|
|
183
|
+
_assertCurrency(input.base, "base");
|
|
184
|
+
_assertCurrency(input.quote, "quote");
|
|
185
|
+
_assertSupported(input.base, supported, "base");
|
|
186
|
+
_assertSupported(input.quote, supported, "quote");
|
|
187
|
+
if (input.base === input.quote) {
|
|
188
|
+
throw new TypeError(
|
|
189
|
+
"currencyDisplay.setRate: base and quote must differ (identity rate is implicit)"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
var bps = _rateToBps(input.rate);
|
|
193
|
+
var source = input.source == null ? "manual" : input.source;
|
|
194
|
+
_assertSource(source);
|
|
195
|
+
var ttlMs = input.ttlMs == null ? defaultTtlMs : input.ttlMs;
|
|
196
|
+
if (typeof ttlMs !== "number" || !isFinite(ttlMs) || ttlMs <= 0) {
|
|
197
|
+
throw new TypeError(
|
|
198
|
+
"currencyDisplay.setRate: ttlMs must be a positive number, got " +
|
|
199
|
+
JSON.stringify(input.ttlMs)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
var fetchedAt = _now();
|
|
203
|
+
var expiresAt = fetchedAt + ttlMs;
|
|
204
|
+
// Upsert. The (base, quote) primary key collapses the operator's
|
|
205
|
+
// re-fetch into an overwrite -- the row identity is the pair, not
|
|
206
|
+
// the fetched_at timestamp.
|
|
207
|
+
await query(
|
|
208
|
+
"INSERT INTO fx_rates (base_currency, quote_currency, rate_bps, source, fetched_at, expires_at) " +
|
|
209
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6) " +
|
|
210
|
+
"ON CONFLICT(base_currency, quote_currency) DO UPDATE SET " +
|
|
211
|
+
"rate_bps = excluded.rate_bps, source = excluded.source, " +
|
|
212
|
+
"fetched_at = excluded.fetched_at, expires_at = excluded.expires_at",
|
|
213
|
+
[input.base, input.quote, bps, source, fetchedAt, expiresAt]
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
base: input.base,
|
|
217
|
+
quote: input.quote,
|
|
218
|
+
rate_bps: bps,
|
|
219
|
+
source: source,
|
|
220
|
+
fetched_at: fetchedAt,
|
|
221
|
+
expires_at: expiresAt,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function getRate(base, quote) {
|
|
226
|
+
_assertCurrency(base, "base");
|
|
227
|
+
_assertCurrency(quote, "quote");
|
|
228
|
+
var r = await query(
|
|
229
|
+
"SELECT rate_bps, source, fetched_at, expires_at " +
|
|
230
|
+
"FROM fx_rates WHERE base_currency = ?1 AND quote_currency = ?2 LIMIT 1",
|
|
231
|
+
[base, quote]
|
|
232
|
+
);
|
|
233
|
+
var row = r.rows[0];
|
|
234
|
+
if (!row) return null;
|
|
235
|
+
return {
|
|
236
|
+
rate_bps: Number(row.rate_bps),
|
|
237
|
+
source: String(row.source),
|
|
238
|
+
fetched_at: Number(row.fetched_at),
|
|
239
|
+
expires_at: Number(row.expires_at),
|
|
240
|
+
stale: Number(row.expires_at) < _now(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function convert(input) {
|
|
245
|
+
if (!input || typeof input !== "object") {
|
|
246
|
+
throw new TypeError("currencyDisplay.convert: input object required");
|
|
247
|
+
}
|
|
248
|
+
_assertNonNegInt(input.amount_minor, "amount_minor");
|
|
249
|
+
_assertCurrency(input.from, "from");
|
|
250
|
+
_assertCurrency(input.to, "to");
|
|
251
|
+
_assertSupported(input.from, supported, "from");
|
|
252
|
+
_assertSupported(input.to, supported, "to");
|
|
253
|
+
if (input.from === input.to) {
|
|
254
|
+
return { converted_minor: input.amount_minor, rate_bps: BPS_SCALE, stale: false };
|
|
255
|
+
}
|
|
256
|
+
var at = input.at == null ? _now() : input.at;
|
|
257
|
+
if (typeof at !== "number" || !isFinite(at)) {
|
|
258
|
+
throw new TypeError(
|
|
259
|
+
"currencyDisplay.convert: at must be a number (epoch-ms), got " +
|
|
260
|
+
JSON.stringify(input.at)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
var rateRow = await getRate(input.from, input.to);
|
|
264
|
+
if (!rateRow || rateRow.expires_at < at) {
|
|
265
|
+
return { converted_minor: null, rate_bps: null, stale: true };
|
|
266
|
+
}
|
|
267
|
+
// Compose b.money.convert so the rounding step is the framework's
|
|
268
|
+
// banker's-rounding implementation. Build a single-call rate
|
|
269
|
+
// provider on the fly -- the framework expects a decimal-shaped
|
|
270
|
+
// string from `.rate(from, to)`.
|
|
271
|
+
var money = _b().money.fromMinorUnits(BigInt(input.amount_minor), input.from);
|
|
272
|
+
var rateStr = _bpsToDecimal(rateRow.rate_bps);
|
|
273
|
+
var provider = { rate: function (_from, _to) { return rateStr; } };
|
|
274
|
+
var converted = _b().money.convert(money, input.to, provider);
|
|
275
|
+
return {
|
|
276
|
+
converted_minor: Number(converted.toMinorUnits()),
|
|
277
|
+
rate_bps: rateRow.rate_bps,
|
|
278
|
+
stale: false,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function format(input) {
|
|
283
|
+
if (!input || typeof input !== "object") {
|
|
284
|
+
throw new TypeError("currencyDisplay.format: input object required");
|
|
285
|
+
}
|
|
286
|
+
_assertNonNegInt(input.amount_minor, "amount_minor");
|
|
287
|
+
_assertCurrency(input.currency, "currency");
|
|
288
|
+
var locale = input.locale == null ? "en-US" : input.locale;
|
|
289
|
+
if (typeof locale !== "string" || !locale.length) {
|
|
290
|
+
throw new TypeError(
|
|
291
|
+
"currencyDisplay.format: locale must be a non-empty string, got " +
|
|
292
|
+
JSON.stringify(input.locale)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
// Mirror pricing.format -- delegate to b.money so the ISO 4217
|
|
296
|
+
// exponent (JPY 0, USD 2, BHD 3) is correct without local
|
|
297
|
+
// bookkeeping.
|
|
298
|
+
var money = _b().money.fromMinorUnits(BigInt(input.amount_minor), input.currency);
|
|
299
|
+
return money.format(locale);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function convertAndFormat(input) {
|
|
303
|
+
if (!input || typeof input !== "object") {
|
|
304
|
+
throw new TypeError("currencyDisplay.convertAndFormat: input object required");
|
|
305
|
+
}
|
|
306
|
+
var locale = input.locale == null ? "en-US" : input.locale;
|
|
307
|
+
var converted = await convert({
|
|
308
|
+
amount_minor: input.amount_minor,
|
|
309
|
+
from: input.from,
|
|
310
|
+
to: input.to,
|
|
311
|
+
at: input.at,
|
|
312
|
+
});
|
|
313
|
+
if (converted.converted_minor == null) {
|
|
314
|
+
return {
|
|
315
|
+
display: null,
|
|
316
|
+
rate_bps: null,
|
|
317
|
+
stale: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
var display = format({
|
|
321
|
+
amount_minor: converted.converted_minor,
|
|
322
|
+
currency: input.to,
|
|
323
|
+
locale: locale,
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
display: display,
|
|
327
|
+
converted_minor: converted.converted_minor,
|
|
328
|
+
rate_bps: converted.rate_bps,
|
|
329
|
+
stale: converted.stale,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function cleanupExpired(ts) {
|
|
334
|
+
var cutoff = ts == null ? _now() : ts;
|
|
335
|
+
if (typeof cutoff !== "number" || !isFinite(cutoff)) {
|
|
336
|
+
throw new TypeError(
|
|
337
|
+
"currencyDisplay.cleanupExpired: ts must be a number (epoch-ms), got " +
|
|
338
|
+
JSON.stringify(ts)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
var r = await query(
|
|
342
|
+
"DELETE FROM fx_rates WHERE expires_at < ?1",
|
|
343
|
+
[cutoff]
|
|
344
|
+
);
|
|
345
|
+
return Number(r.rowCount || 0);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Operator nightly-import path. Validates EVERY row before writing
|
|
349
|
+
// any -- a bad row in the batch surfaces as a thrown TypeError with
|
|
350
|
+
// the row's index and the offending field, and zero rows land in
|
|
351
|
+
// the table. (D1 has no client-side transactions; the pre-flight
|
|
352
|
+
// validate-then-write pattern is the atomicity story.)
|
|
353
|
+
async function bulkSetRates(rows) {
|
|
354
|
+
if (!Array.isArray(rows)) {
|
|
355
|
+
throw new TypeError("currencyDisplay.bulkSetRates: rows must be an array");
|
|
356
|
+
}
|
|
357
|
+
if (rows.length === 0) return { written: 0 };
|
|
358
|
+
var normalized = [];
|
|
359
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
360
|
+
var row = rows[i];
|
|
361
|
+
if (!row || typeof row !== "object") {
|
|
362
|
+
throw new TypeError(
|
|
363
|
+
"currencyDisplay.bulkSetRates: row[" + i + "] must be an object"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
_assertCurrency(row.base, "base");
|
|
368
|
+
_assertCurrency(row.quote, "quote");
|
|
369
|
+
_assertSupported(row.base, supported, "base");
|
|
370
|
+
_assertSupported(row.quote, supported, "quote");
|
|
371
|
+
if (row.base === row.quote) {
|
|
372
|
+
throw new TypeError(
|
|
373
|
+
"base and quote must differ (identity rate is implicit)"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
var bps = _rateToBps(row.rate);
|
|
377
|
+
var source = row.source == null ? "manual" : row.source;
|
|
378
|
+
_assertSource(source);
|
|
379
|
+
var ttlMs = row.ttlMs == null ? defaultTtlMs : row.ttlMs;
|
|
380
|
+
if (typeof ttlMs !== "number" || !isFinite(ttlMs) || ttlMs <= 0) {
|
|
381
|
+
throw new TypeError("ttlMs must be a positive number");
|
|
382
|
+
}
|
|
383
|
+
normalized.push({
|
|
384
|
+
base: row.base,
|
|
385
|
+
quote: row.quote,
|
|
386
|
+
bps: bps,
|
|
387
|
+
source: source,
|
|
388
|
+
ttlMs: ttlMs,
|
|
389
|
+
});
|
|
390
|
+
} catch (e) {
|
|
391
|
+
throw new TypeError(
|
|
392
|
+
"currencyDisplay.bulkSetRates: row[" + i + "] — " +
|
|
393
|
+
(e && e.message || "invalid")
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
var fetchedAt = _now();
|
|
398
|
+
var written = 0;
|
|
399
|
+
for (var j = 0; j < normalized.length; j += 1) {
|
|
400
|
+
var n = normalized[j];
|
|
401
|
+
var expiresAt = fetchedAt + n.ttlMs;
|
|
402
|
+
await query(
|
|
403
|
+
"INSERT INTO fx_rates (base_currency, quote_currency, rate_bps, source, fetched_at, expires_at) " +
|
|
404
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6) " +
|
|
405
|
+
"ON CONFLICT(base_currency, quote_currency) DO UPDATE SET " +
|
|
406
|
+
"rate_bps = excluded.rate_bps, source = excluded.source, " +
|
|
407
|
+
"fetched_at = excluded.fetched_at, expires_at = excluded.expires_at",
|
|
408
|
+
[n.base, n.quote, n.bps, n.source, fetchedAt, expiresAt]
|
|
409
|
+
);
|
|
410
|
+
written += 1;
|
|
411
|
+
}
|
|
412
|
+
return { written: written, fetched_at: fetchedAt };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
supportedCurrencies: supported.slice(),
|
|
417
|
+
setRate: setRate,
|
|
418
|
+
getRate: getRate,
|
|
419
|
+
convert: convert,
|
|
420
|
+
format: format,
|
|
421
|
+
convertAndFormat: convertAndFormat,
|
|
422
|
+
cleanupExpired: cleanupExpired,
|
|
423
|
+
bulkSetRates: bulkSetRates,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
create: create,
|
|
429
|
+
DEFAULT_TTL_MS: DEFAULT_TTL_MS,
|
|
430
|
+
DEFAULT_SUPPORTED: DEFAULT_SUPPORTED,
|
|
431
|
+
BPS_SCALE: BPS_SCALE,
|
|
432
|
+
};
|