@blamejs/blamejs-shop 0.0.65 → 0.0.70

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/assembly-instructions.js +777 -0
  3. package/lib/auto-replenish.js +933 -0
  4. package/lib/business-hours.js +980 -0
  5. package/lib/click-and-collect.js +711 -0
  6. package/lib/clickstream.js +713 -0
  7. package/lib/cost-layers.js +774 -0
  8. package/lib/credit-limits.js +752 -0
  9. package/lib/currency-rounding.js +525 -0
  10. package/lib/customer-activity.js +862 -0
  11. package/lib/customer-notes.js +712 -0
  12. package/lib/customer-risk-profile.js +593 -0
  13. package/lib/customer-surveys.js +1012 -0
  14. package/lib/damage-photos.js +473 -0
  15. package/lib/discount-allocation.js +557 -0
  16. package/lib/dropship-forwarding.js +645 -0
  17. package/lib/email-templates.js +817 -0
  18. package/lib/index.js +45 -0
  19. package/lib/inventory-allocations.js +559 -0
  20. package/lib/inventory-writeoffs.js +636 -0
  21. package/lib/knowledge-base.js +1104 -0
  22. package/lib/locale-router.js +1077 -0
  23. package/lib/operator-roles.js +768 -0
  24. package/lib/order-escalation.js +951 -0
  25. package/lib/order-ratings.js +495 -0
  26. package/lib/order-tags.js +944 -0
  27. package/lib/packing-slips.js +810 -0
  28. package/lib/payment-retries.js +816 -0
  29. package/lib/pick-lists.js +639 -0
  30. package/lib/pixel-events.js +995 -0
  31. package/lib/preorder.js +595 -0
  32. package/lib/print-queue.js +681 -0
  33. package/lib/product-qa.js +749 -0
  34. package/lib/promo-bundles.js +835 -0
  35. package/lib/push-notifications.js +937 -0
  36. package/lib/refund-automation.js +853 -0
  37. package/lib/reorder-reminders.js +798 -0
  38. package/lib/robots-config.js +753 -0
  39. package/lib/seller-signup.js +1052 -0
  40. package/lib/site-redirects.js +690 -0
  41. package/lib/sitemap-generator.js +717 -0
  42. package/lib/subscription-gifts.js +710 -0
  43. package/lib/tax-cert-renewals.js +632 -0
  44. package/lib/theme-assets.js +711 -0
  45. package/lib/tier-benefits.js +776 -0
  46. package/lib/vendor/MANIFEST.json +2 -2
  47. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  48. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  49. package/lib/vendor/blamejs/lib/metrics.js +68 -4
  50. package/lib/vendor/blamejs/package.json +1 -1
  51. package/lib/vendor/blamejs/release-notes/v0.12.5.json +40 -0
  52. package/lib/wishlist-alerts.js +842 -0
  53. package/lib/wishlist-sharing.js +718 -0
  54. package/package.json +1 -1
