@blamejs/blamejs-shop 0.0.52 → 0.0.54
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 +4 -0
- package/SECURITY.md +5 -3
- package/lib/analytics.js +400 -0
- package/lib/email.js +264 -0
- package/lib/giftcards.js +410 -0
- package/lib/index.js +4 -0
- package/lib/inventory-receive.js +494 -0
- package/lib/newsletter.js +176 -12
- package/lib/payment.js +193 -13
- package/lib/reviews.js +412 -0
- package/lib/storefront.js +52 -20
- package/lib/tax.js +391 -3
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/SECURITY.md +0 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.4.json +19 -0
- package/lib/webhooks.js +293 -16
- package/lib/wishlist.js +269 -0
- package/package.json +1 -1
package/lib/giftcards.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.giftcards
|
|
4
|
+
* @title Gift cards primitive — issue and redeem prepaid balance
|
|
5
|
+
*
|
|
6
|
+
* @intro
|
|
7
|
+
* A gift card is a bearer credential. Whoever knows the plaintext
|
|
8
|
+
* code can spend the balance — so the shop never stores it. On
|
|
9
|
+
* `issue` we generate a 16-character alphanumeric code from
|
|
10
|
+
* `b.crypto.generateBytes`, store the
|
|
11
|
+
* `b.crypto.namespaceHash("giftcard-code", plaintext)` digest, and
|
|
12
|
+
* return the plaintext exactly once. The issuer is responsible for
|
|
13
|
+
* delivering it to the recipient (email, paper insert, etc.).
|
|
14
|
+
*
|
|
15
|
+
* The `code_hint` is the last 4 plaintext characters — useful when
|
|
16
|
+
* an operator triaging a support request needs to identify which
|
|
17
|
+
* row the customer is asking about. Four characters of a 32-letter
|
|
18
|
+
* alphabet is 2^20 ≈ one-in-a-million space, far too small to
|
|
19
|
+
* enable brute-force recovery of the remaining 12.
|
|
20
|
+
*
|
|
21
|
+
* Recipients without an account are addressed by an email-hash
|
|
22
|
+
* (`b.crypto.namespaceHash("giftcard-recipient", email)`) so a
|
|
23
|
+
* stolen D1 dump leaks no recipient addresses while still letting
|
|
24
|
+
* the storefront resolve "this address owns these cards" after the
|
|
25
|
+
* recipient registers.
|
|
26
|
+
*
|
|
27
|
+
* Composition:
|
|
28
|
+
* var gc = bShop.giftcards.create({ query: q });
|
|
29
|
+
* var { id, code, code_hint } = await gc.issue({
|
|
30
|
+
* amount_minor: 5000, currency: "USD", issued_to_email: "alice@example.com",
|
|
31
|
+
* });
|
|
32
|
+
* // deliver `code` to the recipient. Never readable again.
|
|
33
|
+
* var view = await gc.balance(code);
|
|
34
|
+
* var { remaining_balance_minor, redemption_id } =
|
|
35
|
+
* await gc.redeem({ code: code, order_id: orderId, amount_minor: 2500 });
|
|
36
|
+
*
|
|
37
|
+
* Display formatting (`XXXX-XXXX-XXXX-XXXX`) is purely cosmetic —
|
|
38
|
+
* redemption + balance + lookup all strip hyphens (and ASCII
|
|
39
|
+
* whitespace) before hashing, so a customer who types the dashes
|
|
40
|
+
* back in works without special handling.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
var bShop;
|
|
44
|
+
function _b() {
|
|
45
|
+
if (!bShop) bShop = require("./index");
|
|
46
|
+
return bShop.framework;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var CODE_NAMESPACE = "giftcard-code";
|
|
50
|
+
var RECIPIENT_NAMESPACE = "giftcard-recipient";
|
|
51
|
+
|
|
52
|
+
// Alphabet excludes 0/O/I/1 so a code spoken aloud / read off a
|
|
53
|
+
// printed insert doesn't collapse into ambiguous characters. 32
|
|
54
|
+
// glyphs means each byte modulo-32 lands on a uniform draw (256 is a
|
|
55
|
+
// multiple of 32 — no modulo-bias correction needed).
|
|
56
|
+
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
57
|
+
var CODE_LEN = 16;
|
|
58
|
+
var CODE_HINT_LEN = 4;
|
|
59
|
+
var CODE_ALPHABET_RE = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]+$/;
|
|
60
|
+
|
|
61
|
+
var CURRENCY_RE = /^[A-Z]{3}$/;
|
|
62
|
+
var STATUSES = ["active", "redeemed", "expired", "voided"];
|
|
63
|
+
|
|
64
|
+
// ---- validators ---------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function _uuid(s, label) {
|
|
67
|
+
try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
|
|
68
|
+
catch (e) { throw new TypeError("giftcards: " + label + " — " + (e && e.message || "invalid UUID")); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _amountMinor(n, label) {
|
|
72
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
73
|
+
throw new TypeError("giftcards: " + label + " must be a positive integer (minor units)");
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _currency(c) {
|
|
79
|
+
// ISO 4217 alpha-3, uppercase. Matches the storage CHECK
|
|
80
|
+
// (length(currency) = 3) and the operator-facing convention.
|
|
81
|
+
if (typeof c !== "string" || !CURRENCY_RE.test(c)) {
|
|
82
|
+
throw new TypeError("giftcards: currency must be 3-letter uppercase ISO 4217");
|
|
83
|
+
}
|
|
84
|
+
return c;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _status(s) {
|
|
88
|
+
if (typeof s !== "string" || STATUSES.indexOf(s) === -1) {
|
|
89
|
+
throw new TypeError("giftcards: status must be one of " + STATUSES.join(", "));
|
|
90
|
+
}
|
|
91
|
+
return s;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _expiresAt(ts) {
|
|
95
|
+
if (ts == null) return null;
|
|
96
|
+
if (typeof ts !== "number" || !Number.isInteger(ts) || ts <= 0) {
|
|
97
|
+
throw new TypeError("giftcards: expires_at must be a positive integer epoch-ms or null");
|
|
98
|
+
}
|
|
99
|
+
return ts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _now() { return Date.now(); }
|
|
103
|
+
|
|
104
|
+
// ---- code generation + canonicalization ---------------------------------
|
|
105
|
+
|
|
106
|
+
// Map random bytes to the 32-character alphabet. 256 % 32 === 0 so
|
|
107
|
+
// each modulo lands on a uniform alphabet index — no rejection
|
|
108
|
+
// sampling needed. Routes through `b.crypto.generateBytes` (SHAKE256
|
|
109
|
+
// over OS-RNG) for defense-in-depth over the bare OS RNG.
|
|
110
|
+
function _generateCode() {
|
|
111
|
+
var buf = _b().crypto.generateBytes(CODE_LEN);
|
|
112
|
+
var out = "";
|
|
113
|
+
for (var i = 0; i < CODE_LEN; i += 1) {
|
|
114
|
+
out += CODE_ALPHABET.charAt(buf[i] & 31);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Display form: XXXX-XXXX-XXXX-XXXX. Pure cosmetic — the hash
|
|
120
|
+
// derivation runs on the canonicalized (hyphen-stripped) form.
|
|
121
|
+
function _formatCode(plain) {
|
|
122
|
+
return plain.slice(0, 4) + "-" + plain.slice(4, 8) + "-" + plain.slice(8, 12) + "-" + plain.slice(12, 16);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Strip hyphens + ASCII whitespace so a customer who types the
|
|
126
|
+
// dashes (or pastes with a trailing newline) works without special
|
|
127
|
+
// handling. Returns the canonical 16-character uppercase code, or
|
|
128
|
+
// throws if the result isn't a well-formed alphabet draw.
|
|
129
|
+
function _canonicalCode(input) {
|
|
130
|
+
if (typeof input !== "string" || !input.length) {
|
|
131
|
+
throw new TypeError("giftcards: code must be a non-empty string");
|
|
132
|
+
}
|
|
133
|
+
// Operator-facing affordance: tolerate hyphens + ASCII whitespace
|
|
134
|
+
// anywhere. Anything else (including unicode whitespace, control
|
|
135
|
+
// bytes, or out-of-alphabet glyphs) is a refusal — we don't want a
|
|
136
|
+
// sloppy normalizer to map two distinct codes to the same hash.
|
|
137
|
+
var stripped = input.replace(/[-\s]+/g, "").toUpperCase();
|
|
138
|
+
if (stripped.length !== CODE_LEN) {
|
|
139
|
+
throw new TypeError("giftcards: code must be " + CODE_LEN + " alphabet characters (hyphens optional)");
|
|
140
|
+
}
|
|
141
|
+
if (!CODE_ALPHABET_RE.test(stripped)) {
|
|
142
|
+
throw new TypeError("giftcards: code contains characters outside the gift-card alphabet");
|
|
143
|
+
}
|
|
144
|
+
return stripped;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _hashCode(canonical) {
|
|
148
|
+
return _b().crypto.namespaceHash(CODE_NAMESPACE, canonical);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _hashRecipient(email) {
|
|
152
|
+
// Recipient email is a free-form operator-supplied string at this
|
|
153
|
+
// tier; the storefront route layer is responsible for guardEmail
|
|
154
|
+
// validation before issuing the card. Here we only enforce
|
|
155
|
+
// shape-tier: non-empty, no control bytes (the namespaceHash
|
|
156
|
+
// primitive refuses CR/LF itself).
|
|
157
|
+
if (typeof email !== "string" || !email.length) {
|
|
158
|
+
throw new TypeError("giftcards: issued_to_email must be a non-empty string when provided");
|
|
159
|
+
}
|
|
160
|
+
// Lowercase the address before hashing so two casings of the same
|
|
161
|
+
// recipient collide on lookup. Local-part case sensitivity (RFC
|
|
162
|
+
// 5321) is operator-irrelevant for gift-card delivery — operators
|
|
163
|
+
// address the human, not the mailbox.
|
|
164
|
+
return _b().crypto.namespaceHash(RECIPIENT_NAMESPACE, email.toLowerCase());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---- factory ------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function create(opts) {
|
|
170
|
+
opts = opts || {};
|
|
171
|
+
var query = opts.query;
|
|
172
|
+
if (!query) {
|
|
173
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// `lookup` resolves a plaintext code to the live card row. Returns
|
|
177
|
+
// null on no match. The hash compare itself is constant-time
|
|
178
|
+
// because we go through `b.crypto.namespaceHash` (SHA3-512
|
|
179
|
+
// deterministic) and then SQL = comparison on the hex hash — an
|
|
180
|
+
// attacker who can time the query can't distinguish "no row" from
|
|
181
|
+
// "wrong hash" because both paths execute the same query.
|
|
182
|
+
// We additionally route the hex compare through
|
|
183
|
+
// `b.crypto.timingSafeEqual` for the returned row so a future
|
|
184
|
+
// refactor that adds non-constant-time matching can't slip in.
|
|
185
|
+
async function _lookup(plaintextCode) {
|
|
186
|
+
var canonical = _canonicalCode(plaintextCode);
|
|
187
|
+
var hash = _hashCode(canonical);
|
|
188
|
+
var r = await query(
|
|
189
|
+
"SELECT id, code_hash, balance_minor, currency, status, expires_at " +
|
|
190
|
+
"FROM giftcards WHERE code_hash = ?1",
|
|
191
|
+
[hash],
|
|
192
|
+
);
|
|
193
|
+
if (!r.rows.length) return null;
|
|
194
|
+
var row = r.rows[0];
|
|
195
|
+
// Belt-and-braces: the SQL = already matched, but route the hex
|
|
196
|
+
// strings through timingSafeEqual so the equality check leaves no
|
|
197
|
+
// micro-timing oracle in case a future schema change moves to a
|
|
198
|
+
// collection scan.
|
|
199
|
+
if (!_b().crypto.timingSafeEqual(row.code_hash, hash)) return null;
|
|
200
|
+
return {
|
|
201
|
+
id: row.id,
|
|
202
|
+
balance_minor: row.balance_minor,
|
|
203
|
+
currency: row.currency,
|
|
204
|
+
status: row.status,
|
|
205
|
+
expires_at: row.expires_at,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
CODE_NAMESPACE: CODE_NAMESPACE,
|
|
211
|
+
RECIPIENT_NAMESPACE: RECIPIENT_NAMESPACE,
|
|
212
|
+
CODE_ALPHABET: CODE_ALPHABET,
|
|
213
|
+
CODE_LEN: CODE_LEN,
|
|
214
|
+
STATUSES: STATUSES,
|
|
215
|
+
|
|
216
|
+
issue: async function (input) {
|
|
217
|
+
if (!input || typeof input !== "object") {
|
|
218
|
+
throw new TypeError("giftcards.issue: input object required");
|
|
219
|
+
}
|
|
220
|
+
_amountMinor(input.amount_minor, "amount_minor");
|
|
221
|
+
_currency(input.currency);
|
|
222
|
+
var expiresAt = _expiresAt(input.expires_at);
|
|
223
|
+
|
|
224
|
+
var issuedToCustomerId = null;
|
|
225
|
+
if (input.issued_to_customer_id != null) {
|
|
226
|
+
issuedToCustomerId = _uuid(input.issued_to_customer_id, "issued_to_customer_id");
|
|
227
|
+
}
|
|
228
|
+
var issuedToEmailHash = null;
|
|
229
|
+
if (input.issued_to_email != null) {
|
|
230
|
+
issuedToEmailHash = _hashRecipient(input.issued_to_email);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Allow neither, either, or both. A purely operator-issued card
|
|
234
|
+
// (promotion / refund credit) has no recipient identity at all.
|
|
235
|
+
|
|
236
|
+
var id = _b().uuid.v7();
|
|
237
|
+
var code = _generateCode();
|
|
238
|
+
var hash = _hashCode(code);
|
|
239
|
+
var hint = code.slice(CODE_LEN - CODE_HINT_LEN);
|
|
240
|
+
var ts = _now();
|
|
241
|
+
|
|
242
|
+
await query(
|
|
243
|
+
"INSERT INTO giftcards (id, code_hash, code_hint, currency, issued_minor, balance_minor, " +
|
|
244
|
+
"issued_to_customer_id, issued_to_email_hash, expires_at, status, created_at, updated_at) " +
|
|
245
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?5, ?6, ?7, ?8, 'active', ?9, ?9)",
|
|
246
|
+
[
|
|
247
|
+
id, hash, hint, input.currency, input.amount_minor,
|
|
248
|
+
issuedToCustomerId, issuedToEmailHash, expiresAt, ts,
|
|
249
|
+
],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// `code` is returned plaintext exactly ONCE. The issuer
|
|
253
|
+
// delivers it. Subsequent reads against this row only ever see
|
|
254
|
+
// the hash + hint.
|
|
255
|
+
return {
|
|
256
|
+
id: id,
|
|
257
|
+
code: _formatCode(code),
|
|
258
|
+
code_hint: hint,
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// Public helper — returns null on no-match (constant-time at the
|
|
263
|
+
// hash layer; see `_lookup`).
|
|
264
|
+
lookup: function (plaintextCode) {
|
|
265
|
+
return _lookup(plaintextCode);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
balance: async function (plaintextCode) {
|
|
269
|
+
var row = await _lookup(plaintextCode);
|
|
270
|
+
if (!row) return null;
|
|
271
|
+
return {
|
|
272
|
+
balance_minor: row.balance_minor,
|
|
273
|
+
currency: row.currency,
|
|
274
|
+
status: row.status,
|
|
275
|
+
expires_at: row.expires_at,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
redeem: async function (input) {
|
|
280
|
+
if (!input || typeof input !== "object") {
|
|
281
|
+
throw new TypeError("giftcards.redeem: input object required");
|
|
282
|
+
}
|
|
283
|
+
_amountMinor(input.amount_minor, "amount_minor");
|
|
284
|
+
var orderId = null;
|
|
285
|
+
if (input.order_id != null) orderId = _uuid(input.order_id, "order_id");
|
|
286
|
+
|
|
287
|
+
var row = await _lookup(input.code);
|
|
288
|
+
if (!row) {
|
|
289
|
+
var miss = new Error("giftcards.redeem: code not recognized");
|
|
290
|
+
miss.code = "GIFTCARD_NOT_FOUND";
|
|
291
|
+
throw miss;
|
|
292
|
+
}
|
|
293
|
+
if (row.status !== "active") {
|
|
294
|
+
var inactive = new Error("giftcards.redeem: card is " + row.status);
|
|
295
|
+
inactive.code = "GIFTCARD_NOT_ACTIVE";
|
|
296
|
+
throw inactive;
|
|
297
|
+
}
|
|
298
|
+
var ts = _now();
|
|
299
|
+
if (row.expires_at != null && row.expires_at <= ts) {
|
|
300
|
+
// Expired-but-still-flagged-active: lazily transition the
|
|
301
|
+
// row so future reads reflect reality, then refuse this
|
|
302
|
+
// redemption. The transition itself is idempotent — every
|
|
303
|
+
// `redeem`/`balance` caller does the same check.
|
|
304
|
+
await query(
|
|
305
|
+
"UPDATE giftcards SET status = 'expired', updated_at = ?1 WHERE id = ?2 AND status = 'active'",
|
|
306
|
+
[ts, row.id],
|
|
307
|
+
);
|
|
308
|
+
var exp = new Error("giftcards.redeem: card is expired");
|
|
309
|
+
exp.code = "GIFTCARD_EXPIRED";
|
|
310
|
+
throw exp;
|
|
311
|
+
}
|
|
312
|
+
if (input.amount_minor > row.balance_minor) {
|
|
313
|
+
var ins = new Error("giftcards.redeem: amount exceeds remaining balance");
|
|
314
|
+
ins.code = "GIFTCARD_INSUFFICIENT_BALANCE";
|
|
315
|
+
throw ins;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Atomic decrement guarded by a balance check at the SQL tier
|
|
319
|
+
// so two concurrent redemptions can't double-spend. The
|
|
320
|
+
// `balance_minor >= ?` predicate plus the row-level lock the
|
|
321
|
+
// UPDATE takes means whichever transaction lands second sees
|
|
322
|
+
// rowCount === 0 and we surface as insufficient.
|
|
323
|
+
var dec = await query(
|
|
324
|
+
"UPDATE giftcards SET balance_minor = balance_minor - ?1, " +
|
|
325
|
+
"status = CASE WHEN balance_minor - ?1 = 0 THEN 'redeemed' ELSE status END, " +
|
|
326
|
+
"updated_at = ?2 WHERE id = ?3 AND balance_minor >= ?1 AND status = 'active'",
|
|
327
|
+
[input.amount_minor, ts, row.id],
|
|
328
|
+
);
|
|
329
|
+
if (dec.rowCount === 0) {
|
|
330
|
+
// Race: another redemption beat us to the balance. Refuse
|
|
331
|
+
// with the same shape as the up-front insufficient check so
|
|
332
|
+
// the caller doesn't have to distinguish "checked then
|
|
333
|
+
// raced" from "always insufficient".
|
|
334
|
+
var raced = new Error("giftcards.redeem: amount exceeds remaining balance");
|
|
335
|
+
raced.code = "GIFTCARD_INSUFFICIENT_BALANCE";
|
|
336
|
+
throw raced;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
var redemptionId = _b().uuid.v7();
|
|
340
|
+
await query(
|
|
341
|
+
"INSERT INTO giftcard_redemptions (id, giftcard_id, order_id, amount_minor, redeemed_at) " +
|
|
342
|
+
"VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
343
|
+
[redemptionId, row.id, orderId, input.amount_minor, ts],
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
var remaining = row.balance_minor - input.amount_minor;
|
|
347
|
+
return {
|
|
348
|
+
remaining_balance_minor: remaining,
|
|
349
|
+
redemption_id: redemptionId,
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
"void": async function (id, opts2) {
|
|
354
|
+
opts2 = opts2 || {};
|
|
355
|
+
_uuid(id, "giftcard id");
|
|
356
|
+
var r = await query(
|
|
357
|
+
"SELECT id, status FROM giftcards WHERE id = ?1",
|
|
358
|
+
[id],
|
|
359
|
+
);
|
|
360
|
+
if (!r.rows.length) return null;
|
|
361
|
+
var row = r.rows[0];
|
|
362
|
+
if (row.status === "redeemed") {
|
|
363
|
+
var already = new Error("giftcards.void: card is fully redeemed");
|
|
364
|
+
already.code = "GIFTCARD_ALREADY_REDEEMED";
|
|
365
|
+
throw already;
|
|
366
|
+
}
|
|
367
|
+
if (row.status === "voided") {
|
|
368
|
+
// Idempotent — already voided; return the row as-is.
|
|
369
|
+
var existing = await query("SELECT * FROM giftcards WHERE id = ?1", [id]);
|
|
370
|
+
return existing.rows[0] || null;
|
|
371
|
+
}
|
|
372
|
+
var ts = _now();
|
|
373
|
+
await query(
|
|
374
|
+
"UPDATE giftcards SET status = 'voided', updated_at = ?1 WHERE id = ?2",
|
|
375
|
+
[ts, id],
|
|
376
|
+
);
|
|
377
|
+
// `opts2.reason` is operator-supplied free-form; it's not
|
|
378
|
+
// persisted on the row (no schema column) but accepted so a
|
|
379
|
+
// future audit-log primitive can ride alongside without a
|
|
380
|
+
// surface change.
|
|
381
|
+
void opts2.reason;
|
|
382
|
+
var after = await query("SELECT * FROM giftcards WHERE id = ?1", [id]);
|
|
383
|
+
return after.rows[0] || null;
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
listForCustomer: async function (customerId, opts3) {
|
|
387
|
+
_uuid(customerId, "customer_id");
|
|
388
|
+
opts3 = opts3 || {};
|
|
389
|
+
var sql = "SELECT * FROM giftcards WHERE issued_to_customer_id = ?1";
|
|
390
|
+
var params = [customerId];
|
|
391
|
+
if (opts3.status != null) {
|
|
392
|
+
_status(opts3.status);
|
|
393
|
+
sql += " AND status = ?2";
|
|
394
|
+
params.push(opts3.status);
|
|
395
|
+
}
|
|
396
|
+
sql += " ORDER BY created_at DESC";
|
|
397
|
+
var r = await query(sql, params);
|
|
398
|
+
return r.rows;
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
create: create,
|
|
405
|
+
CODE_NAMESPACE: CODE_NAMESPACE,
|
|
406
|
+
RECIPIENT_NAMESPACE: RECIPIENT_NAMESPACE,
|
|
407
|
+
CODE_ALPHABET: CODE_ALPHABET,
|
|
408
|
+
CODE_LEN: CODE_LEN,
|
|
409
|
+
STATUSES: STATUSES,
|
|
410
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -54,4 +54,8 @@ module.exports = {
|
|
|
54
54
|
webhooks: require("./webhooks"),
|
|
55
55
|
analytics: require("./analytics"),
|
|
56
56
|
inventoryAlerts: require("./inventory-alerts"),
|
|
57
|
+
giftcards: require("./giftcards"),
|
|
58
|
+
reviews: require("./reviews"),
|
|
59
|
+
wishlist: require("./wishlist"),
|
|
60
|
+
inventoryReceive: require("./inventory-receive"),
|
|
57
61
|
};
|