@blamejs/blamejs-shop 0.0.61 → 0.0.64
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/compliance-export.js +614 -0
- package/lib/email-campaigns.js +844 -0
- package/lib/error-log.js +525 -0
- package/lib/geolocation.js +651 -0
- package/lib/gift-registry.js +820 -0
- package/lib/index.js +15 -0
- package/lib/invoice-renderer.js +618 -0
- package/lib/live-chat.js +714 -0
- package/lib/loyalty-redemption.js +673 -0
- package/lib/plan-changes.js +508 -0
- package/lib/refund-policy.js +965 -0
- package/lib/sms-dispatcher.js +7 -1
- package/lib/stock-transfers.js +777 -0
- package/lib/store-credit.js +565 -0
- package/lib/storefront-dashboards.js +863 -0
- package/lib/vendors.js +797 -0
- package/package.json +1 -1
package/lib/vendors.js
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module shop.vendors
|
|
4
|
+
* @title Vendors primitive — multi-vendor marketplace registry +
|
|
5
|
+
* per-vendor catalog assignment + payout commission ledger
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* A vendor is a supplier / brand the operator drop-ships from.
|
|
9
|
+
* Distinct from `affiliates`: an affiliate is a referrer paid for
|
|
10
|
+
* driving traffic; a vendor actually ships the goods. The operator
|
|
11
|
+
* registers each vendor with a stable `slug` handle, contact info,
|
|
12
|
+
* a payout rule, and a `commission_split_bps` share of every order
|
|
13
|
+
* the vendor's SKUs appear on. Catalog SKUs are assigned to exactly
|
|
14
|
+
* one vendor via `assignSku`; the join table's UNIQUE on `sku`
|
|
15
|
+
* refuses a second vendor claiming the same SKU until the first
|
|
16
|
+
* `unassignSku`s it.
|
|
17
|
+
*
|
|
18
|
+
* FSM:
|
|
19
|
+
*
|
|
20
|
+
* active <-> paused (pauseVendor / reinstateVendor)
|
|
21
|
+
* active|paused -> archived (archiveVendor — terminal)
|
|
22
|
+
*
|
|
23
|
+
* Commission math:
|
|
24
|
+
*
|
|
25
|
+
* commission_minor = floor(gross_minor * commission_split_bps / 10000)
|
|
26
|
+
*
|
|
27
|
+
* `recordCommission({ vendor_slug, order_id, gross_minor, currency })`
|
|
28
|
+
* writes a pending commission row at order-completion time.
|
|
29
|
+
* `payoutsDue({ as_of })` aggregates outstanding pending commissions
|
|
30
|
+
* per vendor for the finance pipeline to drain. The finance side
|
|
31
|
+
* stamps the payout reference + status transition via the
|
|
32
|
+
* commission row's lifecycle (out of scope for this primitive's
|
|
33
|
+
* v1 surface — that's a downstream operator-driven settlement;
|
|
34
|
+
* the ledger row is the audit grain).
|
|
35
|
+
*
|
|
36
|
+
* Composes:
|
|
37
|
+
* - `b.crypto.namespaceHash` — contact-email hashing under the
|
|
38
|
+
* "vendor-contact-email" namespace so
|
|
39
|
+
* the raw address never lands on disk
|
|
40
|
+
* - `b.guardEmail` — strict-profile validate + sanitize
|
|
41
|
+
* - `b.uuid.v7` — commission row PK (monotonic
|
|
42
|
+
* lexicographic so audit queries sort
|
|
43
|
+
* cleanly without an extra index)
|
|
44
|
+
*
|
|
45
|
+
* Surface:
|
|
46
|
+
* registerVendor({ slug, name, contact_email, contact_phone?,
|
|
47
|
+
* address?, payout_method, payout_address,
|
|
48
|
+
* commission_split_bps, status })
|
|
49
|
+
* getVendor(slug) / vendorBySlug(slug)
|
|
50
|
+
* listVendors({ status? })
|
|
51
|
+
* updateVendor(slug, patch)
|
|
52
|
+
* pauseVendor(slug) / reinstateVendor(slug) / archiveVendor(slug)
|
|
53
|
+
* assignSku({ vendor_slug, sku }) /
|
|
54
|
+
* unassignSku({ vendor_slug, sku })
|
|
55
|
+
* vendorForSku(sku) / skusForVendor(vendor_slug)
|
|
56
|
+
* recordCommission({ vendor_slug, order_id, gross_minor, currency,
|
|
57
|
+
* occurred_at? })
|
|
58
|
+
* payoutsDue({ as_of })
|
|
59
|
+
*
|
|
60
|
+
* Storage: `migrations-d1/0084_vendors.sql` — three tables,
|
|
61
|
+
* `vendors` + `vendor_skus` + `vendor_commissions`. ON DELETE
|
|
62
|
+
* CASCADE drops the join + ledger rows when the vendor row is hard-
|
|
63
|
+
* deleted (the primitive only soft-deletes via `archiveVendor`;
|
|
64
|
+
* hard delete is an operator-side migration concern).
|
|
65
|
+
*
|
|
66
|
+
* @primitive vendors
|
|
67
|
+
* @related b.crypto, b.guardEmail, b.uuid, shop.affiliates
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
var MAX_NAME_LEN = 200;
|
|
71
|
+
var MAX_PAYOUT_ADDRESS_LEN = 512;
|
|
72
|
+
var MAX_ADDRESS_JSON_LEN = 4096;
|
|
73
|
+
var MAX_PHONE_LEN = 32;
|
|
74
|
+
var MAX_SLUG_LEN = 64;
|
|
75
|
+
var MAX_SKU_LEN = 128;
|
|
76
|
+
|
|
77
|
+
var EMAIL_NAMESPACE = "vendor-contact-email";
|
|
78
|
+
|
|
79
|
+
var PAYOUT_METHODS = [
|
|
80
|
+
"paypal", "bank_transfer", "stripe_connect", "gift_card", "store_credit",
|
|
81
|
+
];
|
|
82
|
+
var VENDOR_STATUSES = ["active", "paused", "archived"];
|
|
83
|
+
var COMMISSION_STATUSES = ["pending", "paid", "voided"];
|
|
84
|
+
|
|
85
|
+
var BPS_DENOMINATOR = 10000;
|
|
86
|
+
var MAX_BPS = 10000;
|
|
87
|
+
var MAX_AMOUNT_MINOR = 100000000000; // 1e11 — sanity cap on a
|
|
88
|
+
// single gross_minor /
|
|
89
|
+
// commission_minor row.
|
|
90
|
+
|
|
91
|
+
var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
92
|
+
var SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
93
|
+
var PHONE_RE = /^\+?[1-9]\d{1,14}$/;
|
|
94
|
+
|
|
95
|
+
// Control bytes + zero-width / direction-override family. The name +
|
|
96
|
+
// payout_address render in operator dashboards; embedded control /
|
|
97
|
+
// direction-override bytes are a slipping-class for header injection
|
|
98
|
+
// + visual-spoofing attacks downstream. Spelled with \u-escapes so
|
|
99
|
+
// ESLint's no-irregular-whitespace stays happy.
|
|
100
|
+
var CONTROL_BYTE_STRICT_RE = /[\x00-\x1f\x7f]/;
|
|
101
|
+
var ZERO_WIDTH_RE = new RegExp(
|
|
102
|
+
"[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF\\u061C]"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Mutable columns for `updateVendor(slug, patch)`. Slug is immutable
|
|
106
|
+
// — it's the public handle + the join-table key; mutating it would
|
|
107
|
+
// orphan vendor_skus + vendor_commissions rows. Contact email is
|
|
108
|
+
// immutable — changing it would orphan the email-hash audit trail.
|
|
109
|
+
// Status transitions land via pauseVendor / reinstateVendor /
|
|
110
|
+
// archiveVendor (each one stamps its own timestamp column).
|
|
111
|
+
// The patch keys are the operator-facing names, NOT raw SQL column
|
|
112
|
+
// names — `address` is the hydrated object, mapped to the `address_json`
|
|
113
|
+
// column at the write site. The friendlier surface keeps the operator
|
|
114
|
+
// console + the primitive's hydrated read-shape symmetric.
|
|
115
|
+
var ALLOWED_UPDATE_COLUMNS = Object.freeze([
|
|
116
|
+
"name", "contact_phone", "address", "payout_method",
|
|
117
|
+
"payout_address", "commission_split_bps",
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// Lazy framework handle — matches the pattern used by every other
|
|
121
|
+
// shop primitive; avoids the require cycle that would arise from
|
|
122
|
+
// importing `./index` at module-eval time.
|
|
123
|
+
var bShop;
|
|
124
|
+
function _b() {
|
|
125
|
+
if (!bShop) bShop = require("./index");
|
|
126
|
+
return bShop.framework;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- validators ---------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
function _slug(s, label) {
|
|
132
|
+
if (typeof s !== "string" || !s.length) {
|
|
133
|
+
throw new TypeError("vendors: " + label + " must be a non-empty string");
|
|
134
|
+
}
|
|
135
|
+
if (s.length > MAX_SLUG_LEN) {
|
|
136
|
+
throw new TypeError("vendors: " + label + " must be <= " + MAX_SLUG_LEN + " characters");
|
|
137
|
+
}
|
|
138
|
+
if (!SLUG_RE.test(s)) {
|
|
139
|
+
throw new TypeError("vendors: " + label + " must be lowercase alnum + dash, no leading/trailing dash");
|
|
140
|
+
}
|
|
141
|
+
return s;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _sku(s) {
|
|
145
|
+
if (typeof s !== "string" || !s.length) {
|
|
146
|
+
throw new TypeError("vendors: sku must be a non-empty string");
|
|
147
|
+
}
|
|
148
|
+
if (s.length > MAX_SKU_LEN) {
|
|
149
|
+
throw new TypeError("vendors: sku must be <= " + MAX_SKU_LEN + " characters");
|
|
150
|
+
}
|
|
151
|
+
if (!SKU_RE.test(s)) {
|
|
152
|
+
throw new TypeError("vendors: sku must match /^[A-Za-z0-9][A-Za-z0-9._-]*$/");
|
|
153
|
+
}
|
|
154
|
+
return s;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _name(s) {
|
|
158
|
+
if (typeof s !== "string") {
|
|
159
|
+
throw new TypeError("vendors: name must be a string");
|
|
160
|
+
}
|
|
161
|
+
var trimmed = s.trim();
|
|
162
|
+
if (!trimmed.length) {
|
|
163
|
+
throw new TypeError("vendors: name must be non-empty after trim");
|
|
164
|
+
}
|
|
165
|
+
if (s.length > MAX_NAME_LEN) {
|
|
166
|
+
throw new TypeError("vendors: name must be <= " + MAX_NAME_LEN + " characters");
|
|
167
|
+
}
|
|
168
|
+
if (CONTROL_BYTE_STRICT_RE.test(s)) {
|
|
169
|
+
throw new TypeError("vendors: name contains control bytes");
|
|
170
|
+
}
|
|
171
|
+
if (ZERO_WIDTH_RE.test(s)) {
|
|
172
|
+
throw new TypeError("vendors: name contains zero-width / direction-override bytes");
|
|
173
|
+
}
|
|
174
|
+
return s;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _payoutMethod(s) {
|
|
178
|
+
if (typeof s !== "string" || PAYOUT_METHODS.indexOf(s) === -1) {
|
|
179
|
+
throw new TypeError("vendors: payout_method must be one of " + PAYOUT_METHODS.join(", "));
|
|
180
|
+
}
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _payoutAddress(s) {
|
|
185
|
+
if (typeof s !== "string") {
|
|
186
|
+
throw new TypeError("vendors: payout_address must be a string");
|
|
187
|
+
}
|
|
188
|
+
var trimmed = s.trim();
|
|
189
|
+
if (!trimmed.length) {
|
|
190
|
+
throw new TypeError("vendors: payout_address must be non-empty after trim");
|
|
191
|
+
}
|
|
192
|
+
if (s.length > MAX_PAYOUT_ADDRESS_LEN) {
|
|
193
|
+
throw new TypeError("vendors: payout_address must be <= " + MAX_PAYOUT_ADDRESS_LEN + " characters");
|
|
194
|
+
}
|
|
195
|
+
if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
|
|
196
|
+
throw new TypeError("vendors: payout_address contains control / zero-width bytes");
|
|
197
|
+
}
|
|
198
|
+
return s;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _phone(value) {
|
|
202
|
+
if (value == null || value === "") return null;
|
|
203
|
+
if (typeof value !== "string") {
|
|
204
|
+
throw new TypeError("vendors: contact_phone must be a string or null");
|
|
205
|
+
}
|
|
206
|
+
var trimmed = value.trim();
|
|
207
|
+
if (!trimmed.length) return null;
|
|
208
|
+
if (trimmed.length > MAX_PHONE_LEN) {
|
|
209
|
+
throw new TypeError("vendors: contact_phone must be <= " + MAX_PHONE_LEN + " characters");
|
|
210
|
+
}
|
|
211
|
+
if (!PHONE_RE.test(trimmed)) {
|
|
212
|
+
throw new TypeError("vendors: contact_phone must match E.164-ish shape (^\\+?[1-9]\\d{1,14}$)");
|
|
213
|
+
}
|
|
214
|
+
return trimmed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _addressJson(value) {
|
|
218
|
+
if (value == null) return null;
|
|
219
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
220
|
+
throw new TypeError("vendors: address must be a plain object or null");
|
|
221
|
+
}
|
|
222
|
+
var encoded;
|
|
223
|
+
try { encoded = JSON.stringify(value); }
|
|
224
|
+
catch (_e) {
|
|
225
|
+
throw new TypeError("vendors: address must be JSON-serialisable");
|
|
226
|
+
}
|
|
227
|
+
if (encoded.length > MAX_ADDRESS_JSON_LEN) {
|
|
228
|
+
throw new TypeError("vendors: address JSON must be <= " + MAX_ADDRESS_JSON_LEN + " characters serialised");
|
|
229
|
+
}
|
|
230
|
+
if (CONTROL_BYTE_STRICT_RE.test(encoded) || ZERO_WIDTH_RE.test(encoded)) {
|
|
231
|
+
throw new TypeError("vendors: address contains control / zero-width bytes");
|
|
232
|
+
}
|
|
233
|
+
return encoded;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function _commissionSplit(n) {
|
|
237
|
+
if (!Number.isInteger(n) || n < 0 || n > MAX_BPS) {
|
|
238
|
+
throw new TypeError("vendors: commission_split_bps must be an integer 0.." + MAX_BPS);
|
|
239
|
+
}
|
|
240
|
+
return n;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _vendorStatus(s) {
|
|
244
|
+
if (typeof s !== "string" || VENDOR_STATUSES.indexOf(s) === -1) {
|
|
245
|
+
throw new TypeError("vendors: status must be one of " + VENDOR_STATUSES.join(", "));
|
|
246
|
+
}
|
|
247
|
+
return s;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function _grossMinor(n) {
|
|
251
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
252
|
+
throw new TypeError("vendors: gross_minor must be a non-negative integer");
|
|
253
|
+
}
|
|
254
|
+
if (n > MAX_AMOUNT_MINOR) {
|
|
255
|
+
throw new TypeError("vendors: gross_minor must be <= " + MAX_AMOUNT_MINOR);
|
|
256
|
+
}
|
|
257
|
+
return n;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function _currency(s) {
|
|
261
|
+
if (typeof s !== "string" || !/^[A-Z]{3}$/.test(s)) {
|
|
262
|
+
throw new TypeError("vendors: currency must be a 3-letter uppercase ISO-4217 code");
|
|
263
|
+
}
|
|
264
|
+
return s;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function _orderId(s) {
|
|
268
|
+
if (typeof s !== "string" || !s.length) {
|
|
269
|
+
throw new TypeError("vendors: order_id must be a non-empty string");
|
|
270
|
+
}
|
|
271
|
+
if (s.length > 256) {
|
|
272
|
+
throw new TypeError("vendors: order_id must be <= 256 characters");
|
|
273
|
+
}
|
|
274
|
+
if (CONTROL_BYTE_STRICT_RE.test(s) || ZERO_WIDTH_RE.test(s)) {
|
|
275
|
+
throw new TypeError("vendors: order_id contains control / zero-width bytes");
|
|
276
|
+
}
|
|
277
|
+
return s;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _normalizeEmail(input) {
|
|
281
|
+
if (typeof input !== "string" || !input.length) {
|
|
282
|
+
throw new TypeError("vendors: contact_email must be a non-empty string");
|
|
283
|
+
}
|
|
284
|
+
var guardEmail = _b().guardEmail;
|
|
285
|
+
var report;
|
|
286
|
+
try {
|
|
287
|
+
report = guardEmail.validate(input, { profile: "strict" });
|
|
288
|
+
} catch (e) {
|
|
289
|
+
throw new TypeError("vendors: contact_email — " + (e && e.message || "invalid email"));
|
|
290
|
+
}
|
|
291
|
+
if (!report || report.ok === false) {
|
|
292
|
+
var first = (report && report.issues && report.issues[0]) || {};
|
|
293
|
+
throw new TypeError("vendors: contact_email — " + (first.snippet || first.ruleId || "refused at strict profile"));
|
|
294
|
+
}
|
|
295
|
+
var canonical;
|
|
296
|
+
try {
|
|
297
|
+
canonical = guardEmail.sanitize(input, { profile: "strict" });
|
|
298
|
+
} catch (e2) {
|
|
299
|
+
throw new TypeError("vendors: contact_email — " + (e2 && e2.message || "refused"));
|
|
300
|
+
}
|
|
301
|
+
return canonical.trim().toLowerCase();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function _now() { return Date.now(); }
|
|
305
|
+
|
|
306
|
+
// ---- commission math ----------------------------------------------------
|
|
307
|
+
|
|
308
|
+
function _computeCommissionMinor(grossMinor, splitBps) {
|
|
309
|
+
// Floor division on integers keeps the rounding consistent across
|
|
310
|
+
// platforms; the operator absorbs the sub-cent dust (alternative is
|
|
311
|
+
// rounding up, which lets a vendor over-claim by splitting orders).
|
|
312
|
+
return Math.floor((grossMinor * splitBps) / BPS_DENOMINATOR);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---- row hydration ------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
function _hydrateVendor(row) {
|
|
318
|
+
if (!row) return null;
|
|
319
|
+
return {
|
|
320
|
+
slug: row.slug,
|
|
321
|
+
name: row.name,
|
|
322
|
+
contact_email_hash: row.contact_email_hash,
|
|
323
|
+
contact_email_normalised: row.contact_email_normalised,
|
|
324
|
+
contact_phone: row.contact_phone == null ? null : row.contact_phone,
|
|
325
|
+
address: row.address_json == null ? null : JSON.parse(row.address_json),
|
|
326
|
+
payout_method: row.payout_method,
|
|
327
|
+
payout_address: row.payout_address,
|
|
328
|
+
commission_split_bps: Number(row.commission_split_bps),
|
|
329
|
+
status: row.status,
|
|
330
|
+
paused_at: row.paused_at == null ? null : Number(row.paused_at),
|
|
331
|
+
archived_at: row.archived_at == null ? null : Number(row.archived_at),
|
|
332
|
+
created_at: Number(row.created_at),
|
|
333
|
+
updated_at: Number(row.updated_at),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- factory ------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
function create(opts) {
|
|
340
|
+
opts = opts || {};
|
|
341
|
+
var query = opts.query;
|
|
342
|
+
if (!query) {
|
|
343
|
+
query = function (sql, params) { return _b().externalDb.query(sql, params); };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _hashEmail(canonicalEmail) {
|
|
347
|
+
return _b().crypto.namespaceHash(EMAIL_NAMESPACE, canonicalEmail);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function _getVendorRaw(slug) {
|
|
351
|
+
var r = await query("SELECT * FROM vendors WHERE slug = ?1", [slug]);
|
|
352
|
+
return r.rows[0] || null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function _getCommissionRaw(id) {
|
|
356
|
+
var r = await query("SELECT * FROM vendor_commissions WHERE id = ?1", [id]);
|
|
357
|
+
return r.rows[0] || null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
EMAIL_NAMESPACE: EMAIL_NAMESPACE,
|
|
362
|
+
PAYOUT_METHODS: PAYOUT_METHODS.slice(),
|
|
363
|
+
VENDOR_STATUSES: VENDOR_STATUSES.slice(),
|
|
364
|
+
COMMISSION_STATUSES: COMMISSION_STATUSES.slice(),
|
|
365
|
+
MAX_NAME_LEN: MAX_NAME_LEN,
|
|
366
|
+
MAX_PAYOUT_ADDRESS_LEN: MAX_PAYOUT_ADDRESS_LEN,
|
|
367
|
+
MAX_ADDRESS_JSON_LEN: MAX_ADDRESS_JSON_LEN,
|
|
368
|
+
MAX_PHONE_LEN: MAX_PHONE_LEN,
|
|
369
|
+
MAX_SLUG_LEN: MAX_SLUG_LEN,
|
|
370
|
+
MAX_SKU_LEN: MAX_SKU_LEN,
|
|
371
|
+
BPS_DENOMINATOR: BPS_DENOMINATOR,
|
|
372
|
+
MAX_BPS: MAX_BPS,
|
|
373
|
+
|
|
374
|
+
registerVendor: async function (input) {
|
|
375
|
+
if (!input || typeof input !== "object") {
|
|
376
|
+
throw new TypeError("vendors.registerVendor: input object required");
|
|
377
|
+
}
|
|
378
|
+
var slug = _slug(input.slug, "slug");
|
|
379
|
+
var name = _name(input.name);
|
|
380
|
+
var emailNorm = _normalizeEmail(input.contact_email);
|
|
381
|
+
var emailHash = _hashEmail(emailNorm);
|
|
382
|
+
var phone = _phone(input.contact_phone);
|
|
383
|
+
var addressJson = _addressJson(input.address);
|
|
384
|
+
var payoutMethod = _payoutMethod(input.payout_method);
|
|
385
|
+
var payoutAddr = _payoutAddress(input.payout_address);
|
|
386
|
+
var splitBps = _commissionSplit(input.commission_split_bps);
|
|
387
|
+
var status = _vendorStatus(input.status);
|
|
388
|
+
if (status === "archived") {
|
|
389
|
+
// Refuse opening a vendor in the terminal state — archive
|
|
390
|
+
// is the operator-driven soft-delete, never a registration
|
|
391
|
+
// outcome. Operators register active|paused and transition
|
|
392
|
+
// later via archiveVendor.
|
|
393
|
+
throw new TypeError("vendors.registerVendor: status must be 'active' or 'paused' at registration");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Slug uniqueness — PRIMARY KEY enforces it at SQL too, but
|
|
397
|
+
// surfacing the refusal as a typed error is friendlier than
|
|
398
|
+
// letting SQLITE_CONSTRAINT leak.
|
|
399
|
+
var existing = await _getVendorRaw(slug);
|
|
400
|
+
if (existing) {
|
|
401
|
+
var dupe = new Error("vendors.registerVendor: slug " + JSON.stringify(slug) + " already registered");
|
|
402
|
+
dupe.code = "VENDOR_SLUG_TAKEN";
|
|
403
|
+
throw dupe;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
var ts = _now();
|
|
407
|
+
await query(
|
|
408
|
+
"INSERT INTO vendors " +
|
|
409
|
+
"(slug, name, contact_email_hash, contact_email_normalised, " +
|
|
410
|
+
" contact_phone, address_json, payout_method, payout_address, " +
|
|
411
|
+
" commission_split_bps, status, paused_at, archived_at, " +
|
|
412
|
+
" created_at, updated_at) " +
|
|
413
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, NULL, ?12, ?12)",
|
|
414
|
+
[
|
|
415
|
+
slug, name, emailHash, emailNorm, phone, addressJson,
|
|
416
|
+
payoutMethod, payoutAddr, splitBps, status,
|
|
417
|
+
status === "paused" ? ts : null, ts,
|
|
418
|
+
],
|
|
419
|
+
);
|
|
420
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
getVendor: async function (slug) {
|
|
424
|
+
_slug(slug, "slug");
|
|
425
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
// Alias — operator vocabulary keeps both names; the slug IS the
|
|
429
|
+
// identifier so there's no other lookup path to disambiguate
|
|
430
|
+
// against. `getVendor` and `vendorBySlug` resolve identically.
|
|
431
|
+
vendorBySlug: async function (slug) {
|
|
432
|
+
_slug(slug, "slug");
|
|
433
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
listVendors: async function (listOpts) {
|
|
437
|
+
listOpts = listOpts || {};
|
|
438
|
+
var sql, params;
|
|
439
|
+
if (listOpts.status != null) {
|
|
440
|
+
var status = _vendorStatus(listOpts.status);
|
|
441
|
+
sql = "SELECT * FROM vendors WHERE status = ?1 ORDER BY created_at DESC, slug ASC";
|
|
442
|
+
params = [status];
|
|
443
|
+
} else {
|
|
444
|
+
sql = "SELECT * FROM vendors ORDER BY created_at DESC, slug ASC";
|
|
445
|
+
params = [];
|
|
446
|
+
}
|
|
447
|
+
var r = await query(sql, params);
|
|
448
|
+
return r.rows.map(_hydrateVendor);
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
// Patch-style update — only ALLOWED_UPDATE_COLUMNS can be set.
|
|
452
|
+
// Slug + contact_email are immutable post-registration (changing
|
|
453
|
+
// the slug would orphan vendor_skus + vendor_commissions; changing
|
|
454
|
+
// the email would orphan the email-hash audit trail). Archived
|
|
455
|
+
// vendors refuse mutation — the terminal state preserves the row
|
|
456
|
+
// as-shipped for audit; operators register a successor.
|
|
457
|
+
updateVendor: async function (slug, patch) {
|
|
458
|
+
_slug(slug, "slug");
|
|
459
|
+
if (!patch || typeof patch !== "object") {
|
|
460
|
+
throw new TypeError("vendors.updateVendor: patch object required");
|
|
461
|
+
}
|
|
462
|
+
var keys = Object.keys(patch);
|
|
463
|
+
if (!keys.length) {
|
|
464
|
+
throw new TypeError("vendors.updateVendor: patch must contain at least one column");
|
|
465
|
+
}
|
|
466
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
467
|
+
if (ALLOWED_UPDATE_COLUMNS.indexOf(keys[i]) === -1) {
|
|
468
|
+
throw new TypeError("vendors.updateVendor: column '" + keys[i] + "' not updatable");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
var current = await _getVendorRaw(slug);
|
|
473
|
+
if (!current) return null;
|
|
474
|
+
if (current.status === "archived") {
|
|
475
|
+
var refused = new Error("vendors.updateVendor: refused — vendor is archived");
|
|
476
|
+
refused.code = "VENDOR_ARCHIVED";
|
|
477
|
+
throw refused;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
var sets = [];
|
|
481
|
+
var params = [];
|
|
482
|
+
var idx = 1;
|
|
483
|
+
function _set(col, val) {
|
|
484
|
+
sets.push(col + " = ?" + idx);
|
|
485
|
+
params.push(val);
|
|
486
|
+
idx += 1;
|
|
487
|
+
}
|
|
488
|
+
if (patch.name != null) _set("name", _name(patch.name));
|
|
489
|
+
if (Object.prototype.hasOwnProperty.call(patch, "contact_phone")) _set("contact_phone", _phone(patch.contact_phone));
|
|
490
|
+
if (Object.prototype.hasOwnProperty.call(patch, "address")) _set("address_json", _addressJson(patch.address));
|
|
491
|
+
if (patch.payout_method != null) _set("payout_method", _payoutMethod(patch.payout_method));
|
|
492
|
+
if (patch.payout_address != null) _set("payout_address", _payoutAddress(patch.payout_address));
|
|
493
|
+
if (patch.commission_split_bps != null) _set("commission_split_bps", _commissionSplit(patch.commission_split_bps));
|
|
494
|
+
|
|
495
|
+
var ts = _now();
|
|
496
|
+
_set("updated_at", ts);
|
|
497
|
+
params.push(slug);
|
|
498
|
+
var sql = "UPDATE vendors SET " + sets.join(", ") + " WHERE slug = ?" + idx;
|
|
499
|
+
await query(sql, params);
|
|
500
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
// FSM: active -> paused. Refuses if already paused (idempotency
|
|
504
|
+
// hazard — second call would overwrite paused_at and lose the
|
|
505
|
+
// original pause timestamp) or archived (terminal).
|
|
506
|
+
pauseVendor: async function (slug) {
|
|
507
|
+
_slug(slug, "slug");
|
|
508
|
+
var current = await _getVendorRaw(slug);
|
|
509
|
+
if (!current) return null;
|
|
510
|
+
if (current.status === "paused") {
|
|
511
|
+
var already = new Error("vendors.pauseVendor: refused — vendor is already paused");
|
|
512
|
+
already.code = "VENDOR_TRANSITION_REFUSED";
|
|
513
|
+
throw already;
|
|
514
|
+
}
|
|
515
|
+
if (current.status === "archived") {
|
|
516
|
+
var arch = new Error("vendors.pauseVendor: refused — vendor is archived");
|
|
517
|
+
arch.code = "VENDOR_TRANSITION_REFUSED";
|
|
518
|
+
throw arch;
|
|
519
|
+
}
|
|
520
|
+
var ts = _now();
|
|
521
|
+
await query(
|
|
522
|
+
"UPDATE vendors SET status = 'paused', paused_at = ?1, updated_at = ?1 WHERE slug = ?2",
|
|
523
|
+
[ts, slug],
|
|
524
|
+
);
|
|
525
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
// FSM: paused -> active. Refuses if active (no-op) or archived
|
|
529
|
+
// (terminal). Clears paused_at so the audit trail records the
|
|
530
|
+
// most recent pause window only — operator can rebuild full
|
|
531
|
+
// history from the operator-audit-log primitive if needed.
|
|
532
|
+
reinstateVendor: async function (slug) {
|
|
533
|
+
_slug(slug, "slug");
|
|
534
|
+
var current = await _getVendorRaw(slug);
|
|
535
|
+
if (!current) return null;
|
|
536
|
+
if (current.status === "active") {
|
|
537
|
+
var already = new Error("vendors.reinstateVendor: refused — vendor is already active");
|
|
538
|
+
already.code = "VENDOR_TRANSITION_REFUSED";
|
|
539
|
+
throw already;
|
|
540
|
+
}
|
|
541
|
+
if (current.status === "archived") {
|
|
542
|
+
var arch = new Error("vendors.reinstateVendor: refused — vendor is archived");
|
|
543
|
+
arch.code = "VENDOR_TRANSITION_REFUSED";
|
|
544
|
+
throw arch;
|
|
545
|
+
}
|
|
546
|
+
var ts = _now();
|
|
547
|
+
await query(
|
|
548
|
+
"UPDATE vendors SET status = 'active', paused_at = NULL, updated_at = ?1 WHERE slug = ?2",
|
|
549
|
+
[ts, slug],
|
|
550
|
+
);
|
|
551
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
// FSM: active|paused -> archived (terminal). Refuses if already
|
|
555
|
+
// archived. Historical vendor_skus + vendor_commissions rows are
|
|
556
|
+
// preserved — the FK constraint cascades only on hard-delete,
|
|
557
|
+
// which this primitive never performs.
|
|
558
|
+
archiveVendor: async function (slug) {
|
|
559
|
+
_slug(slug, "slug");
|
|
560
|
+
var current = await _getVendorRaw(slug);
|
|
561
|
+
if (!current) return null;
|
|
562
|
+
if (current.status === "archived") {
|
|
563
|
+
var already = new Error("vendors.archiveVendor: refused — vendor is already archived");
|
|
564
|
+
already.code = "VENDOR_TRANSITION_REFUSED";
|
|
565
|
+
throw already;
|
|
566
|
+
}
|
|
567
|
+
var ts = _now();
|
|
568
|
+
await query(
|
|
569
|
+
"UPDATE vendors SET status = 'archived', archived_at = ?1, updated_at = ?1 WHERE slug = ?2",
|
|
570
|
+
[ts, slug],
|
|
571
|
+
);
|
|
572
|
+
return _hydrateVendor(await _getVendorRaw(slug));
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
// Assign a SKU to a vendor. The join table's UNIQUE on `sku`
|
|
576
|
+
// enforces single-vendor ownership — a second `assignSku` for the
|
|
577
|
+
// same SKU (whether to the same or a different vendor) refuses.
|
|
578
|
+
// Operators must `unassignSku` first. Archived vendors refuse new
|
|
579
|
+
// assignments; paused vendors are allowed (a pause is a temporary
|
|
580
|
+
// halt, not an inventory unhook — the operator may be onboarding
|
|
581
|
+
// SKUs while KYC clears).
|
|
582
|
+
assignSku: async function (input) {
|
|
583
|
+
if (!input || typeof input !== "object") {
|
|
584
|
+
throw new TypeError("vendors.assignSku: input object required");
|
|
585
|
+
}
|
|
586
|
+
var vendorSlug = _slug(input.vendor_slug, "vendor_slug");
|
|
587
|
+
var sku = _sku(input.sku);
|
|
588
|
+
|
|
589
|
+
var vendor = await _getVendorRaw(vendorSlug);
|
|
590
|
+
if (!vendor) {
|
|
591
|
+
var miss = new Error("vendors.assignSku: vendor not found");
|
|
592
|
+
miss.code = "VENDOR_NOT_FOUND";
|
|
593
|
+
throw miss;
|
|
594
|
+
}
|
|
595
|
+
if (vendor.status === "archived") {
|
|
596
|
+
var arch = new Error("vendors.assignSku: refused — vendor is archived");
|
|
597
|
+
arch.code = "VENDOR_ARCHIVED";
|
|
598
|
+
throw arch;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Surface the UNIQUE refusal as a typed error rather than
|
|
602
|
+
// leaking SQLITE_CONSTRAINT. The check-then-insert is racy on
|
|
603
|
+
// a real D1; the SQL UNIQUE is the actual enforcement and the
|
|
604
|
+
// caller catches both code paths via the typed error below.
|
|
605
|
+
var existing = await query(
|
|
606
|
+
"SELECT vendor_slug FROM vendor_skus WHERE sku = ?1", [sku],
|
|
607
|
+
);
|
|
608
|
+
if (existing.rows.length) {
|
|
609
|
+
var taken = new Error(
|
|
610
|
+
"vendors.assignSku: sku " + JSON.stringify(sku) +
|
|
611
|
+
" is already assigned to vendor " + JSON.stringify(existing.rows[0].vendor_slug)
|
|
612
|
+
);
|
|
613
|
+
taken.code = "VENDOR_SKU_TAKEN";
|
|
614
|
+
throw taken;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
var ts = _now();
|
|
618
|
+
try {
|
|
619
|
+
await query(
|
|
620
|
+
"INSERT INTO vendor_skus (vendor_slug, sku, assigned_at) VALUES (?1, ?2, ?3)",
|
|
621
|
+
[vendorSlug, sku, ts],
|
|
622
|
+
);
|
|
623
|
+
} catch (e) {
|
|
624
|
+
if (e && e.message && e.message.indexOf("UNIQUE") !== -1) {
|
|
625
|
+
var raced = new Error("vendors.assignSku: sku " + JSON.stringify(sku) + " is already assigned");
|
|
626
|
+
raced.code = "VENDOR_SKU_TAKEN";
|
|
627
|
+
throw raced;
|
|
628
|
+
}
|
|
629
|
+
throw e;
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
vendor_slug: vendorSlug,
|
|
633
|
+
sku: sku,
|
|
634
|
+
assigned_at: ts,
|
|
635
|
+
};
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
// Remove the (vendor, sku) assignment. Returns true when a row
|
|
639
|
+
// was removed; false when no such assignment existed (idempotent
|
|
640
|
+
// — unassigning twice is a no-op rather than a refusal). The
|
|
641
|
+
// vendor_slug guard ensures an operator can't unassign someone
|
|
642
|
+
// else's SKU by SKU alone — the call must match the owning
|
|
643
|
+
// vendor.
|
|
644
|
+
unassignSku: async function (input) {
|
|
645
|
+
if (!input || typeof input !== "object") {
|
|
646
|
+
throw new TypeError("vendors.unassignSku: input object required");
|
|
647
|
+
}
|
|
648
|
+
var vendorSlug = _slug(input.vendor_slug, "vendor_slug");
|
|
649
|
+
var sku = _sku(input.sku);
|
|
650
|
+
var r = await query(
|
|
651
|
+
"DELETE FROM vendor_skus WHERE vendor_slug = ?1 AND sku = ?2",
|
|
652
|
+
[vendorSlug, sku],
|
|
653
|
+
);
|
|
654
|
+
return Number(r.rowCount || 0) > 0;
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
// Lookup which vendor (if any) owns a given SKU. Returns the
|
|
658
|
+
// hydrated vendor row, or null when the SKU is unassigned. The
|
|
659
|
+
// join hits the UNIQUE index on `sku` so this is O(log n).
|
|
660
|
+
vendorForSku: async function (sku) {
|
|
661
|
+
_sku(sku);
|
|
662
|
+
var r = await query(
|
|
663
|
+
"SELECT v.* FROM vendor_skus s JOIN vendors v ON v.slug = s.vendor_slug WHERE s.sku = ?1",
|
|
664
|
+
[sku],
|
|
665
|
+
);
|
|
666
|
+
return _hydrateVendor(r.rows[0]);
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
// List every SKU assigned to a vendor, newest assignment first.
|
|
670
|
+
// Returns the raw `{ sku, assigned_at }` rows rather than
|
|
671
|
+
// joining the catalog — the operator console will hydrate
|
|
672
|
+
// product titles on its own page.
|
|
673
|
+
skusForVendor: async function (vendorSlug) {
|
|
674
|
+
_slug(vendorSlug, "vendor_slug");
|
|
675
|
+
var r = await query(
|
|
676
|
+
"SELECT sku, assigned_at FROM vendor_skus WHERE vendor_slug = ?1 " +
|
|
677
|
+
"ORDER BY assigned_at DESC, sku ASC",
|
|
678
|
+
[vendorSlug],
|
|
679
|
+
);
|
|
680
|
+
return r.rows.map(function (row) {
|
|
681
|
+
return { sku: row.sku, assigned_at: Number(row.assigned_at) };
|
|
682
|
+
});
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
// Write a commission row at order-completion time. The
|
|
686
|
+
// commission_minor is computed at write time from the vendor's
|
|
687
|
+
// *current* commission_split_bps; the row preserves the resolved
|
|
688
|
+
// amount so a later operator-side rate change doesn't
|
|
689
|
+
// retroactively alter historical payouts. Idempotent on
|
|
690
|
+
// (vendor_slug, order_id) — a second call returns the existing
|
|
691
|
+
// row's snapshot. Archived vendors refuse new commission writes
|
|
692
|
+
// — the historical ledger is closed.
|
|
693
|
+
recordCommission: async function (input) {
|
|
694
|
+
if (!input || typeof input !== "object") {
|
|
695
|
+
throw new TypeError("vendors.recordCommission: input object required");
|
|
696
|
+
}
|
|
697
|
+
var vendorSlug = _slug(input.vendor_slug, "vendor_slug");
|
|
698
|
+
var orderId = _orderId(input.order_id);
|
|
699
|
+
var grossMinor = _grossMinor(input.gross_minor);
|
|
700
|
+
var currency = _currency(input.currency);
|
|
701
|
+
var occurredAt;
|
|
702
|
+
if (input.occurred_at != null) {
|
|
703
|
+
if (!Number.isInteger(input.occurred_at) || input.occurred_at < 0) {
|
|
704
|
+
throw new TypeError("vendors.recordCommission: occurred_at must be a non-negative integer (ms epoch)");
|
|
705
|
+
}
|
|
706
|
+
occurredAt = input.occurred_at;
|
|
707
|
+
} else {
|
|
708
|
+
occurredAt = _now();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
var vendor = await _getVendorRaw(vendorSlug);
|
|
712
|
+
if (!vendor) {
|
|
713
|
+
var miss = new Error("vendors.recordCommission: vendor not found");
|
|
714
|
+
miss.code = "VENDOR_NOT_FOUND";
|
|
715
|
+
throw miss;
|
|
716
|
+
}
|
|
717
|
+
if (vendor.status === "archived") {
|
|
718
|
+
var arch = new Error("vendors.recordCommission: refused — vendor is archived");
|
|
719
|
+
arch.code = "VENDOR_ARCHIVED";
|
|
720
|
+
throw arch;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Idempotency on (vendor_slug, order_id) — surfaced via the
|
|
724
|
+
// UNIQUE index. A retried checkout finalisation lands on the
|
|
725
|
+
// same row.
|
|
726
|
+
var dupe = await query(
|
|
727
|
+
"SELECT * FROM vendor_commissions WHERE vendor_slug = ?1 AND order_id = ?2",
|
|
728
|
+
[vendorSlug, orderId],
|
|
729
|
+
);
|
|
730
|
+
if (dupe.rows.length) {
|
|
731
|
+
return dupe.rows[0];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
var commissionMinor = _computeCommissionMinor(
|
|
735
|
+
grossMinor, Number(vendor.commission_split_bps)
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
var id = _b().uuid.v7();
|
|
739
|
+
await query(
|
|
740
|
+
"INSERT INTO vendor_commissions " +
|
|
741
|
+
"(id, vendor_slug, order_id, gross_minor, commission_minor, " +
|
|
742
|
+
" currency, status, occurred_at, paid_at, payout_reference) " +
|
|
743
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'pending', ?7, NULL, NULL)",
|
|
744
|
+
[id, vendorSlug, orderId, grossMinor, commissionMinor, currency, occurredAt],
|
|
745
|
+
);
|
|
746
|
+
return await _getCommissionRaw(id);
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
// Operator finance dashboard — which vendors are owed as of
|
|
750
|
+
// `as_of`. Returns one row per vendor whose sum-of-pending
|
|
751
|
+
// commissions (occurred_at <= as_of) is > 0, with `pending_minor`
|
|
752
|
+
// total + commission count. Sorted by pending_minor DESC so the
|
|
753
|
+
// largest payout lands at the top. Archived vendors still appear
|
|
754
|
+
// if they have outstanding pending rows — the operator must drain
|
|
755
|
+
// the ledger before considering the relationship fully closed.
|
|
756
|
+
payoutsDue: async function (input) {
|
|
757
|
+
if (!input || typeof input !== "object") {
|
|
758
|
+
throw new TypeError("vendors.payoutsDue: input object required");
|
|
759
|
+
}
|
|
760
|
+
if (!Number.isInteger(input.as_of) || input.as_of < 0) {
|
|
761
|
+
throw new TypeError("vendors.payoutsDue: as_of must be a non-negative integer (ms epoch)");
|
|
762
|
+
}
|
|
763
|
+
var r = await query(
|
|
764
|
+
"SELECT vendor_slug, SUM(commission_minor) AS pending_minor, COUNT(*) AS commission_count " +
|
|
765
|
+
"FROM vendor_commissions " +
|
|
766
|
+
"WHERE status = 'pending' AND occurred_at <= ?1 " +
|
|
767
|
+
"GROUP BY vendor_slug " +
|
|
768
|
+
"HAVING SUM(commission_minor) > 0 " +
|
|
769
|
+
"ORDER BY pending_minor DESC, vendor_slug ASC",
|
|
770
|
+
[input.as_of],
|
|
771
|
+
);
|
|
772
|
+
return r.rows.map(function (row) {
|
|
773
|
+
return {
|
|
774
|
+
vendor_slug: row.vendor_slug,
|
|
775
|
+
pending_minor: Number(row.pending_minor || 0),
|
|
776
|
+
commission_count: Number(row.commission_count || 0),
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
module.exports = {
|
|
784
|
+
create: create,
|
|
785
|
+
EMAIL_NAMESPACE: EMAIL_NAMESPACE,
|
|
786
|
+
PAYOUT_METHODS: PAYOUT_METHODS.slice(),
|
|
787
|
+
VENDOR_STATUSES: VENDOR_STATUSES.slice(),
|
|
788
|
+
COMMISSION_STATUSES: COMMISSION_STATUSES.slice(),
|
|
789
|
+
MAX_NAME_LEN: MAX_NAME_LEN,
|
|
790
|
+
MAX_PAYOUT_ADDRESS_LEN: MAX_PAYOUT_ADDRESS_LEN,
|
|
791
|
+
MAX_ADDRESS_JSON_LEN: MAX_ADDRESS_JSON_LEN,
|
|
792
|
+
MAX_PHONE_LEN: MAX_PHONE_LEN,
|
|
793
|
+
MAX_SLUG_LEN: MAX_SLUG_LEN,
|
|
794
|
+
MAX_SKU_LEN: MAX_SKU_LEN,
|
|
795
|
+
BPS_DENOMINATOR: BPS_DENOMINATOR,
|
|
796
|
+
MAX_BPS: MAX_BPS,
|
|
797
|
+
};
|