@@ -0,0 +1,1052 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.sellerSignup
4
+ * @title Seller signup — marketplace seller onboarding funnel
5
+ *
6
+ * @intro
7
+ * The inbound application-to-vendor pipeline. A prospective
8
+ * seller submits an application; the operator (or an internal
9
+ * KYC/AML team) requests supporting documents, the applicant
10
+ * uploads them, the operator either approves (the primitive
11
+ * composes `vendors.registerVendor` to mint the real vendor
12
+ * row), rejects (terminal, reason captured), or requests
13
+ * revisions (re-opens for additional docs).
14
+ *
15
+ * Distinct from `vendors` (the registry of suppliers the
16
+ * operator drop-ships from — the OUTPUT of this primitive on a
17
+ * happy approval path) and from `customer_roles` (which models
18
+ * B2B account hierarchy inside a single customer entity — a
19
+ * distinct relationship class, not a vendor pipeline).
20
+ *
21
+ * FSM:
22
+ *
23
+ * submitted -> docs_pending (requestDocument)
24
+ * docs_pending -> in_review (every requested doc uploaded)
25
+ * in_review -> approved (approveApplication, terminal,
26
+ * composes vendors.registerVendor)
27
+ * in_review -> revisions_requested (requestRevisions)
28
+ * revisions_requested -> docs_pending (auto on first new
29
+ * requestDocument while in this
30
+ * state)
31
+ * submitted | docs_pending | in_review | revisions_requested ->
32
+ * rejected (rejectApplication, terminal)
33
+ *
34
+ * (approved and rejected are terminal — no transitions out.)
35
+ *
36
+ * Composes:
37
+ * - vendors — composed at approveApplication time
38
+ * to mint the real vendor row.
39
+ * Injected as `opts.vendors`.
40
+ * Required at approve time; absent at
41
+ * construction means submit / doc /
42
+ * reject / revisions all still work,
43
+ * but approveApplication refuses.
44
+ * - b.crypto.namespaceHash — contact_email / contact_phone /
45
+ * tax_id are hashed under three
46
+ * distinct namespaces so the raw
47
+ * values never land on disk
48
+ * - b.guardEmail — strict-profile validate + sanitize
49
+ * on contact_email
50
+ * - b.uuid.v7 — application + document PKs
51
+ * (sortable; B-tree locality so the
52
+ * operator inbox keyset-paginates
53
+ * without a second index)
54
+ *
55
+ * Surface:
56
+ * submitApplication({ business_name, contact_email,
57
+ * contact_phone, business_address,
58
+ * tax_id_kind, tax_id_value, category_focus,
59
+ * expected_monthly_volume_minor, currency,
60
+ * references_json? })
61
+ * requestDocument({ application_id, doc_kind, instructions })
62
+ * recordDocumentUploaded({ application_id, doc_kind, sha3_512,
63
+ * byte_size, mime_type })
64
+ * approveApplication({ application_id, approved_by, vendor_slug })
65
+ * rejectApplication({ application_id, reason, rejected_by })
66
+ * requestRevisions({ application_id, fields, instructions })
67
+ * getApplication(id) / listApplications({ status?, cursor? })
68
+ * documentsForApplication(application_id)
69
+ *
70
+ * Storage: `migrations-d1/0146_seller_signup.sql` — two tables,
71
+ * `seller_applications` + `seller_application_documents`. ON
72
+ * DELETE CASCADE drops the docs when the application row is
73
+ * hard-deleted (the primitive only soft-transitions; hard-
74
+ * delete is an operator-side privacy-erasure migration concern).
75
+ *
76
+ * @primitive sellerSignup
77
+ * @related shop.vendors, shop.customerRoles, b.crypto, b.guardEmail, b.uuid
78
+ */
79
+
80
+ var MAX_BUSINESS_NAME_LEN = 200;
81
+ var MAX_PHONE_LEN = 32;
82
+ var MAX_TAX_ID_LEN = 64;
83
+ var MAX_ADDRESS_JSON_LEN = 4096;
84
+ var MAX_CATEGORY_FOCUS_LEN = 4096;
85
+ var MAX_REFERENCES_JSON_LEN = 8192;
86
+ var MAX_INSTRUCTIONS_LEN = 2000;
87
+ var MAX_REJECT_REASON_LEN = 1000;
88
+ var MAX_OPERATOR_ID_LEN = 256;
89
+ var MAX_MIME_TYPE_LEN = 128;
90
+ var MAX_FIELDS_LEN = 32;
91
+ var MAX_FIELD_NAME_LEN = 64;
92
+ var MAX_LIST_LIMIT = 200;
93
+ var MAX_VOLUME_MINOR = 100000000000; // 1e11 sanity cap
94
+ var MAX_BYTE_SIZE = 5368709120; // 5 GiB sanity cap per doc
95
+
96
+ var EMAIL_NAMESPACE = "seller-signup-email";
97
+ var PHONE_NAMESPACE = "seller-signup-phone";
98
+ var TAX_ID_NAMESPACE = "seller-signup-tax-id";
99
+
100
+ var TAX_ID_KINDS = Object.freeze([
101
+ "us_ssn", "us_ein", "eu_vat", "uk_utr", "au_abn", "ca_bn", "other",
102
+ ]);
103
+
104
+ var DOC_KINDS = Object.freeze([
105
+ "w9", "w8ben", "id", "business_license", "utility_bill",
106
+ "insurance", "bank_statement", "other",
107
+ ]);
108
+
109
+ var APPLICATION_STATUSES = Object.freeze([
110
+ "submitted", "docs_pending", "in_review", "revisions_requested",
111
+ "approved", "rejected", "withdrawn",
112
+ ]);
113
+
114
+ var DOCUMENT_STATUSES = Object.freeze([
115
+ "requested", "uploaded", "accepted", "rejected",
116
+ ]);
117
+
118
+ var TERMINAL_APPLICATION_STATUSES = Object.freeze([
119
+ "approved", "rejected", "withdrawn",
120
+ ]);
121
+
122
+ var PHONE_RE = /^\+?[1-9]\d{1,14}$/;
123
+ var TAX_ID_RE = /^[A-Za-z0-9][A-Za-z0-9 .\-_/]{0,63}$/;
124
+ var FIELD_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
125
+ var SHA3_512_RE = /^[0-9a-f]{128}$/;
126
+ var MIME_RE = /^[A-Za-z0-9!#$&^_.+\-]{1,127}\/[A-Za-z0-9!#$&^_.+\-]{1,127}$/;
127
+
128
+ // Control bytes + zero-width / direction-override family — matches
129
+ // the shape vendors / purchase-orders use. Operator-rendered fields
130
+ // refuse these to keep downstream dashboards / KYC printouts safe
131
+ // from header-injection + visual-spoofing attacks.
132
+ var CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
133
+ var ZERO_WIDTH_RE = new RegExp(
134
+ "[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
135
+ );
136
+
137
+ // Lazy framework handle — matches the pattern every other shop
138
+ // primitive uses; avoids the require cycle that would arise from
139
+ // importing `./index` at module-eval time.
140
+ var bShop;
141
+ function _b() {
142
+ if (!bShop) bShop = require("./index");
143
+ return bShop.framework;
144
+ }
145
+
146
+ // ---- validators ---------------------------------------------------------
147
+
148
+ function _uuid(s, label) {
149
+ try {
150
+ return _b().guardUuid.sanitize(s, { profile: "strict" });
151
+ } catch (e) {
152
+ throw new TypeError("seller-signup: " + label + " — " + (e && e.message || "invalid UUID"));
153
+ }
154
+ }
155
+
156
+ function _businessName(s) {
157
+ if (typeof s !== "string") {
158
+ throw new TypeError("seller-signup: business_name must be a string");
159
+ }
160
+ var trimmed = s.trim();
161
+ if (!trimmed.length) {
162
+ throw new TypeError("seller-signup: business_name must be non-empty after trim");
163
+ }
164
+ if (s.length > MAX_BUSINESS_NAME_LEN) {
165
+ throw new TypeError("seller-signup: business_name must be <= " + MAX_BUSINESS_NAME_LEN + " characters");
166
+ }
167
+ if (CONTROL_BYTE_RE.test(s)) {
168
+ throw new TypeError("seller-signup: business_name contains control bytes");
169
+ }
170
+ if (ZERO_WIDTH_RE.test(s)) {
171
+ throw new TypeError("seller-signup: business_name contains zero-width / direction-override bytes");
172
+ }
173
+ return s;
174
+ }
175
+
176
+ function _normalizeEmail(input) {
177
+ if (typeof input !== "string" || !input.length) {
178
+ throw new TypeError("seller-signup: contact_email must be a non-empty string");
179
+ }
180
+ var guardEmail = _b().guardEmail;
181
+ var report;
182
+ try {
183
+ report = guardEmail.validate(input, { profile: "strict" });
184
+ } catch (e) {
185
+ throw new TypeError("seller-signup: contact_email — " + (e && e.message || "invalid email"));
186
+ }
187
+ if (!report || report.ok === false) {
188
+ var first = (report && report.issues && report.issues[0]) || {};
189
+ throw new TypeError("seller-signup: contact_email — " + (first.snippet || first.ruleId || "refused at strict profile"));
190
+ }
191
+ var canonical;
192
+ try {
193
+ canonical = guardEmail.sanitize(input, { profile: "strict" });
194
+ } catch (e2) {
195
+ throw new TypeError("seller-signup: contact_email — " + (e2 && e2.message || "refused"));
196
+ }
197
+ return canonical.trim().toLowerCase();
198
+ }
199
+
200
+ function _phone(value) {
201
+ if (typeof value !== "string" || !value.length) {
202
+ throw new TypeError("seller-signup: contact_phone must be a non-empty string");
203
+ }
204
+ var trimmed = value.trim();
205
+ if (!trimmed.length) {
206
+ throw new TypeError("seller-signup: contact_phone must be non-empty after trim");
207
+ }
208
+ if (trimmed.length > MAX_PHONE_LEN) {
209
+ throw new TypeError("seller-signup: contact_phone must be <= " + MAX_PHONE_LEN + " characters");
210
+ }
211
+ if (!PHONE_RE.test(trimmed)) {
212
+ throw new TypeError("seller-signup: contact_phone must match E.164-ish shape (^\\+?[1-9]\\d{1,14}$)");
213
+ }
214
+ return trimmed;
215
+ }
216
+
217
+ function _businessAddress(value) {
218
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
219
+ throw new TypeError("seller-signup: business_address must be a plain object");
220
+ }
221
+ var encoded;
222
+ try { encoded = JSON.stringify(value); }
223
+ catch (_e) {
224
+ throw new TypeError("seller-signup: business_address must be JSON-serialisable");
225
+ }
226
+ if (encoded.length > MAX_ADDRESS_JSON_LEN) {
227
+ throw new TypeError("seller-signup: business_address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
228
+ }
229
+ if (CONTROL_BYTE_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
230
+ throw new TypeError("seller-signup: business_address contains control / zero-width bytes");
231
+ }
232
+ return encoded;
233
+ }
234
+
235
+ function _taxIdKind(s) {
236
+ if (typeof s !== "string" || TAX_ID_KINDS.indexOf(s) === -1) {
237
+ throw new TypeError("seller-signup: tax_id_kind must be one of " + TAX_ID_KINDS.join(", "));
238
+ }
239
+ return s;
240
+ }
241
+
242
+ function _taxIdValue(s) {
243
+ if (typeof s !== "string" || !s.length) {
244
+ throw new TypeError("seller-signup: tax_id_value must be a non-empty string");
245
+ }
246
+ var trimmed = s.trim();
247
+ if (!trimmed.length) {
248
+ throw new TypeError("seller-signup: tax_id_value must be non-empty after trim");
249
+ }
250
+ if (trimmed.length > MAX_TAX_ID_LEN) {
251
+ throw new TypeError("seller-signup: tax_id_value must be <= " + MAX_TAX_ID_LEN + " characters");
252
+ }
253
+ if (!TAX_ID_RE.test(trimmed)) {
254
+ throw new TypeError("seller-signup: tax_id_value must match /^[A-Za-z0-9][A-Za-z0-9 .\\-_/]*$/");
255
+ }
256
+ return trimmed;
257
+ }
258
+
259
+ function _categoryFocus(value) {
260
+ if (!Array.isArray(value)) {
261
+ throw new TypeError("seller-signup: category_focus must be a non-empty array");
262
+ }
263
+ if (value.length === 0) {
264
+ throw new TypeError("seller-signup: category_focus must contain at least one category");
265
+ }
266
+ for (var i = 0; i < value.length; i += 1) {
267
+ if (typeof value[i] !== "string" || !value[i].length) {
268
+ throw new TypeError("seller-signup: category_focus[" + i + "] must be a non-empty string");
269
+ }
270
+ if (CONTROL_BYTE_RE.test(value[i]) || ZERO_WIDTH_RE.test(value[i])) {
271
+ throw new TypeError("seller-signup: category_focus[" + i + "] contains control / zero-width bytes");
272
+ }
273
+ }
274
+ var encoded;
275
+ try { encoded = JSON.stringify(value); }
276
+ catch (_e) {
277
+ throw new TypeError("seller-signup: category_focus must be JSON-serialisable");
278
+ }
279
+ if (encoded.length > MAX_CATEGORY_FOCUS_LEN) {
280
+ throw new TypeError("seller-signup: category_focus JSON must be <= " + MAX_CATEGORY_FOCUS_LEN + " characters serialised");
281
+ }
282
+ return encoded;
283
+ }
284
+
285
+ function _volumeMinor(n) {
286
+ if (!Number.isInteger(n) || n < 0) {
287
+ throw new TypeError("seller-signup: expected_monthly_volume_minor must be a non-negative integer");
288
+ }
289
+ if (n > MAX_VOLUME_MINOR) {
290
+ throw new TypeError("seller-signup: expected_monthly_volume_minor must be <= " + MAX_VOLUME_MINOR);
291
+ }
292
+ return n;
293
+ }
294
+
295
+ function _currency(s) {
296
+ if (typeof s !== "string" || !/^[A-Z]{3}$/.test(s)) {
297
+ throw new TypeError("seller-signup: currency must be a 3-letter uppercase ISO-4217 code");
298
+ }
299
+ return s;
300
+ }
301
+
302
+ function _referencesJson(value) {
303
+ if (value == null) return null;
304
+ if (typeof value !== "object") {
305
+ throw new TypeError("seller-signup: references_json must be an object / array or null");
306
+ }
307
+ var encoded;
308
+ try { encoded = JSON.stringify(value); }
309
+ catch (_e) {
310
+ throw new TypeError("seller-signup: references_json must be JSON-serialisable");
311
+ }
312
+ if (encoded.length > MAX_REFERENCES_JSON_LEN) {
313
+ throw new TypeError("seller-signup: references_json must be <= " + MAX_REFERENCES_JSON_LEN + " characters serialised");
314
+ }
315
+ if (CONTROL_BYTE_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
316
+ throw new TypeError("seller-signup: references_json contains control / zero-width bytes");
317
+ }
318
+ return encoded;
319
+ }
320
+
321
+ function _docKind(s) {
322
+ if (typeof s !== "string" || DOC_KINDS.indexOf(s) === -1) {
323
+ throw new TypeError("seller-signup: doc_kind must be one of " + DOC_KINDS.join(", "));
324
+ }
325
+ return s;
326
+ }
327
+
328
+ function _instructions(s) {
329
+ if (typeof s !== "string" || !s.length) {
330
+ throw new TypeError("seller-signup: instructions must be a non-empty string");
331
+ }
332
+ if (s.length > MAX_INSTRUCTIONS_LEN) {
333
+ throw new TypeError("seller-signup: instructions must be <= " + MAX_INSTRUCTIONS_LEN + " characters");
334
+ }
335
+ if (CONTROL_BYTE_RE.test(s.replace(/[\t\n\r]/g, ""))) {
336
+ throw new TypeError("seller-signup: instructions contains control bytes");
337
+ }
338
+ if (ZERO_WIDTH_RE.test(s)) {
339
+ throw new TypeError("seller-signup: instructions contains zero-width / direction-override bytes");
340
+ }
341
+ return s;
342
+ }
343
+
344
+ function _rejectReason(s) {
345
+ if (typeof s !== "string" || !s.length) {
346
+ throw new TypeError("seller-signup: reason must be a non-empty string");
347
+ }
348
+ if (s.length > MAX_REJECT_REASON_LEN) {
349
+ throw new TypeError("seller-signup: reason must be <= " + MAX_REJECT_REASON_LEN + " characters");
350
+ }
351
+ if (CONTROL_BYTE_RE.test(s.replace(/[\t\n\r]/g, ""))) {
352
+ throw new TypeError("seller-signup: reason contains control bytes");
353
+ }
354
+ if (ZERO_WIDTH_RE.test(s)) {
355
+ throw new TypeError("seller-signup: reason contains zero-width / direction-override bytes");
356
+ }
357
+ return s;
358
+ }
359
+
360
+ function _operatorId(s, label) {
361
+ if (typeof s !== "string" || !s.length) {
362
+ throw new TypeError("seller-signup: " + label + " must be a non-empty string");
363
+ }
364
+ if (s.length > MAX_OPERATOR_ID_LEN) {
365
+ throw new TypeError("seller-signup: " + label + " must be <= " + MAX_OPERATOR_ID_LEN + " characters");
366
+ }
367
+ if (CONTROL_BYTE_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
368
+ throw new TypeError("seller-signup: " + label + " contains control / zero-width bytes");
369
+ }
370
+ return s;
371
+ }
372
+
373
+ function _sha3_512(s) {
374
+ if (typeof s !== "string") {
375
+ throw new TypeError("seller-signup: sha3_512 must be a string");
376
+ }
377
+ if (!SHA3_512_RE.test(s)) {
378
+ throw new TypeError("seller-signup: sha3_512 must be 128 lowercase hex characters");
379
+ }
380
+ return s;
381
+ }
382
+
383
+ function _byteSize(n) {
384
+ if (!Number.isInteger(n) || n <= 0) {
385
+ throw new TypeError("seller-signup: byte_size must be a positive integer");
386
+ }
387
+ if (n > MAX_BYTE_SIZE) {
388
+ throw new TypeError("seller-signup: byte_size must be <= " + MAX_BYTE_SIZE);
389
+ }
390
+ return n;
391
+ }
392
+
393
+ function _mimeType(s) {
394
+ if (typeof s !== "string" || !s.length) {
395
+ throw new TypeError("seller-signup: mime_type must be a non-empty string");
396
+ }
397
+ if (s.length > MAX_MIME_TYPE_LEN) {
398
+ throw new TypeError("seller-signup: mime_type must be <= " + MAX_MIME_TYPE_LEN + " characters");
399
+ }
400
+ if (!MIME_RE.test(s)) {
401
+ throw new TypeError("seller-signup: mime_type must match RFC-6838 type/subtype shape");
402
+ }
403
+ return s;
404
+ }
405
+
406
+ function _fieldsArray(value) {
407
+ if (!Array.isArray(value)) {
408
+ throw new TypeError("seller-signup: fields must be a non-empty array");
409
+ }
410
+ if (value.length === 0) {
411
+ throw new TypeError("seller-signup: fields must contain at least one entry");
412
+ }
413
+ if (value.length > MAX_FIELDS_LEN) {
414
+ throw new TypeError("seller-signup: fields must contain <= " + MAX_FIELDS_LEN + " entries");
415
+ }
416
+ var seen = Object.create(null);
417
+ for (var i = 0; i < value.length; i += 1) {
418
+ var f = value[i];
419
+ if (typeof f !== "string" || !f.length) {
420
+ throw new TypeError("seller-signup: fields[" + i + "] must be a non-empty string");
421
+ }
422
+ if (f.length > MAX_FIELD_NAME_LEN) {
423
+ throw new TypeError("seller-signup: fields[" + i + "] must be <= " + MAX_FIELD_NAME_LEN + " characters");
424
+ }
425
+ if (!FIELD_NAME_RE.test(f)) {
426
+ throw new TypeError("seller-signup: fields[" + i + "] must match /^[a-z][a-z0-9_]*$/");
427
+ }
428
+ if (seen[f]) {
429
+ throw new TypeError("seller-signup: fields contains duplicate " + JSON.stringify(f));
430
+ }
431
+ seen[f] = true;
432
+ }
433
+ return value.slice();
434
+ }
435
+
436
+ function _applicationStatus(s) {
437
+ if (typeof s !== "string" || APPLICATION_STATUSES.indexOf(s) === -1) {
438
+ throw new TypeError("seller-signup: status must be one of " + APPLICATION_STATUSES.join(", "));
439
+ }
440
+ return s;
441
+ }
442
+
443
+ function _listLimit(n) {
444
+ if (!Number.isInteger(n) || n <= 0 || n > MAX_LIST_LIMIT) {
445
+ throw new TypeError("seller-signup: limit must be an integer in [1, " + MAX_LIST_LIMIT + "]");
446
+ }
447
+ return n;
448
+ }
449
+
450
+ // Monotonic clock — guarantees each subsequent call returns a
451
+ // strictly greater integer even when the wall clock resolution
452
+ // can't keep up (Windows / fast CI runners). Application + document
453
+ // rows depend on created_at ordering for the operator inbox sort;
454
+ // any duplicate timestamp would force a tie-break on the secondary
455
+ // (id) column, which is fine for correctness but produces test
456
+ // flakes where a sub-millisecond write-then-list reads the rows in
457
+ // an unexpected order.
458
+ var _lastTs = 0;
459
+ function _now() {
460
+ var t = Date.now();
461
+ if (t <= _lastTs) { t = _lastTs + 1; }
462
+ _lastTs = t;
463
+ return t;
464
+ }
465
+
466
+ // ---- row hydration ------------------------------------------------------
467
+
468
+ function _hydrateApplication(row) {
469
+ if (!row) return null;
470
+ return {
471
+ id: row.id,
472
+ business_name: row.business_name,
473
+ contact_email_hash: row.contact_email_hash,
474
+ contact_email_normalised: row.contact_email_normalised,
475
+ contact_phone_hash: row.contact_phone_hash,
476
+ contact_phone_normalised: row.contact_phone_normalised,
477
+ business_address: JSON.parse(row.business_address_json),
478
+ tax_id_kind: row.tax_id_kind,
479
+ tax_id_hash: row.tax_id_hash,
480
+ tax_id_normalised_last4: row.tax_id_normalised_last4,
481
+ category_focus: JSON.parse(row.category_focus_json),
482
+ expected_monthly_volume_minor: Number(row.expected_monthly_volume_minor),
483
+ currency: row.currency,
484
+ references_json: row.references_json == null ? null : JSON.parse(row.references_json),
485
+ status: row.status,
486
+ approved_at: row.approved_at == null ? null : Number(row.approved_at),
487
+ approved_by: row.approved_by == null ? null : row.approved_by,
488
+ rejected_at: row.rejected_at == null ? null : Number(row.rejected_at),
489
+ rejected_by: row.rejected_by == null ? null : row.rejected_by,
490
+ reject_reason: row.reject_reason == null ? null : row.reject_reason,
491
+ vendor_slug: row.vendor_slug == null ? null : row.vendor_slug,
492
+ created_at: Number(row.created_at),
493
+ updated_at: Number(row.updated_at),
494
+ };
495
+ }
496
+
497
+ function _hydrateDocument(row) {
498
+ if (!row) return null;
499
+ return {
500
+ id: row.id,
501
+ application_id: row.application_id,
502
+ doc_kind: row.doc_kind,
503
+ status: row.status,
504
+ instructions: row.instructions,
505
+ sha3_512: row.sha3_512 == null ? null : row.sha3_512,
506
+ byte_size: row.byte_size == null ? null : Number(row.byte_size),
507
+ mime_type: row.mime_type == null ? null : row.mime_type,
508
+ uploaded_at: row.uploaded_at == null ? null : Number(row.uploaded_at),
509
+ accepted_at: row.accepted_at == null ? null : Number(row.accepted_at),
510
+ rejected_at: row.rejected_at == null ? null : Number(row.rejected_at),
511
+ reject_reason: row.reject_reason == null ? null : row.reject_reason,
512
+ created_at: Number(row.created_at),
513
+ updated_at: Number(row.updated_at),
514
+ };
515
+ }
516
+
517
+ // ---- factory ------------------------------------------------------------
518
+
519
+ function create(opts) {
520
+ opts = opts || {};
521
+ var query = opts.query;
522
+ if (!query) {
523
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
524
+ }
525
+ // The vendors handle is optional at construction. approveApplication
526
+ // requires it — when absent the approve verb refuses with a typed
527
+ // error. Submit / requestDocument / recordDocumentUploaded /
528
+ // rejectApplication / requestRevisions all work without it.
529
+ var vendorsHandle = opts.vendors || null;
530
+ if (vendorsHandle && typeof vendorsHandle.registerVendor !== "function") {
531
+ throw new TypeError("seller-signup.create: opts.vendors must expose registerVendor when provided");
532
+ }
533
+
534
+ function _hashEmail(canonical) {
535
+ return _b().crypto.namespaceHash(EMAIL_NAMESPACE, canonical);
536
+ }
537
+ function _hashPhone(normalized) {
538
+ return _b().crypto.namespaceHash(PHONE_NAMESPACE, normalized);
539
+ }
540
+ function _hashTaxId(normalized) {
541
+ return _b().crypto.namespaceHash(TAX_ID_NAMESPACE, normalized);
542
+ }
543
+
544
+ async function _getApplicationRaw(id) {
545
+ var r = await query("SELECT * FROM seller_applications WHERE id = ?1", [id]);
546
+ return r.rows[0] || null;
547
+ }
548
+
549
+ async function _getDocumentRaw(id) {
550
+ var r = await query("SELECT * FROM seller_application_documents WHERE id = ?1", [id]);
551
+ return r.rows[0] || null;
552
+ }
553
+
554
+ // Compute the next application status after a doc event. When
555
+ // every document is at status='uploaded' (or 'accepted'), the
556
+ // application advances to 'in_review'. While any doc is still
557
+ // 'requested', the application sits at 'docs_pending'.
558
+ async function _statusAfterDocChange(applicationId, priorStatus) {
559
+ if (priorStatus === "approved" || priorStatus === "rejected" || priorStatus === "withdrawn") {
560
+ return priorStatus;
561
+ }
562
+ var docs = (await query(
563
+ "SELECT status FROM seller_application_documents WHERE application_id = ?1",
564
+ [applicationId],
565
+ )).rows;
566
+ if (docs.length === 0) {
567
+ return "submitted";
568
+ }
569
+ var anyRequested = false;
570
+ for (var i = 0; i < docs.length; i += 1) {
571
+ if (docs[i].status === "requested") {
572
+ anyRequested = true;
573
+ break;
574
+ }
575
+ }
576
+ if (anyRequested) return "docs_pending";
577
+ return "in_review";
578
+ }
579
+
580
+ return {
581
+ EMAIL_NAMESPACE: EMAIL_NAMESPACE,
582
+ PHONE_NAMESPACE: PHONE_NAMESPACE,
583
+ TAX_ID_NAMESPACE: TAX_ID_NAMESPACE,
584
+ TAX_ID_KINDS: TAX_ID_KINDS.slice(),
585
+ DOC_KINDS: DOC_KINDS.slice(),
586
+ APPLICATION_STATUSES: APPLICATION_STATUSES.slice(),
587
+ DOCUMENT_STATUSES: DOCUMENT_STATUSES.slice(),
588
+ TERMINAL_APPLICATION_STATUSES: TERMINAL_APPLICATION_STATUSES.slice(),
589
+ MAX_BUSINESS_NAME_LEN: MAX_BUSINESS_NAME_LEN,
590
+ MAX_PHONE_LEN: MAX_PHONE_LEN,
591
+ MAX_TAX_ID_LEN: MAX_TAX_ID_LEN,
592
+ MAX_ADDRESS_JSON_LEN: MAX_ADDRESS_JSON_LEN,
593
+ MAX_CATEGORY_FOCUS_LEN: MAX_CATEGORY_FOCUS_LEN,
594
+ MAX_REFERENCES_JSON_LEN: MAX_REFERENCES_JSON_LEN,
595
+ MAX_INSTRUCTIONS_LEN: MAX_INSTRUCTIONS_LEN,
596
+ MAX_REJECT_REASON_LEN: MAX_REJECT_REASON_LEN,
597
+ MAX_OPERATOR_ID_LEN: MAX_OPERATOR_ID_LEN,
598
+ MAX_MIME_TYPE_LEN: MAX_MIME_TYPE_LEN,
599
+ MAX_FIELDS_LEN: MAX_FIELDS_LEN,
600
+ MAX_FIELD_NAME_LEN: MAX_FIELD_NAME_LEN,
601
+ MAX_LIST_LIMIT: MAX_LIST_LIMIT,
602
+ MAX_VOLUME_MINOR: MAX_VOLUME_MINOR,
603
+ MAX_BYTE_SIZE: MAX_BYTE_SIZE,
604
+
605
+ // Capture an inbound application. Email / phone / tax_id are
606
+ // hashed via namespaceHash so the raw values never land on disk.
607
+ // The application opens at status='submitted' and waits for the
608
+ // operator to call requestDocument; the docs cascade then walks
609
+ // it through docs_pending -> in_review -> approved | rejected.
610
+ submitApplication: async function (input) {
611
+ if (!input || typeof input !== "object") {
612
+ throw new TypeError("seller-signup.submitApplication: input object required");
613
+ }
614
+ var businessName = _businessName(input.business_name);
615
+ var emailNorm = _normalizeEmail(input.contact_email);
616
+ var emailHash = _hashEmail(emailNorm);
617
+ var phoneNorm = _phone(input.contact_phone);
618
+ var phoneHash = _hashPhone(phoneNorm);
619
+ var addressJson = _businessAddress(input.business_address);
620
+ var taxIdKind = _taxIdKind(input.tax_id_kind);
621
+ var taxIdValue = _taxIdValue(input.tax_id_value);
622
+ var taxIdNormalized = taxIdValue.replace(/\s+/g, "").toUpperCase();
623
+ var taxIdHash = _hashTaxId(taxIdNormalized);
624
+ var taxIdLast4 = taxIdNormalized.slice(-4);
625
+ var categoryFocus = _categoryFocus(input.category_focus);
626
+ var volume = _volumeMinor(input.expected_monthly_volume_minor);
627
+ var currency = _currency(input.currency);
628
+ var referencesJson = _referencesJson(input.references_json);
629
+
630
+ var id = _b().uuid.v7();
631
+ var ts = _now();
632
+ await query(
633
+ "INSERT INTO seller_applications " +
634
+ "(id, business_name, contact_email_hash, contact_email_normalised, " +
635
+ " contact_phone_hash, contact_phone_normalised, business_address_json, " +
636
+ " tax_id_kind, tax_id_hash, tax_id_normalised_last4, category_focus_json, " +
637
+ " expected_monthly_volume_minor, currency, references_json, status, " +
638
+ " approved_at, approved_by, rejected_at, rejected_by, reject_reason, " +
639
+ " vendor_slug, created_at, updated_at) " +
640
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, " +
641
+ " 'submitted', NULL, NULL, NULL, NULL, NULL, NULL, ?15, ?15)",
642
+ [
643
+ id, businessName, emailHash, emailNorm, phoneHash, phoneNorm,
644
+ addressJson, taxIdKind, taxIdHash, taxIdLast4, categoryFocus,
645
+ volume, currency, referencesJson, ts,
646
+ ],
647
+ );
648
+ return _hydrateApplication(await _getApplicationRaw(id));
649
+ },
650
+
651
+ // Operator requests a supporting document. The document row
652
+ // lands at status='requested'; the applicant later uploads via
653
+ // recordDocumentUploaded. Multiple distinct doc_kinds may be
654
+ // requested at once. Re-requesting the same doc_kind WHILE the
655
+ // prior request is still open ('requested') refuses — the
656
+ // operator should wait for the upload or escalate via
657
+ // rejectApplication; re-requesting AFTER an upload is the
658
+ // requestRevisions flow.
659
+ requestDocument: async function (input) {
660
+ if (!input || typeof input !== "object") {
661
+ throw new TypeError("seller-signup.requestDocument: input object required");
662
+ }
663
+ var applicationId = _uuid(input.application_id, "application_id");
664
+ var docKind = _docKind(input.doc_kind);
665
+ var instructions = _instructions(input.instructions);
666
+
667
+ var app = await _getApplicationRaw(applicationId);
668
+ if (!app) {
669
+ var miss = new Error("seller-signup.requestDocument: application not found");
670
+ miss.code = "APPLICATION_NOT_FOUND";
671
+ throw miss;
672
+ }
673
+ if (TERMINAL_APPLICATION_STATUSES.indexOf(app.status) !== -1) {
674
+ var term = new Error(
675
+ "seller-signup.requestDocument: refused — application is " + app.status + " (terminal)"
676
+ );
677
+ term.code = "APPLICATION_TERMINAL";
678
+ throw term;
679
+ }
680
+
681
+ var open = await query(
682
+ "SELECT id FROM seller_application_documents " +
683
+ "WHERE application_id = ?1 AND doc_kind = ?2 AND status = 'requested'",
684
+ [applicationId, docKind],
685
+ );
686
+ if (open.rows.length) {
687
+ var dup = new Error(
688
+ "seller-signup.requestDocument: refused — doc_kind " + JSON.stringify(docKind) +
689
+ " already has an open request on this application"
690
+ );
691
+ dup.code = "DOCUMENT_REQUEST_OPEN";
692
+ throw dup;
693
+ }
694
+
695
+ var id = _b().uuid.v7();
696
+ var ts = _now();
697
+ await query(
698
+ "INSERT INTO seller_application_documents " +
699
+ "(id, application_id, doc_kind, status, instructions, sha3_512, byte_size, " +
700
+ " mime_type, uploaded_at, accepted_at, rejected_at, reject_reason, " +
701
+ " created_at, updated_at) " +
702
+ "VALUES (?1, ?2, ?3, 'requested', ?4, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?5, ?5)",
703
+ [id, applicationId, docKind, instructions, ts],
704
+ );
705
+
706
+ // Advance application FSM to docs_pending if it was submitted
707
+ // or revisions_requested (the operator can transition into
708
+ // docs_pending by requesting any document).
709
+ var nextStatus = await _statusAfterDocChange(applicationId, app.status);
710
+ if (nextStatus !== app.status) {
711
+ await query(
712
+ "UPDATE seller_applications SET status = ?1, updated_at = ?2 WHERE id = ?3",
713
+ [nextStatus, ts, applicationId],
714
+ );
715
+ } else {
716
+ await query(
717
+ "UPDATE seller_applications SET updated_at = ?1 WHERE id = ?2",
718
+ [ts, applicationId],
719
+ );
720
+ }
721
+
722
+ return _hydrateDocument(await _getDocumentRaw(id));
723
+ },
724
+
725
+ // Applicant uploads the doc. Status moves from requested ->
726
+ // uploaded; sha3_512 + byte_size + mime_type are captured.
727
+ // Refuses when the doc isn't at status='requested' (idempotent
728
+ // hazard — a retry would clobber the original upload's
729
+ // sha3_512 + byte_size + mime_type). Application FSM advances
730
+ // to in_review when this was the last outstanding requested doc.
731
+ recordDocumentUploaded: async function (input) {
732
+ if (!input || typeof input !== "object") {
733
+ throw new TypeError("seller-signup.recordDocumentUploaded: input object required");
734
+ }
735
+ var applicationId = _uuid(input.application_id, "application_id");
736
+ var docKind = _docKind(input.doc_kind);
737
+ var sha3 = _sha3_512(input.sha3_512);
738
+ var byteSize = _byteSize(input.byte_size);
739
+ var mimeType = _mimeType(input.mime_type);
740
+
741
+ var app = await _getApplicationRaw(applicationId);
742
+ if (!app) {
743
+ var miss = new Error("seller-signup.recordDocumentUploaded: application not found");
744
+ miss.code = "APPLICATION_NOT_FOUND";
745
+ throw miss;
746
+ }
747
+ if (TERMINAL_APPLICATION_STATUSES.indexOf(app.status) !== -1) {
748
+ var term = new Error(
749
+ "seller-signup.recordDocumentUploaded: refused — application is " + app.status + " (terminal)"
750
+ );
751
+ term.code = "APPLICATION_TERMINAL";
752
+ throw term;
753
+ }
754
+
755
+ // Find the open request for (application, doc_kind).
756
+ var open = await query(
757
+ "SELECT * FROM seller_application_documents " +
758
+ "WHERE application_id = ?1 AND doc_kind = ?2 AND status = 'requested' " +
759
+ "ORDER BY created_at DESC LIMIT 1",
760
+ [applicationId, docKind],
761
+ );
762
+ if (!open.rows.length) {
763
+ var noOpen = new Error(
764
+ "seller-signup.recordDocumentUploaded: refused — no open request for doc_kind " +
765
+ JSON.stringify(docKind) + " on this application"
766
+ );
767
+ noOpen.code = "NO_OPEN_REQUEST";
768
+ throw noOpen;
769
+ }
770
+
771
+ var ts = _now();
772
+ await query(
773
+ "UPDATE seller_application_documents " +
774
+ "SET status = 'uploaded', sha3_512 = ?1, byte_size = ?2, mime_type = ?3, " +
775
+ " uploaded_at = ?4, updated_at = ?4 " +
776
+ "WHERE id = ?5",
777
+ [sha3, byteSize, mimeType, ts, open.rows[0].id],
778
+ );
779
+
780
+ var nextStatus = await _statusAfterDocChange(applicationId, app.status);
781
+ if (nextStatus !== app.status) {
782
+ await query(
783
+ "UPDATE seller_applications SET status = ?1, updated_at = ?2 WHERE id = ?3",
784
+ [nextStatus, ts, applicationId],
785
+ );
786
+ } else {
787
+ await query(
788
+ "UPDATE seller_applications SET updated_at = ?1 WHERE id = ?2",
789
+ [ts, applicationId],
790
+ );
791
+ }
792
+ return _hydrateDocument(await _getDocumentRaw(open.rows[0].id));
793
+ },
794
+
795
+ // Operator approves the application. Composes
796
+ // vendors.registerVendor to mint the real vendor row, then
797
+ // transitions the application to approved with the resulting
798
+ // vendor_slug captured. Refuses if the application is already
799
+ // terminal, if any document is still 'requested', or if no
800
+ // vendors handle was wired at construction. The vendor's
801
+ // contact_email + contact_phone + address are filled from the
802
+ // application's normalised columns; commission_split_bps +
803
+ // payout_method + payout_address are operator decisions
804
+ // captured separately (the operator passes them via the
805
+ // vendor_slug call chain, but the v1 surface assumes operator-
806
+ // sane defaults — caller supplies vendor_slug here, the
807
+ // primitive registers with payout_method='bank_transfer' +
808
+ // commission_split_bps=7000 and the operator updates via
809
+ // vendors.updateVendor afterwards).
810
+ approveApplication: async function (input) {
811
+ if (!input || typeof input !== "object") {
812
+ throw new TypeError("seller-signup.approveApplication: input object required");
813
+ }
814
+ var applicationId = _uuid(input.application_id, "application_id");
815
+ var approvedBy = _operatorId(input.approved_by, "approved_by");
816
+ if (typeof input.vendor_slug !== "string" || !input.vendor_slug.length) {
817
+ throw new TypeError("seller-signup.approveApplication: vendor_slug must be a non-empty string");
818
+ }
819
+ var vendorSlug = input.vendor_slug;
820
+
821
+ if (!vendorsHandle) {
822
+ var noVendors = new Error(
823
+ "seller-signup.approveApplication: refused — opts.vendors was not wired at construction; " +
824
+ "the approve verb composes vendors.registerVendor and cannot run without it"
825
+ );
826
+ noVendors.code = "VENDORS_HANDLE_MISSING";
827
+ throw noVendors;
828
+ }
829
+
830
+ var app = await _getApplicationRaw(applicationId);
831
+ if (!app) {
832
+ var miss = new Error("seller-signup.approveApplication: application not found");
833
+ miss.code = "APPLICATION_NOT_FOUND";
834
+ throw miss;
835
+ }
836
+ if (TERMINAL_APPLICATION_STATUSES.indexOf(app.status) !== -1) {
837
+ var term = new Error(
838
+ "seller-signup.approveApplication: refused — application is " + app.status + " (terminal)"
839
+ );
840
+ term.code = "APPLICATION_TERMINAL";
841
+ throw term;
842
+ }
843
+ if (app.status !== "in_review") {
844
+ var notReady = new Error(
845
+ "seller-signup.approveApplication: refused — application is " + app.status +
846
+ "; approve requires status='in_review' (all requested documents uploaded)"
847
+ );
848
+ notReady.code = "APPLICATION_NOT_READY";
849
+ throw notReady;
850
+ }
851
+
852
+ // Compose vendors.registerVendor — the operator-facing v1
853
+ // defaults a fresh marketplace seller to bank_transfer payout +
854
+ // 70% commission split; the operator tunes both via
855
+ // vendors.updateVendor after approval.
856
+ var vendor;
857
+ try {
858
+ vendor = await vendorsHandle.registerVendor({
859
+ slug: vendorSlug,
860
+ name: app.business_name,
861
+ contact_email: app.contact_email_normalised,
862
+ contact_phone: app.contact_phone_normalised,
863
+ address: JSON.parse(app.business_address_json),
864
+ payout_method: "bank_transfer",
865
+ payout_address: "pending-operator-update",
866
+ commission_split_bps: 7000,
867
+ status: "active",
868
+ });
869
+ } catch (e) {
870
+ // Surface the vendors error with our typed-code wrapper so
871
+ // operators can distinguish a vendors-layer refusal from an
872
+ // application-layer refusal.
873
+ var wrapped = new Error(
874
+ "seller-signup.approveApplication: vendors.registerVendor refused — " +
875
+ (e && e.message || "unknown")
876
+ );
877
+ wrapped.code = "VENDORS_REGISTER_REFUSED";
878
+ wrapped.cause = e;
879
+ throw wrapped;
880
+ }
881
+
882
+ var ts = _now();
883
+ await query(
884
+ "UPDATE seller_applications " +
885
+ "SET status = 'approved', approved_at = ?1, approved_by = ?2, " +
886
+ " vendor_slug = ?3, updated_at = ?1 " +
887
+ "WHERE id = ?4",
888
+ [ts, approvedBy, vendor.slug, applicationId],
889
+ );
890
+
891
+ return {
892
+ application: _hydrateApplication(await _getApplicationRaw(applicationId)),
893
+ vendor: vendor,
894
+ };
895
+ },
896
+
897
+ // Operator rejects the application. Terminal — no transitions
898
+ // out. The reason is captured for audit. Open document requests
899
+ // are NOT auto-closed (they remain at status='requested') so
900
+ // the audit trail records what was asked for; downstream
901
+ // operator-side erasure flows handle physical cleanup.
902
+ rejectApplication: async function (input) {
903
+ if (!input || typeof input !== "object") {
904
+ throw new TypeError("seller-signup.rejectApplication: input object required");
905
+ }
906
+ var applicationId = _uuid(input.application_id, "application_id");
907
+ var reason = _rejectReason(input.reason);
908
+ var rejectedBy = _operatorId(input.rejected_by, "rejected_by");
909
+
910
+ var app = await _getApplicationRaw(applicationId);
911
+ if (!app) {
912
+ var miss = new Error("seller-signup.rejectApplication: application not found");
913
+ miss.code = "APPLICATION_NOT_FOUND";
914
+ throw miss;
915
+ }
916
+ if (TERMINAL_APPLICATION_STATUSES.indexOf(app.status) !== -1) {
917
+ var term = new Error(
918
+ "seller-signup.rejectApplication: refused — application is " + app.status + " (terminal)"
919
+ );
920
+ term.code = "APPLICATION_TERMINAL";
921
+ throw term;
922
+ }
923
+
924
+ var ts = _now();
925
+ await query(
926
+ "UPDATE seller_applications " +
927
+ "SET status = 'rejected', rejected_at = ?1, rejected_by = ?2, " +
928
+ " reject_reason = ?3, updated_at = ?1 " +
929
+ "WHERE id = ?4",
930
+ [ts, rejectedBy, reason, applicationId],
931
+ );
932
+ return _hydrateApplication(await _getApplicationRaw(applicationId));
933
+ },
934
+
935
+ // Operator asks the applicant to refile something. Re-opens the
936
+ // application for additional doc cycles: transitions status to
937
+ // 'revisions_requested'; the operator follows up with one or
938
+ // more requestDocument calls that cascade the application back
939
+ // into 'docs_pending'. `fields` is the list of operator-facing
940
+ // field names the applicant needs to revise (rendered in the
941
+ // applicant's revision UI); `instructions` is a free-text
942
+ // explanation. Refuses on terminal statuses.
943
+ requestRevisions: async function (input) {
944
+ if (!input || typeof input !== "object") {
945
+ throw new TypeError("seller-signup.requestRevisions: input object required");
946
+ }
947
+ var applicationId = _uuid(input.application_id, "application_id");
948
+ var fields = _fieldsArray(input.fields);
949
+ var instructions = _instructions(input.instructions);
950
+
951
+ var app = await _getApplicationRaw(applicationId);
952
+ if (!app) {
953
+ var miss = new Error("seller-signup.requestRevisions: application not found");
954
+ miss.code = "APPLICATION_NOT_FOUND";
955
+ throw miss;
956
+ }
957
+ if (TERMINAL_APPLICATION_STATUSES.indexOf(app.status) !== -1) {
958
+ var term = new Error(
959
+ "seller-signup.requestRevisions: refused — application is " + app.status + " (terminal)"
960
+ );
961
+ term.code = "APPLICATION_TERMINAL";
962
+ throw term;
963
+ }
964
+
965
+ var ts = _now();
966
+ await query(
967
+ "UPDATE seller_applications SET status = 'revisions_requested', updated_at = ?1 WHERE id = ?2",
968
+ [ts, applicationId],
969
+ );
970
+ return {
971
+ application: _hydrateApplication(await _getApplicationRaw(applicationId)),
972
+ fields: fields,
973
+ instructions: instructions,
974
+ };
975
+ },
976
+
977
+ getApplication: async function (id) {
978
+ var applicationId = _uuid(id, "id");
979
+ return _hydrateApplication(await _getApplicationRaw(applicationId));
980
+ },
981
+
982
+ // Operator inbox list. Optionally filter by status. Cursor is
983
+ // a millisecond epoch — list rows older than the cursor.
984
+ // Mirrors the shape used by loyalty-redemption / customer-notes.
985
+ listApplications: async function (listOpts) {
986
+ listOpts = listOpts || {};
987
+ var limit = listOpts.limit == null ? 50 : listOpts.limit;
988
+ _listLimit(limit);
989
+
990
+ var sql = "SELECT * FROM seller_applications";
991
+ var params = [];
992
+ var where = [];
993
+ if (listOpts.status != null) {
994
+ var status = _applicationStatus(listOpts.status);
995
+ where.push("status = ?" + (params.length + 1));
996
+ params.push(status);
997
+ }
998
+ if (listOpts.cursor != null) {
999
+ if (!Number.isInteger(listOpts.cursor) || listOpts.cursor < 0) {
1000
+ throw new TypeError("seller-signup.listApplications: cursor must be a non-negative integer (ms epoch)");
1001
+ }
1002
+ where.push("created_at < ?" + (params.length + 1));
1003
+ params.push(listOpts.cursor);
1004
+ }
1005
+ if (where.length) sql += " WHERE " + where.join(" AND ");
1006
+ sql += " ORDER BY created_at DESC, id DESC LIMIT ?" + (params.length + 1);
1007
+ params.push(limit);
1008
+
1009
+ var rows = (await query(sql, params)).rows;
1010
+ var hydrated = rows.map(_hydrateApplication);
1011
+ var nextCursor = hydrated.length === limit ? hydrated[hydrated.length - 1].created_at : null;
1012
+ return { rows: hydrated, next_cursor: nextCursor };
1013
+ },
1014
+
1015
+ documentsForApplication: async function (applicationId) {
1016
+ var id = _uuid(applicationId, "application_id");
1017
+ var r = await query(
1018
+ "SELECT * FROM seller_application_documents WHERE application_id = ?1 " +
1019
+ "ORDER BY created_at ASC, id ASC",
1020
+ [id],
1021
+ );
1022
+ return r.rows.map(_hydrateDocument);
1023
+ },
1024
+ };
1025
+ }
1026
+
1027
+ module.exports = {
1028
+ create: create,
1029
+ EMAIL_NAMESPACE: EMAIL_NAMESPACE,
1030
+ PHONE_NAMESPACE: PHONE_NAMESPACE,
1031
+ TAX_ID_NAMESPACE: TAX_ID_NAMESPACE,
1032
+ TAX_ID_KINDS: TAX_ID_KINDS.slice(),
1033
+ DOC_KINDS: DOC_KINDS.slice(),
1034
+ APPLICATION_STATUSES: APPLICATION_STATUSES.slice(),
1035
+ DOCUMENT_STATUSES: DOCUMENT_STATUSES.slice(),
1036
+ TERMINAL_APPLICATION_STATUSES: TERMINAL_APPLICATION_STATUSES.slice(),
1037
+ MAX_BUSINESS_NAME_LEN: MAX_BUSINESS_NAME_LEN,
1038
+ MAX_PHONE_LEN: MAX_PHONE_LEN,
1039
+ MAX_TAX_ID_LEN: MAX_TAX_ID_LEN,
1040
+ MAX_ADDRESS_JSON_LEN: MAX_ADDRESS_JSON_LEN,
1041
+ MAX_CATEGORY_FOCUS_LEN: MAX_CATEGORY_FOCUS_LEN,
1042
+ MAX_REFERENCES_JSON_LEN: MAX_REFERENCES_JSON_LEN,
1043
+ MAX_INSTRUCTIONS_LEN: MAX_INSTRUCTIONS_LEN,
1044
+ MAX_REJECT_REASON_LEN: MAX_REJECT_REASON_LEN,
1045
+ MAX_OPERATOR_ID_LEN: MAX_OPERATOR_ID_LEN,
1046
+ MAX_MIME_TYPE_LEN: MAX_MIME_TYPE_LEN,
1047
+ MAX_FIELDS_LEN: MAX_FIELDS_LEN,
1048
+ MAX_FIELD_NAME_LEN: MAX_FIELD_NAME_LEN,
1049
+ MAX_LIST_LIMIT: MAX_LIST_LIMIT,
1050
+ MAX_VOLUME_MINOR: MAX_VOLUME_MINOR,
1051
+ MAX_BYTE_SIZE: MAX_BYTE_SIZE,
1052
+ };