@blamejs/blamejs-shop 0.0.62 → 0.0.65

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.
@@ -0,0 +1,640 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.customerRoles
4
+ * @title Customer roles — B2B company-customer + employee logins
5
+ *
6
+ * @intro
7
+ * The `customers` primitive (lib/customers.js, migration 0006)
8
+ * models a single passkey-enrolled buyer per row. That shape fits
9
+ * direct-to-consumer storefronts cleanly. It does NOT fit B2B,
10
+ * where the buying entity is a company and several humans log in
11
+ * on its behalf, each carrying a different authority — a junior
12
+ * buyer who can drop items into the cart but cannot submit the
13
+ * purchase order, a manager who can submit but cannot exceed a
14
+ * payment-terms threshold, an admin who can mint and revoke
15
+ * employee logins. This primitive layers that role-and-capability
16
+ * structure on top of `customers` without modifying the underlying
17
+ * table.
18
+ *
19
+ * Composition rule: a "company customer" is just a regular row in
20
+ * `customers` (the operator decides whether it represents an
21
+ * organization or an individual). An "employee customer" is
22
+ * another regular row in `customers`. The link between them is
23
+ * `customer_role_assignments`, which carries the role slug that
24
+ * gates what the employee can do for the company.
25
+ *
26
+ * Roles are operator-authored. Each role names a capability set
27
+ * drawn from a closed enum:
28
+ *
29
+ * can_view_orders — see existing orders for the company
30
+ * can_place_order — submit a new order on behalf
31
+ * can_approve_order — sign off on an order that another
32
+ * employee staged (the audit row
33
+ * lands in `customer_order_approvals`
34
+ * via `recordOrderApproval`)
35
+ * can_manage_users — assign / unassign other employees
36
+ * can_view_pricing — see operator-confidential prices
37
+ * (negotiated discounts, contract
38
+ * rates, B2B price lists)
39
+ * can_apply_payment_terms — flip an order to net-30 / net-60
40
+ * instead of pay-now
41
+ * can_request_quote — open a sales-channel quote
42
+ * can_view_invoices — pull historical invoices for the
43
+ * company
44
+ *
45
+ * The enum is closed at this layer — `defineRole` refuses any
46
+ * capability token outside the allow-list, so a typo doesn't
47
+ * silently produce a role that grants nothing. Adding a new
48
+ * capability is a code change here, plus the operator re-defining
49
+ * the affected roles.
50
+ *
51
+ * Surface:
52
+ *
53
+ * - defineRole({ slug, title, capabilities: [...] })
54
+ * Create a role. `slug` is the stable operator handle
55
+ * (UNIQUE). Redefinition is refused — operators mutate via
56
+ * `updateRole`.
57
+ *
58
+ * - assignRole({ company_customer_id, employee_customer_id,
59
+ * role_slug })
60
+ * Attach the employee to the company at the named role. The
61
+ * pair (company, employee) is UNIQUE — re-assigning the same
62
+ * employee replaces the prior role row. Archived roles
63
+ * refuse new assignments; existing assignments keep
64
+ * resolving `hasCapability` (so an in-flight quote doesn't
65
+ * lose authority while operators rotate role definitions).
66
+ *
67
+ * - unassignRole({ company_customer_id, employee_customer_id })
68
+ * Remove the role binding. Idempotent — re-removing returns
69
+ * false. The employee's `customers` row is NOT touched; only
70
+ * the role assignment goes away.
71
+ *
72
+ * - rolesForEmployee({ employee_customer_id, company_customer_id? })
73
+ * Returns the assignment rows the employee carries. With
74
+ * `company_customer_id` set, scopes to that single pairing.
75
+ * Useful for "switch active company" UIs.
76
+ *
77
+ * - employeesForCompany(company_customer_id)
78
+ * Returns every assignment row the company has, employee +
79
+ * role joined into one row per employee.
80
+ *
81
+ * - hasCapability({ employee_customer_id, company_customer_id,
82
+ * capability })
83
+ * The fast-path gate. Returns true iff the (company,
84
+ * employee) pair has an assignment, the role row exists, and
85
+ * the role's capability list contains the requested
86
+ * capability. Missing assignment, missing role, archived
87
+ * role's capability stripped — every miss returns false.
88
+ *
89
+ * - getRole(slug) / listRoles({ active_only?, limit? }) /
90
+ * updateRole(slug, patch) / archiveRole(slug)
91
+ * Read + mutate role definitions. `updateRole` accepts
92
+ * `title` and / or `capabilities`; nothing else is allowed
93
+ * (slug is immutable).
94
+ *
95
+ * - recordOrderApproval({ order_id, approved_by, role_slug })
96
+ * Audit hook for role-gated actions. The caller composes
97
+ * `hasCapability(can_approve_order)` then this — the
98
+ * primitive doesn't double-check capability so the same
99
+ * function can record any role-gated audit (the role slug
100
+ * is captured so the auditor sees which authority the
101
+ * employee invoked under). Append-only.
102
+ *
103
+ * Storage:
104
+ * - `customer_roles`, `customer_role_assignments`,
105
+ * `customer_order_approvals` (migration
106
+ * `0101_customer_roles.sql`).
107
+ *
108
+ * @primitive customerRoles
109
+ * @related customers, order, operatorAuditLog
110
+ */
111
+
112
+ // ---- constants ----------------------------------------------------------
113
+
114
+ var MAX_SLUG_LEN = 80;
115
+ var MAX_TITLE_LEN = 200;
116
+ var MAX_LIST_LIMIT = 200;
117
+
118
+ // Slug shape matches the rest of the codebase — alnum-leading, alnum +
119
+ // hyphen + underscore + dot, capped length.
120
+ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
121
+
122
+ var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
123
+
124
+ // Closed capability enum. Adding a new capability is a code change
125
+ // here + the operator re-defines the affected roles.
126
+ var CAPABILITIES = Object.freeze([
127
+ "can_view_orders",
128
+ "can_place_order",
129
+ "can_approve_order",
130
+ "can_manage_users",
131
+ "can_view_pricing",
132
+ "can_apply_payment_terms",
133
+ "can_request_quote",
134
+ "can_view_invoices",
135
+ ]);
136
+
137
+ var ALLOWED_PATCH_COLUMNS = Object.freeze(["title", "capabilities"]);
138
+
139
+ var bShop;
140
+ function _b() {
141
+ if (!bShop) bShop = require("./index");
142
+ return bShop.framework;
143
+ }
144
+
145
+ // ---- validators ---------------------------------------------------------
146
+
147
+ function _slug(s, label) {
148
+ if (typeof s !== "string" || !SLUG_RE.test(s)) {
149
+ throw new TypeError("customerRoles: " + (label || "slug") +
150
+ " must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/ (<= " + MAX_SLUG_LEN + " chars)");
151
+ }
152
+ return s;
153
+ }
154
+
155
+ function _title(s) {
156
+ if (typeof s !== "string" || !s.length || s.length > MAX_TITLE_LEN) {
157
+ throw new TypeError("customerRoles: title must be a non-empty string <= " + MAX_TITLE_LEN + " chars");
158
+ }
159
+ if (CONTROL_BYTE_RE.test(s)) {
160
+ throw new TypeError("customerRoles: title must not contain control bytes");
161
+ }
162
+ return s;
163
+ }
164
+
165
+ function _capability(s) {
166
+ if (typeof s !== "string" || CAPABILITIES.indexOf(s) === -1) {
167
+ throw new TypeError("customerRoles: capability " + JSON.stringify(s) +
168
+ " is not in the allow-list (" + CAPABILITIES.join(", ") + ")");
169
+ }
170
+ return s;
171
+ }
172
+
173
+ function _capabilities(arr) {
174
+ if (!Array.isArray(arr) || !arr.length) {
175
+ throw new TypeError("customerRoles: capabilities must be a non-empty array of allow-list tokens");
176
+ }
177
+ var seen = Object.create(null);
178
+ var out = [];
179
+ for (var i = 0; i < arr.length; i += 1) {
180
+ var c = _capability(arr[i]);
181
+ if (seen[c]) {
182
+ throw new TypeError("customerRoles: capabilities contains duplicate " + JSON.stringify(c));
183
+ }
184
+ seen[c] = true;
185
+ out.push(c);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ function _uuid(s, label) {
191
+ try { return _b().guardUuid.sanitize(s, { profile: "strict" }); }
192
+ catch (e) { throw new TypeError("customerRoles: " + label + " — " + (e && e.message || "invalid UUID")); }
193
+ }
194
+
195
+ function _opaqueId(s, label) {
196
+ if (typeof s !== "string" || !s.length || s.length > 200) {
197
+ throw new TypeError("customerRoles: " + label + " must be a non-empty string (<= 200 chars)");
198
+ }
199
+ if (CONTROL_BYTE_RE.test(s)) {
200
+ throw new TypeError("customerRoles: " + label + " must not contain control bytes");
201
+ }
202
+ return s;
203
+ }
204
+
205
+ function _now() { return Date.now(); }
206
+
207
+ // ---- row hydration ------------------------------------------------------
208
+
209
+ function _safeParseArray(s) {
210
+ if (s == null) return [];
211
+ try {
212
+ var parsed = JSON.parse(s);
213
+ if (Array.isArray(parsed)) return parsed;
214
+ return [];
215
+ } catch (_e) {
216
+ return [];
217
+ }
218
+ }
219
+
220
+ function _hydrateRole(r) {
221
+ if (!r) return null;
222
+ // Re-filter capabilities through the allow-list on read so a
223
+ // hand-edited or migration-era row that carries a stale token is
224
+ // silently dropped instead of polluting the operator surface.
225
+ var raw = _safeParseArray(r.capabilities_json);
226
+ var caps = [];
227
+ for (var i = 0; i < raw.length; i += 1) {
228
+ if (typeof raw[i] === "string" && CAPABILITIES.indexOf(raw[i]) !== -1) {
229
+ caps.push(raw[i]);
230
+ }
231
+ }
232
+ return {
233
+ slug: r.slug,
234
+ title: r.title,
235
+ capabilities: caps,
236
+ archived_at: r.archived_at == null ? null : Number(r.archived_at),
237
+ created_at: Number(r.created_at),
238
+ updated_at: Number(r.updated_at),
239
+ };
240
+ }
241
+
242
+ function _hydrateAssignment(r) {
243
+ if (!r) return null;
244
+ return {
245
+ id: r.id,
246
+ company_customer_id: r.company_customer_id,
247
+ employee_customer_id: r.employee_customer_id,
248
+ role_slug: r.role_slug,
249
+ assigned_at: Number(r.assigned_at),
250
+ };
251
+ }
252
+
253
+ function _hydrateApproval(r) {
254
+ if (!r) return null;
255
+ return {
256
+ id: r.id,
257
+ order_id: r.order_id,
258
+ approved_by: r.approved_by,
259
+ role_slug: r.role_slug,
260
+ occurred_at: Number(r.occurred_at),
261
+ };
262
+ }
263
+
264
+ // ---- factory ------------------------------------------------------------
265
+
266
+ function create(opts) {
267
+ opts = opts || {};
268
+ var query = opts.query;
269
+ if (!query) {
270
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
271
+ }
272
+ // `customers` integration is optional — the primitive enforces UUID
273
+ // shape on every id parameter, but if the operator wires the live
274
+ // `customers` primitive in we additionally verify the referenced
275
+ // customer row exists before assigning. Tests pass a stub that
276
+ // satisfies the `.get(id)` contract; production wires the real
277
+ // `bShop.customers.create(...)` instance.
278
+ var customers = opts.customers || null;
279
+
280
+ async function _requireCustomer(id, label) {
281
+ if (!customers) return;
282
+ var row = await customers.get(id);
283
+ if (!row) {
284
+ throw new TypeError("customerRoles: " + label + " " + JSON.stringify(id) + " not found in customers");
285
+ }
286
+ }
287
+
288
+ // ---- defineRole ----------------------------------------------------
289
+
290
+ async function defineRole(input) {
291
+ if (!input || typeof input !== "object") {
292
+ throw new TypeError("customerRoles.defineRole: input object required");
293
+ }
294
+ var slug = _slug(input.slug);
295
+ var title = _title(input.title);
296
+ var caps = _capabilities(input.capabilities);
297
+
298
+ var existing = (await query(
299
+ "SELECT slug FROM customer_roles WHERE slug = ?1 LIMIT 1",
300
+ [slug],
301
+ )).rows[0];
302
+ if (existing) {
303
+ throw new TypeError("customerRoles.defineRole: slug " + JSON.stringify(slug) +
304
+ " already exists - use updateRole");
305
+ }
306
+
307
+ var ts = _now();
308
+ await query(
309
+ "INSERT INTO customer_roles (slug, title, capabilities_json, archived_at, created_at, updated_at) " +
310
+ "VALUES (?1, ?2, ?3, NULL, ?4, ?4)",
311
+ [slug, title, JSON.stringify(caps), ts],
312
+ );
313
+ return await getRole(slug);
314
+ }
315
+
316
+ // ---- getRole / listRoles -------------------------------------------
317
+
318
+ async function getRole(slug) {
319
+ _slug(slug);
320
+ var r = (await query(
321
+ "SELECT * FROM customer_roles WHERE slug = ?1 LIMIT 1",
322
+ [slug],
323
+ )).rows[0];
324
+ return _hydrateRole(r);
325
+ }
326
+
327
+ async function listRoles(listOpts) {
328
+ listOpts = listOpts || {};
329
+ var activeOnly = false;
330
+ if (listOpts.active_only != null) {
331
+ if (typeof listOpts.active_only !== "boolean") {
332
+ throw new TypeError("customerRoles.listRoles: active_only must be a boolean");
333
+ }
334
+ activeOnly = listOpts.active_only;
335
+ }
336
+ var limit = listOpts.limit == null ? 50 : listOpts.limit;
337
+ if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_LIST_LIMIT) {
338
+ throw new TypeError("customerRoles.listRoles: limit must be an integer in [1, " + MAX_LIST_LIMIT + "]");
339
+ }
340
+ var sql, params;
341
+ if (activeOnly) {
342
+ sql = "SELECT * FROM customer_roles WHERE archived_at IS NULL " +
343
+ "ORDER BY created_at ASC, slug ASC LIMIT ?1";
344
+ params = [limit];
345
+ } else {
346
+ sql = "SELECT * FROM customer_roles ORDER BY created_at ASC, slug ASC LIMIT ?1";
347
+ params = [limit];
348
+ }
349
+ var rows = (await query(sql, params)).rows;
350
+ var out = [];
351
+ for (var i = 0; i < rows.length; i += 1) out.push(_hydrateRole(rows[i]));
352
+ return out;
353
+ }
354
+
355
+ // ---- updateRole ----------------------------------------------------
356
+
357
+ async function updateRole(slug, patch) {
358
+ _slug(slug);
359
+ if (!patch || typeof patch !== "object") {
360
+ throw new TypeError("customerRoles.updateRole: patch object required");
361
+ }
362
+ var keys = Object.keys(patch);
363
+ if (!keys.length) {
364
+ throw new TypeError("customerRoles.updateRole: patch must include at least one column");
365
+ }
366
+ var current = await getRole(slug);
367
+ if (!current) {
368
+ throw new TypeError("customerRoles.updateRole: slug " + JSON.stringify(slug) + " not found");
369
+ }
370
+
371
+ var sets = [];
372
+ var params = [];
373
+ var idx = 1;
374
+ for (var i = 0; i < keys.length; i += 1) {
375
+ var col = keys[i];
376
+ if (ALLOWED_PATCH_COLUMNS.indexOf(col) === -1) {
377
+ throw new TypeError("customerRoles.updateRole: unsupported column " + JSON.stringify(col));
378
+ }
379
+ if (col === "title") {
380
+ sets.push("title = ?" + idx);
381
+ params.push(_title(patch[col]));
382
+ } else /* capabilities */ {
383
+ sets.push("capabilities_json = ?" + idx);
384
+ params.push(JSON.stringify(_capabilities(patch[col])));
385
+ }
386
+ idx += 1;
387
+ }
388
+ sets.push("updated_at = ?" + idx);
389
+ params.push(_now());
390
+ idx += 1;
391
+ params.push(slug);
392
+
393
+ var r = await query(
394
+ "UPDATE customer_roles SET " + sets.join(", ") + " WHERE slug = ?" + idx,
395
+ params,
396
+ );
397
+ if (Number(r.rowCount || 0) === 0) {
398
+ throw new TypeError("customerRoles.updateRole: slug " + JSON.stringify(slug) + " not found");
399
+ }
400
+ return await getRole(slug);
401
+ }
402
+
403
+ // ---- archiveRole ---------------------------------------------------
404
+
405
+ async function archiveRole(slug) {
406
+ _slug(slug);
407
+ var current = await getRole(slug);
408
+ if (!current) {
409
+ throw new TypeError("customerRoles.archiveRole: slug " + JSON.stringify(slug) + " not found");
410
+ }
411
+ // Idempotent — re-archive returns the existing tombstone.
412
+ if (current.archived_at != null) return current;
413
+ var ts = _now();
414
+ await query(
415
+ "UPDATE customer_roles SET archived_at = ?1, updated_at = ?1 WHERE slug = ?2",
416
+ [ts, slug],
417
+ );
418
+ return await getRole(slug);
419
+ }
420
+
421
+ // ---- assignRole ----------------------------------------------------
422
+
423
+ async function assignRole(input) {
424
+ if (!input || typeof input !== "object") {
425
+ throw new TypeError("customerRoles.assignRole: input object required");
426
+ }
427
+ var companyId = _opaqueId(input.company_customer_id, "company_customer_id");
428
+ var employeeId = _opaqueId(input.employee_customer_id, "employee_customer_id");
429
+ if (companyId === employeeId) {
430
+ throw new TypeError("customerRoles.assignRole: company_customer_id and employee_customer_id must differ");
431
+ }
432
+ var roleSlug = _slug(input.role_slug, "role_slug");
433
+
434
+ var role = await getRole(roleSlug);
435
+ if (!role) {
436
+ throw new TypeError("customerRoles.assignRole: role " + JSON.stringify(roleSlug) + " not found");
437
+ }
438
+ if (role.archived_at != null) {
439
+ throw new TypeError("customerRoles.assignRole: role " + JSON.stringify(roleSlug) +
440
+ " is archived - new assignments are refused");
441
+ }
442
+
443
+ await _requireCustomer(companyId, "company_customer_id");
444
+ await _requireCustomer(employeeId, "employee_customer_id");
445
+
446
+ var ts = _now();
447
+ var existing = (await query(
448
+ "SELECT id FROM customer_role_assignments " +
449
+ "WHERE company_customer_id = ?1 AND employee_customer_id = ?2 LIMIT 1",
450
+ [companyId, employeeId],
451
+ )).rows[0];
452
+ if (existing) {
453
+ // Replace-on-conflict — the operator changes an employee's role
454
+ // by calling assignRole again. The id + assigned_at refresh so
455
+ // the audit trail captures the rotation.
456
+ await query(
457
+ "UPDATE customer_role_assignments SET role_slug = ?1, assigned_at = ?2 WHERE id = ?3",
458
+ [roleSlug, ts, existing.id],
459
+ );
460
+ var updated = (await query(
461
+ "SELECT * FROM customer_role_assignments WHERE id = ?1",
462
+ [existing.id],
463
+ )).rows[0];
464
+ return _hydrateAssignment(updated);
465
+ }
466
+ var id = _b().uuid.v7();
467
+ await query(
468
+ "INSERT INTO customer_role_assignments " +
469
+ "(id, company_customer_id, employee_customer_id, role_slug, assigned_at) " +
470
+ "VALUES (?1, ?2, ?3, ?4, ?5)",
471
+ [id, companyId, employeeId, roleSlug, ts],
472
+ );
473
+ return {
474
+ id: id,
475
+ company_customer_id: companyId,
476
+ employee_customer_id: employeeId,
477
+ role_slug: roleSlug,
478
+ assigned_at: ts,
479
+ };
480
+ }
481
+
482
+ // ---- unassignRole --------------------------------------------------
483
+
484
+ async function unassignRole(input) {
485
+ if (!input || typeof input !== "object") {
486
+ throw new TypeError("customerRoles.unassignRole: input object required");
487
+ }
488
+ var companyId = _opaqueId(input.company_customer_id, "company_customer_id");
489
+ var employeeId = _opaqueId(input.employee_customer_id, "employee_customer_id");
490
+ var r = await query(
491
+ "DELETE FROM customer_role_assignments " +
492
+ "WHERE company_customer_id = ?1 AND employee_customer_id = ?2",
493
+ [companyId, employeeId],
494
+ );
495
+ return Number(r.rowCount || 0) > 0;
496
+ }
497
+
498
+ // ---- rolesForEmployee ----------------------------------------------
499
+
500
+ async function rolesForEmployee(input) {
501
+ if (!input || typeof input !== "object") {
502
+ throw new TypeError("customerRoles.rolesForEmployee: input object required");
503
+ }
504
+ var employeeId = _opaqueId(input.employee_customer_id, "employee_customer_id");
505
+ var rows;
506
+ if (input.company_customer_id != null) {
507
+ var companyId = _opaqueId(input.company_customer_id, "company_customer_id");
508
+ rows = (await query(
509
+ "SELECT * FROM customer_role_assignments " +
510
+ "WHERE employee_customer_id = ?1 AND company_customer_id = ?2 " +
511
+ "ORDER BY assigned_at ASC",
512
+ [employeeId, companyId],
513
+ )).rows;
514
+ } else {
515
+ rows = (await query(
516
+ "SELECT * FROM customer_role_assignments WHERE employee_customer_id = ?1 " +
517
+ "ORDER BY assigned_at ASC",
518
+ [employeeId],
519
+ )).rows;
520
+ }
521
+ var out = [];
522
+ for (var i = 0; i < rows.length; i += 1) out.push(_hydrateAssignment(rows[i]));
523
+ return out;
524
+ }
525
+
526
+ // ---- employeesForCompany ------------------------------------------
527
+
528
+ async function employeesForCompany(companyId) {
529
+ var id = _opaqueId(companyId, "company_customer_id");
530
+ var rows = (await query(
531
+ "SELECT * FROM customer_role_assignments WHERE company_customer_id = ?1 " +
532
+ "ORDER BY assigned_at ASC",
533
+ [id],
534
+ )).rows;
535
+ var out = [];
536
+ for (var i = 0; i < rows.length; i += 1) out.push(_hydrateAssignment(rows[i]));
537
+ return out;
538
+ }
539
+
540
+ // ---- hasCapability -------------------------------------------------
541
+
542
+ async function hasCapability(input) {
543
+ if (!input || typeof input !== "object") {
544
+ throw new TypeError("customerRoles.hasCapability: input object required");
545
+ }
546
+ var companyId = _opaqueId(input.company_customer_id, "company_customer_id");
547
+ var employeeId = _opaqueId(input.employee_customer_id, "employee_customer_id");
548
+ var capability = _capability(input.capability);
549
+
550
+ var asn = (await query(
551
+ "SELECT role_slug FROM customer_role_assignments " +
552
+ "WHERE company_customer_id = ?1 AND employee_customer_id = ?2 LIMIT 1",
553
+ [companyId, employeeId],
554
+ )).rows[0];
555
+ if (!asn) return false;
556
+
557
+ var role = await getRole(asn.role_slug);
558
+ if (!role) return false;
559
+ // Archived roles still resolve — the in-flight authority that an
560
+ // employee already carries is honoured. New assignments to an
561
+ // archived role are refused upstream in `assignRole`.
562
+ return role.capabilities.indexOf(capability) !== -1;
563
+ }
564
+
565
+ // ---- recordOrderApproval ------------------------------------------
566
+
567
+ async function recordOrderApproval(input) {
568
+ if (!input || typeof input !== "object") {
569
+ throw new TypeError("customerRoles.recordOrderApproval: input object required");
570
+ }
571
+ var orderId = _opaqueId(input.order_id, "order_id");
572
+ var approvedBy = _opaqueId(input.approved_by, "approved_by");
573
+ var roleSlug = _slug(input.role_slug, "role_slug");
574
+
575
+ var role = await getRole(roleSlug);
576
+ if (!role) {
577
+ throw new TypeError("customerRoles.recordOrderApproval: role " +
578
+ JSON.stringify(roleSlug) + " not found");
579
+ }
580
+
581
+ var id = _b().uuid.v7();
582
+ var ts = _now();
583
+ await query(
584
+ "INSERT INTO customer_order_approvals (id, order_id, approved_by, role_slug, occurred_at) " +
585
+ "VALUES (?1, ?2, ?3, ?4, ?5)",
586
+ [id, orderId, approvedBy, roleSlug, ts],
587
+ );
588
+ return {
589
+ id: id,
590
+ order_id: orderId,
591
+ approved_by: approvedBy,
592
+ role_slug: roleSlug,
593
+ occurred_at: ts,
594
+ };
595
+ }
596
+
597
+ async function listOrderApprovals(input) {
598
+ if (!input || typeof input !== "object") {
599
+ throw new TypeError("customerRoles.listOrderApprovals: input object required");
600
+ }
601
+ var orderId = _opaqueId(input.order_id, "order_id");
602
+ var limit = input.limit == null ? 50 : input.limit;
603
+ if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_LIST_LIMIT) {
604
+ throw new TypeError("customerRoles.listOrderApprovals: limit must be an integer in [1, " + MAX_LIST_LIMIT + "]");
605
+ }
606
+ var rows = (await query(
607
+ "SELECT * FROM customer_order_approvals WHERE order_id = ?1 " +
608
+ "ORDER BY occurred_at DESC LIMIT ?2",
609
+ [orderId, limit],
610
+ )).rows;
611
+ var out = [];
612
+ for (var i = 0; i < rows.length; i += 1) out.push(_hydrateApproval(rows[i]));
613
+ return out;
614
+ }
615
+
616
+ return {
617
+ CAPABILITIES: CAPABILITIES,
618
+ defineRole: defineRole,
619
+ getRole: getRole,
620
+ listRoles: listRoles,
621
+ updateRole: updateRole,
622
+ archiveRole: archiveRole,
623
+ assignRole: assignRole,
624
+ unassignRole: unassignRole,
625
+ rolesForEmployee: rolesForEmployee,
626
+ employeesForCompany: employeesForCompany,
627
+ hasCapability: hasCapability,
628
+ recordOrderApproval: recordOrderApproval,
629
+ listOrderApprovals: listOrderApprovals,
630
+ // Exposed for tests that want to skip the live customers integration.
631
+ _uuid: _uuid,
632
+ };
633
+ }
634
+
635
+ module.exports = {
636
+ create: create,
637
+ CAPABILITIES: CAPABILITIES,
638
+ MAX_SLUG_LEN: MAX_SLUG_LEN,
639
+ MAX_TITLE_LEN: MAX_TITLE_LEN,
640
+ };