@shoppexio/storefront-react 0.0.0 → 0.1.1

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 (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +215 -0
  4. package/dist/account.css +1171 -0
  5. package/dist/account.d.ts +23 -0
  6. package/dist/account.js +12685 -0
  7. package/dist/account.js.map +1 -0
  8. package/dist/chunk-D6XKDVAV.js +181 -0
  9. package/dist/chunk-D6XKDVAV.js.map +1 -0
  10. package/dist/index.d.ts +3087 -0
  11. package/dist/index.js +2801 -0
  12. package/dist/index.js.map +1 -0
  13. package/eject/account/account-availability.ts +171 -0
  14. package/eject/account/account-downloads.ts +118 -0
  15. package/eject/account/account-favorites.ts +99 -0
  16. package/eject/account/account-loyalty.ts +241 -0
  17. package/eject/account/account-page.tsx +410 -0
  18. package/eject/account/account-payment-method.ts +99 -0
  19. package/eject/account/account-referrals.ts +68 -0
  20. package/eject/account/account-reseller-invite.ts +88 -0
  21. package/eject/account/account-reseller.ts +685 -0
  22. package/eject/account/account-rewards.ts +104 -0
  23. package/eject/account/account-settings.ts +181 -0
  24. package/eject/account/account-signin-link.ts +122 -0
  25. package/eject/account/account-subscriptions.ts +130 -0
  26. package/eject/account/account-support.ts +155 -0
  27. package/eject/account/account-warranties.ts +118 -0
  28. package/eject/account/customer-account.ts +93 -0
  29. package/eject/account/dashboard.tsx +139 -0
  30. package/eject/account/downloads.tsx +343 -0
  31. package/eject/account/favorites.tsx +201 -0
  32. package/eject/account/i18n.ts +175 -0
  33. package/eject/account/icons.tsx +218 -0
  34. package/eject/account/login.tsx +253 -0
  35. package/eject/account/messages/ar.json +464 -0
  36. package/eject/account/messages/de.json +464 -0
  37. package/eject/account/messages/en.json +464 -0
  38. package/eject/account/messages/es.json +464 -0
  39. package/eject/account/messages/fi.json +464 -0
  40. package/eject/account/messages/fr.json +464 -0
  41. package/eject/account/messages/hi.json +464 -0
  42. package/eject/account/messages/it.json +464 -0
  43. package/eject/account/messages/lt.json +464 -0
  44. package/eject/account/messages/nl.json +464 -0
  45. package/eject/account/messages/pl.json +464 -0
  46. package/eject/account/messages/pt.json +464 -0
  47. package/eject/account/messages/ru.json +464 -0
  48. package/eject/account/messages/tr.json +464 -0
  49. package/eject/account/messages/uk.json +464 -0
  50. package/eject/account/messages/zh.json +464 -0
  51. package/eject/account/orders.tsx +791 -0
  52. package/eject/account/overview.tsx +147 -0
  53. package/eject/account/referrals.tsx +327 -0
  54. package/eject/account/reseller.tsx +1365 -0
  55. package/eject/account/rewards.tsx +641 -0
  56. package/eject/account/settings.tsx +699 -0
  57. package/eject/account/shared.tsx +398 -0
  58. package/eject/account/subscriptions.tsx +421 -0
  59. package/eject/account/support.tsx +453 -0
  60. package/package.json +79 -1
@@ -0,0 +1,685 @@
1
+ /**
2
+ * Pure wire narrowing and basket quote helpers for the wholesale account tab.
3
+ *
4
+ * The shapes below follow the mapper functions in
5
+ * `apps/backend-elysia/src/routes/v1/customer-reseller.ts` exactly. They do not
6
+ * model what the UI wishes the API returned. A missing producer field fails the
7
+ * complete read, so Clean Minimal never renders a partly guessed money or order state.
8
+ */
9
+
10
+ export type ResellerEnrollmentMode = "MANUAL" | "APPLICATION" | "OPEN";
11
+ export type ResellerStatus =
12
+ | "INVITED"
13
+ | "APPLIED"
14
+ | "ACTIVE"
15
+ | "SUSPENDED"
16
+ | "REJECTED";
17
+ export type ResellerAppliedOverride = "variant" | "product" | "tier";
18
+
19
+ export interface ResellerTier {
20
+ id: string;
21
+ name: string | null;
22
+ discount_percent: number | null;
23
+ }
24
+
25
+ export interface ResellerRelationship {
26
+ id: string;
27
+ status: ResellerStatus;
28
+ tier: ResellerTier | null;
29
+ application_note: string | null;
30
+ lifetime_revenue: string;
31
+ lifetime_orders: number;
32
+ approved_at: string | null;
33
+ created_at: string;
34
+ }
35
+
36
+ export interface ResellerProgramState {
37
+ program_enabled: boolean;
38
+ enrollment_mode: ResellerEnrollmentMode;
39
+ application_message: string | null;
40
+ require_application_note: boolean;
41
+ reseller: ResellerRelationship | null;
42
+ }
43
+
44
+ export interface ResellerCatalogVariant {
45
+ variant_id: string;
46
+ title: string;
47
+ stock: number;
48
+ supplier_backed: boolean;
49
+ orderable: boolean;
50
+ base_price: string;
51
+ unit_price: string;
52
+ discount_percent: number;
53
+ applied_override: ResellerAppliedOverride;
54
+ }
55
+
56
+ export interface ResellerCatalogItem {
57
+ product_id: string;
58
+ uniqid: string;
59
+ title: string;
60
+ type: string;
61
+ stock: number;
62
+ supplier_backed: boolean;
63
+ orderable: boolean;
64
+ volume_discount_enabled: boolean;
65
+ base_price: string;
66
+ unit_price: string;
67
+ discount_percent: number;
68
+ applied_override: ResellerAppliedOverride;
69
+ variants: ResellerCatalogVariant[];
70
+ }
71
+
72
+ export interface ResellerCatalog {
73
+ currency: string;
74
+ total: number;
75
+ page: number;
76
+ per_page: number;
77
+ items: ResellerCatalogItem[];
78
+ }
79
+
80
+ export interface ResellerCatalogPayload {
81
+ tier: { id: string; name: string; discount_percent: number };
82
+ catalog: ResellerCatalog;
83
+ }
84
+
85
+ export interface ResellerOrderItemInput {
86
+ product_id: string;
87
+ variant_id?: string;
88
+ quantity: number;
89
+ }
90
+
91
+ /** Producer limits from `ResellerOrderItemSchema`. Quantities are units, not fractions. */
92
+ export const RESELLER_QUANTITY_MIN_UNITS = 1;
93
+ export const RESELLER_QUANTITY_MAX_UNITS = 100_000;
94
+
95
+ export function isValidResellerQuantity(value: number): boolean {
96
+ return Number.isInteger(value)
97
+ && value >= RESELLER_QUANTITY_MIN_UNITS
98
+ && value <= RESELLER_QUANTITY_MAX_UNITS;
99
+ }
100
+
101
+ export interface ResellerQuoteItem {
102
+ product_id: string;
103
+ product_title: string;
104
+ product_type: string;
105
+ variant_id: string | null;
106
+ variant_title: string | null;
107
+ quantity: number;
108
+ base_price: string;
109
+ discount_percent: number;
110
+ unit_price: string;
111
+ line_total: string;
112
+ applied_override: ResellerAppliedOverride;
113
+ volume_discount_applied: string;
114
+ }
115
+
116
+ export interface ResellerQuote {
117
+ currency: string;
118
+ tier: { id: string; name: string; discount_percent: number };
119
+ items: ResellerQuoteItem[];
120
+ subtotal: string;
121
+ volume_discount_total: string;
122
+ total: string;
123
+ savings: string;
124
+ }
125
+
126
+ export interface ResellerOrderListItem {
127
+ uniqid: string;
128
+ status: string;
129
+ currency: string;
130
+ total: string;
131
+ item_count: number;
132
+ created_at: string;
133
+ }
134
+
135
+ export interface ResellerOrderList {
136
+ total: number;
137
+ page: number;
138
+ per_page: number;
139
+ items: ResellerOrderListItem[];
140
+ }
141
+
142
+ export interface ResellerOrderLine {
143
+ product_id: string | null;
144
+ product_title: string;
145
+ variant_id: string | null;
146
+ variant_title: string | null;
147
+ quantity: number;
148
+ unit_price: string;
149
+ total: string;
150
+ delivered: boolean;
151
+ delivered_at: string | null;
152
+ serials: string[];
153
+ }
154
+
155
+ export interface ResellerOrderDetail {
156
+ uniqid: string;
157
+ status: string;
158
+ currency: string;
159
+ total: string;
160
+ item_count: number;
161
+ created_at: string;
162
+ tier_name: string;
163
+ tier_discount_percent: number;
164
+ partially_delivered: boolean;
165
+ refunded_amount: string | null;
166
+ lines: ResellerOrderLine[];
167
+ }
168
+
169
+ export interface ResellerWallet {
170
+ currency: string;
171
+ balance: string;
172
+ hold_balance: string;
173
+ available: string;
174
+ is_active: boolean;
175
+ }
176
+
177
+ export interface ResellerApiKey {
178
+ id: string;
179
+ name: string;
180
+ key_last4: string;
181
+ last_used_at: string | null;
182
+ revoked_at: string | null;
183
+ created_at: string;
184
+ }
185
+
186
+ export type NarrowResult<T> = { ok: true; data: T } | { ok: false };
187
+
188
+ function isRecord(value: unknown): value is Record<string, unknown> {
189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
190
+ }
191
+
192
+ function isNullableString(value: unknown): value is string | null {
193
+ return value === null || typeof value === "string";
194
+ }
195
+
196
+ function isFiniteNumber(value: unknown): value is number {
197
+ return typeof value === "number" && Number.isFinite(value);
198
+ }
199
+
200
+ function isEnrollmentMode(value: unknown): value is ResellerEnrollmentMode {
201
+ return value === "MANUAL" || value === "APPLICATION" || value === "OPEN";
202
+ }
203
+
204
+ function isResellerStatus(value: unknown): value is ResellerStatus {
205
+ return (
206
+ value === "INVITED" ||
207
+ value === "APPLIED" ||
208
+ value === "ACTIVE" ||
209
+ value === "SUSPENDED" ||
210
+ value === "REJECTED"
211
+ );
212
+ }
213
+
214
+ function isAppliedOverride(value: unknown): value is ResellerAppliedOverride {
215
+ return value === "variant" || value === "product" || value === "tier";
216
+ }
217
+
218
+ function narrowTier(
219
+ value: unknown,
220
+ nullableFields = false,
221
+ ): ResellerTier | null | undefined {
222
+ if (value === null) return null;
223
+ if (!isRecord(value) || typeof value.id !== "string") return undefined;
224
+ if (nullableFields) {
225
+ if (
226
+ !isNullableString(value.name) ||
227
+ !(
228
+ value.discount_percent === null ||
229
+ isFiniteNumber(value.discount_percent)
230
+ )
231
+ ) {
232
+ return undefined;
233
+ }
234
+ return {
235
+ id: value.id,
236
+ name: value.name,
237
+ discount_percent: value.discount_percent,
238
+ };
239
+ }
240
+ if (typeof value.name !== "string" || !isFiniteNumber(value.discount_percent))
241
+ return undefined;
242
+ return {
243
+ id: value.id,
244
+ name: value.name,
245
+ discount_percent: value.discount_percent,
246
+ };
247
+ }
248
+
249
+ function narrowRelationship(
250
+ value: unknown,
251
+ ): ResellerRelationship | null | undefined {
252
+ if (value === null) return null;
253
+ if (!isRecord(value)) return undefined;
254
+ const tier = narrowTier(value.tier, true);
255
+ if (
256
+ typeof value.id !== "string" ||
257
+ !isResellerStatus(value.status) ||
258
+ tier === undefined ||
259
+ !isNullableString(value.application_note) ||
260
+ typeof value.lifetime_revenue !== "string" ||
261
+ !isFiniteNumber(value.lifetime_orders) ||
262
+ !isNullableString(value.approved_at) ||
263
+ typeof value.created_at !== "string"
264
+ ) {
265
+ return undefined;
266
+ }
267
+ return {
268
+ id: value.id,
269
+ status: value.status,
270
+ tier,
271
+ application_note: value.application_note,
272
+ lifetime_revenue: value.lifetime_revenue,
273
+ lifetime_orders: value.lifetime_orders,
274
+ approved_at: value.approved_at,
275
+ created_at: value.created_at,
276
+ };
277
+ }
278
+
279
+ /** `GET /reseller`, written against the route's program response + mapReseller. */
280
+ export function narrowResellerProgram(
281
+ payload: unknown,
282
+ ): NarrowResult<ResellerProgramState> {
283
+ if (!isRecord(payload)) return { ok: false };
284
+ const reseller = narrowRelationship(payload.reseller);
285
+ if (
286
+ typeof payload.program_enabled !== "boolean" ||
287
+ !isEnrollmentMode(payload.enrollment_mode) ||
288
+ !isNullableString(payload.application_message) ||
289
+ typeof payload.require_application_note !== "boolean" ||
290
+ reseller === undefined
291
+ ) {
292
+ return { ok: false };
293
+ }
294
+ return {
295
+ ok: true,
296
+ data: {
297
+ program_enabled: payload.program_enabled,
298
+ enrollment_mode: payload.enrollment_mode,
299
+ application_message: payload.application_message,
300
+ require_application_note: payload.require_application_note,
301
+ reseller,
302
+ },
303
+ };
304
+ }
305
+
306
+ /** Enrollment mutations answer `{ reseller: mapReseller(...) }`. */
307
+ export function narrowResellerMutation(
308
+ payload: unknown,
309
+ ): NarrowResult<ResellerRelationship> {
310
+ if (!isRecord(payload)) return { ok: false };
311
+ const reseller = narrowRelationship(payload.reseller);
312
+ return reseller ? { ok: true, data: reseller } : { ok: false };
313
+ }
314
+
315
+ function narrowCatalogVariant(value: unknown): ResellerCatalogVariant | null {
316
+ if (!isRecord(value)) return null;
317
+ if (
318
+ typeof value.variant_id !== "string" ||
319
+ typeof value.title !== "string" ||
320
+ !isFiniteNumber(value.stock) ||
321
+ typeof value.supplier_backed !== "boolean" ||
322
+ typeof value.orderable !== "boolean" ||
323
+ typeof value.base_price !== "string" ||
324
+ typeof value.unit_price !== "string" ||
325
+ !isFiniteNumber(value.discount_percent) ||
326
+ !isAppliedOverride(value.applied_override)
327
+ )
328
+ return null;
329
+ return value as unknown as ResellerCatalogVariant;
330
+ }
331
+
332
+ function narrowCatalogItem(value: unknown): ResellerCatalogItem | null {
333
+ if (!isRecord(value) || !Array.isArray(value.variants)) return null;
334
+ const variants: ResellerCatalogVariant[] = [];
335
+ for (const raw of value.variants) {
336
+ const variant = narrowCatalogVariant(raw);
337
+ if (!variant) return null;
338
+ variants.push(variant);
339
+ }
340
+ if (
341
+ typeof value.product_id !== "string" ||
342
+ typeof value.uniqid !== "string" ||
343
+ typeof value.title !== "string" ||
344
+ typeof value.type !== "string" ||
345
+ !isFiniteNumber(value.stock) ||
346
+ typeof value.supplier_backed !== "boolean" ||
347
+ typeof value.orderable !== "boolean" ||
348
+ typeof value.volume_discount_enabled !== "boolean" ||
349
+ typeof value.base_price !== "string" ||
350
+ typeof value.unit_price !== "string" ||
351
+ !isFiniteNumber(value.discount_percent) ||
352
+ !isAppliedOverride(value.applied_override)
353
+ )
354
+ return null;
355
+ return {
356
+ product_id: value.product_id,
357
+ uniqid: value.uniqid,
358
+ title: value.title,
359
+ type: value.type,
360
+ stock: value.stock,
361
+ supplier_backed: value.supplier_backed,
362
+ orderable: value.orderable,
363
+ volume_discount_enabled: value.volume_discount_enabled,
364
+ base_price: value.base_price,
365
+ unit_price: value.unit_price,
366
+ discount_percent: value.discount_percent,
367
+ applied_override: value.applied_override,
368
+ variants,
369
+ };
370
+ }
371
+
372
+ /** `GET /reseller/catalog`, written against mapCatalog and its tier wrapper. */
373
+ export function narrowResellerCatalog(
374
+ payload: unknown,
375
+ ): NarrowResult<ResellerCatalogPayload> {
376
+ if (!isRecord(payload) || !isRecord(payload.catalog)) return { ok: false };
377
+ const tier = narrowTier(payload.tier);
378
+ const raw = payload.catalog;
379
+ if (
380
+ !tier ||
381
+ typeof tier.name !== "string" ||
382
+ typeof tier.discount_percent !== "number" ||
383
+ typeof raw.currency !== "string" ||
384
+ !isFiniteNumber(raw.total) ||
385
+ !isFiniteNumber(raw.page) ||
386
+ !isFiniteNumber(raw.per_page) ||
387
+ !Array.isArray(raw.items)
388
+ )
389
+ return { ok: false };
390
+ const items: ResellerCatalogItem[] = [];
391
+ for (const entry of raw.items) {
392
+ const item = narrowCatalogItem(entry);
393
+ if (!item) return { ok: false };
394
+ items.push(item);
395
+ }
396
+ return {
397
+ ok: true,
398
+ data: {
399
+ tier: {
400
+ id: tier.id,
401
+ name: tier.name,
402
+ discount_percent: tier.discount_percent,
403
+ },
404
+ catalog: {
405
+ currency: raw.currency,
406
+ total: raw.total,
407
+ page: raw.page,
408
+ per_page: raw.per_page,
409
+ items,
410
+ },
411
+ },
412
+ };
413
+ }
414
+
415
+ function narrowQuoteItem(value: unknown): ResellerQuoteItem | null {
416
+ if (!isRecord(value)) return null;
417
+ if (
418
+ typeof value.product_id !== "string" ||
419
+ typeof value.product_title !== "string" ||
420
+ typeof value.product_type !== "string" ||
421
+ !isNullableString(value.variant_id) ||
422
+ !isNullableString(value.variant_title) ||
423
+ !isValidResellerQuantity(value.quantity as number) ||
424
+ typeof value.base_price !== "string" ||
425
+ !isFiniteNumber(value.discount_percent) ||
426
+ typeof value.unit_price !== "string" ||
427
+ typeof value.line_total !== "string" ||
428
+ !isAppliedOverride(value.applied_override) ||
429
+ typeof value.volume_discount_applied !== "string"
430
+ )
431
+ return null;
432
+ return value as unknown as ResellerQuoteItem;
433
+ }
434
+
435
+ /** `POST /reseller/orders/quote`, written against mapQuote. */
436
+ export function narrowResellerQuote(
437
+ payload: unknown,
438
+ ): NarrowResult<ResellerQuote> {
439
+ if (!isRecord(payload) || !isRecord(payload.quote)) return { ok: false };
440
+ const raw = payload.quote;
441
+ const tier = narrowTier(raw.tier);
442
+ if (
443
+ !tier ||
444
+ typeof tier.name !== "string" ||
445
+ typeof tier.discount_percent !== "number" ||
446
+ typeof raw.currency !== "string" ||
447
+ !Array.isArray(raw.items) ||
448
+ typeof raw.subtotal !== "string" ||
449
+ typeof raw.volume_discount_total !== "string" ||
450
+ typeof raw.total !== "string" ||
451
+ typeof raw.savings !== "string"
452
+ )
453
+ return { ok: false };
454
+ const items: ResellerQuoteItem[] = [];
455
+ for (const entry of raw.items) {
456
+ const item = narrowQuoteItem(entry);
457
+ if (!item) return { ok: false };
458
+ items.push(item);
459
+ }
460
+ return {
461
+ ok: true,
462
+ data: {
463
+ currency: raw.currency,
464
+ tier: {
465
+ id: tier.id,
466
+ name: tier.name,
467
+ discount_percent: tier.discount_percent,
468
+ },
469
+ items,
470
+ subtotal: raw.subtotal,
471
+ volume_discount_total: raw.volume_discount_total,
472
+ total: raw.total,
473
+ savings: raw.savings,
474
+ },
475
+ };
476
+ }
477
+
478
+ function narrowOrderListItem(value: unknown): ResellerOrderListItem | null {
479
+ if (!isRecord(value)) return null;
480
+ if (
481
+ typeof value.uniqid !== "string" ||
482
+ typeof value.status !== "string" ||
483
+ typeof value.currency !== "string" ||
484
+ typeof value.total !== "string" ||
485
+ !isFiniteNumber(value.item_count) ||
486
+ typeof value.created_at !== "string"
487
+ )
488
+ return null;
489
+ return value as unknown as ResellerOrderListItem;
490
+ }
491
+
492
+ /** `GET /reseller/orders`, written against mapOrderList. */
493
+ export function narrowResellerOrders(
494
+ payload: unknown,
495
+ ): NarrowResult<ResellerOrderList> {
496
+ if (!isRecord(payload) || !isRecord(payload.orders)) return { ok: false };
497
+ const raw = payload.orders;
498
+ if (
499
+ !isFiniteNumber(raw.total) ||
500
+ !isFiniteNumber(raw.page) ||
501
+ !isFiniteNumber(raw.per_page) ||
502
+ !Array.isArray(raw.items)
503
+ )
504
+ return { ok: false };
505
+ const items: ResellerOrderListItem[] = [];
506
+ for (const entry of raw.items) {
507
+ const item = narrowOrderListItem(entry);
508
+ if (!item) return { ok: false };
509
+ items.push(item);
510
+ }
511
+ return {
512
+ ok: true,
513
+ data: { total: raw.total, page: raw.page, per_page: raw.per_page, items },
514
+ };
515
+ }
516
+
517
+ function narrowOrderLine(value: unknown): ResellerOrderLine | null {
518
+ if (
519
+ !isRecord(value) ||
520
+ !Array.isArray(value.serials) ||
521
+ !value.serials.every((serial) => typeof serial === "string")
522
+ ) {
523
+ return null;
524
+ }
525
+ if (
526
+ !isNullableString(value.product_id) ||
527
+ typeof value.product_title !== "string" ||
528
+ !isNullableString(value.variant_id) ||
529
+ !isNullableString(value.variant_title) ||
530
+ !isValidResellerQuantity(value.quantity as number) ||
531
+ typeof value.unit_price !== "string" ||
532
+ typeof value.total !== "string" ||
533
+ typeof value.delivered !== "boolean" ||
534
+ !isNullableString(value.delivered_at)
535
+ )
536
+ return null;
537
+ return value as unknown as ResellerOrderLine;
538
+ }
539
+
540
+ /** `GET /reseller/orders/:uniqid`, written against mapOrderDetail. */
541
+ export function narrowResellerOrderDetail(
542
+ payload: unknown,
543
+ ): NarrowResult<ResellerOrderDetail> {
544
+ if (!isRecord(payload) || !isRecord(payload.order)) return { ok: false };
545
+ const raw = payload.order;
546
+ if (
547
+ typeof raw.uniqid !== "string" ||
548
+ typeof raw.status !== "string" ||
549
+ typeof raw.currency !== "string" ||
550
+ typeof raw.total !== "string" ||
551
+ !isFiniteNumber(raw.item_count) ||
552
+ typeof raw.created_at !== "string" ||
553
+ typeof raw.tier_name !== "string" ||
554
+ !isFiniteNumber(raw.tier_discount_percent) ||
555
+ typeof raw.partially_delivered !== "boolean" ||
556
+ !isNullableString(raw.refunded_amount) ||
557
+ !Array.isArray(raw.lines)
558
+ )
559
+ return { ok: false };
560
+ const lines: ResellerOrderLine[] = [];
561
+ for (const entry of raw.lines) {
562
+ const line = narrowOrderLine(entry);
563
+ if (!line) return { ok: false };
564
+ lines.push(line);
565
+ }
566
+ return {
567
+ ok: true,
568
+ data: {
569
+ uniqid: raw.uniqid,
570
+ status: raw.status,
571
+ currency: raw.currency,
572
+ total: raw.total,
573
+ item_count: raw.item_count,
574
+ created_at: raw.created_at,
575
+ tier_name: raw.tier_name,
576
+ tier_discount_percent: raw.tier_discount_percent,
577
+ partially_delivered: raw.partially_delivered,
578
+ refunded_amount: raw.refunded_amount,
579
+ lines,
580
+ },
581
+ };
582
+ }
583
+
584
+ /** `GET /reseller/wallet`, narrowed against its route-local wallet mapper. */
585
+ export function narrowResellerWallet(
586
+ payload: unknown,
587
+ ): NarrowResult<ResellerWallet> {
588
+ if (!isRecord(payload) || !isRecord(payload.wallet)) return { ok: false };
589
+ const raw = payload.wallet;
590
+ if (
591
+ typeof raw.currency !== "string" ||
592
+ typeof raw.balance !== "string" ||
593
+ typeof raw.hold_balance !== "string" ||
594
+ typeof raw.available !== "string" ||
595
+ typeof raw.is_active !== "boolean"
596
+ )
597
+ return { ok: false };
598
+ return { ok: true, data: raw as unknown as ResellerWallet };
599
+ }
600
+
601
+ function narrowApiKey(value: unknown): ResellerApiKey | null {
602
+ if (!isRecord(value)) return null;
603
+ if (
604
+ typeof value.id !== "string" ||
605
+ typeof value.name !== "string" ||
606
+ typeof value.key_last4 !== "string" ||
607
+ !isNullableString(value.last_used_at) ||
608
+ !isNullableString(value.revoked_at) ||
609
+ typeof value.created_at !== "string"
610
+ )
611
+ return null;
612
+ return value as unknown as ResellerApiKey;
613
+ }
614
+
615
+ /** `GET /reseller/api-keys`, written against mapApiKey. */
616
+ export function narrowResellerApiKeys(
617
+ payload: unknown,
618
+ ): NarrowResult<ResellerApiKey[]> {
619
+ if (!isRecord(payload) || !Array.isArray(payload.api_keys))
620
+ return { ok: false };
621
+ const keys: ResellerApiKey[] = [];
622
+ for (const entry of payload.api_keys) {
623
+ const key = narrowApiKey(entry);
624
+ if (!key) return { ok: false };
625
+ keys.push(key);
626
+ }
627
+ return { ok: true, data: keys };
628
+ }
629
+
630
+ /** Stable identity of one basket version. Order is normalized for comparison. */
631
+ export function resellerBasketKey(
632
+ items: readonly ResellerOrderItemInput[],
633
+ ): string {
634
+ for (const item of items) {
635
+ if (!isValidResellerQuantity(item.quantity)) {
636
+ throw new RangeError(
637
+ `Wholesale quantity must be a whole number from ${RESELLER_QUANTITY_MIN_UNITS} to ${RESELLER_QUANTITY_MAX_UNITS} units; received ${String(item.quantity)}.`,
638
+ );
639
+ }
640
+ }
641
+
642
+ return JSON.stringify(
643
+ [...items]
644
+ .map((item) => ({
645
+ product_id: item.product_id,
646
+ variant_id: item.variant_id ?? null,
647
+ quantity: item.quantity,
648
+ }))
649
+ .sort((left, right) => {
650
+ const product = left.product_id.localeCompare(right.product_id);
651
+ return (
652
+ product ||
653
+ (left.variant_id ?? "").localeCompare(right.variant_id ?? "")
654
+ );
655
+ }),
656
+ );
657
+ }
658
+
659
+ /**
660
+ * The one way into the program this buyer actually has.
661
+ *
662
+ * Normally the shop's enrolment mode decides. An invitation overrides it:
663
+ * `ResellerEnrollmentService.invite` requires an enabled program, not a MANUAL
664
+ * one, so a shop taking open sign-ups can still have invited someone. Offering
665
+ * that buyer the join button offers the one call the producer answers 409 to
666
+ * ("already part of the reseller program"), while the token in their mail has
667
+ * nowhere to go.
668
+ */
669
+ export function resellerEntryMode(
670
+ program: ResellerProgramState,
671
+ ): ResellerEnrollmentMode {
672
+ return program.reseller?.status === "INVITED"
673
+ ? "MANUAL"
674
+ : program.enrollment_mode;
675
+ }
676
+
677
+ /** A quote is displayed only for the basket it priced. */
678
+ export function quoteMatchesBasket(
679
+ quotedBasketKey: string | null,
680
+ items: readonly ResellerOrderItemInput[],
681
+ ): boolean {
682
+ return (
683
+ quotedBasketKey !== null && quotedBasketKey === resellerBasketKey(items)
684
+ );
685
+ }