@withpotter/lib 0.13.0-next.62 → 0.13.0-next.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,484 @@
1
+ {
2
+ "status_none": {
3
+ "status": 200,
4
+ "body": {
5
+ "state": "none"
6
+ }
7
+ },
8
+ "available_no_pack": {
9
+ "status": 200,
10
+ "body": {
11
+ "available": false,
12
+ "template": "atelier-fashion-01",
13
+ "state": "none",
14
+ "reason": "no_pack"
15
+ }
16
+ },
17
+ "error_not_owner_or_admin": {
18
+ "status": 403,
19
+ "body": {
20
+ "statusCode": 403,
21
+ "code": "not_owner_or_admin",
22
+ "message": "Only the owner or an admin of this store can change demo content."
23
+ }
24
+ },
25
+ "error_clear_nothing": {
26
+ "status": 404,
27
+ "body": {
28
+ "statusCode": 404,
29
+ "code": "no_demo_content",
30
+ "message": "There is no demo content to clear."
31
+ }
32
+ },
33
+ "error_invalid_body": {
34
+ "status": 400,
35
+ "body": {
36
+ "statusCode": 400,
37
+ "code": "invalid_body",
38
+ "message": "Unknown field \"nope\"."
39
+ }
40
+ },
41
+ "available_ok": {
42
+ "status": 200,
43
+ "body": {
44
+ "available": true,
45
+ "template": "atelier-fashion-01",
46
+ "state": "none",
47
+ "pack": {
48
+ "id": "atelier-fashion-01",
49
+ "name": "Atelier starter",
50
+ "description": "A small fashion store",
51
+ "version": "1.0.0",
52
+ "audience": "starter",
53
+ "locale": "en-NG",
54
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
55
+ },
56
+ "summary": {
57
+ "products": 2,
58
+ "collections": 1,
59
+ "contentItems": 0,
60
+ "pages": 1,
61
+ "images": 3,
62
+ "shipping": true
63
+ }
64
+ }
65
+ },
66
+ "dry_run": {
67
+ "status": 200,
68
+ "body": {
69
+ "dryRun": true,
70
+ "pack": {
71
+ "id": "atelier-fashion-01",
72
+ "version": "1.0.0",
73
+ "audience": "starter",
74
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
75
+ },
76
+ "plan": {
77
+ "products": {
78
+ "create": 2,
79
+ "update": 0,
80
+ "unchanged": 0,
81
+ "conflict": 0
82
+ },
83
+ "collections": {
84
+ "create": 1,
85
+ "update": 0,
86
+ "unchanged": 0,
87
+ "conflict": 0
88
+ },
89
+ "contentItems": {
90
+ "create": 0,
91
+ "update": 0,
92
+ "unchanged": 0,
93
+ "conflict": 0
94
+ },
95
+ "pages": {
96
+ "write": 3,
97
+ "skip": 0
98
+ },
99
+ "media": 3,
100
+ "shipping": "create"
101
+ },
102
+ "conflicts": [],
103
+ "skippedPages": []
104
+ }
105
+ },
106
+ "apply_202": {
107
+ "status": 202,
108
+ "body": {
109
+ "jobId": "30ac04a6-f11b-4888-a769-84e401dfc8df",
110
+ "state": "applying",
111
+ "resumed": false
112
+ }
113
+ },
114
+ "status_applying": {
115
+ "status": 200,
116
+ "body": {
117
+ "state": "applying",
118
+ "pack": {
119
+ "id": "atelier-fashion-01",
120
+ "template": "atelier-fashion-01",
121
+ "audience": "",
122
+ "hash": ""
123
+ },
124
+ "progress": {
125
+ "phase": "fetching",
126
+ "done": 0,
127
+ "total": 0
128
+ },
129
+ "startedAt": "2026-10-30T05:44:58.977Z",
130
+ "job": "apply",
131
+ "counts": {
132
+ "products": 0,
133
+ "collections": 0,
134
+ "pages": 0,
135
+ "media": 0
136
+ }
137
+ }
138
+ },
139
+ "error_job_in_progress": {
140
+ "status": 409,
141
+ "body": {
142
+ "statusCode": 409,
143
+ "code": "job_in_progress",
144
+ "message": "Demo content is already being changed. Wait for it to finish.",
145
+ "details": {
146
+ "job": "apply"
147
+ }
148
+ }
149
+ },
150
+ "error_adopt_busy": {
151
+ "status": 409,
152
+ "body": {
153
+ "statusCode": 409,
154
+ "code": "job_in_progress",
155
+ "message": "Demo content is already being changed. Wait for it to finish.",
156
+ "details": {
157
+ "job": "apply"
158
+ }
159
+ }
160
+ },
161
+ "status_applied": {
162
+ "status": 200,
163
+ "body": {
164
+ "state": "applied",
165
+ "pack": {
166
+ "id": "atelier-fashion-01",
167
+ "template": "atelier-fashion-01",
168
+ "version": "1.0.0",
169
+ "audience": "starter",
170
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
171
+ },
172
+ "progress": {
173
+ "phase": "activating",
174
+ "done": 3,
175
+ "total": 3
176
+ },
177
+ "startedAt": "2026-10-30T05:44:58.977Z",
178
+ "job": "apply",
179
+ "finishedAt": "2026-10-30T05:44:58.983Z",
180
+ "counts": {
181
+ "products": 2,
182
+ "collections": 1,
183
+ "pages": 1,
184
+ "media": 2
185
+ }
186
+ }
187
+ },
188
+ "available_after_apply": {
189
+ "status": 200,
190
+ "body": {
191
+ "available": true,
192
+ "template": "atelier-fashion-01",
193
+ "state": "applied",
194
+ "pack": {
195
+ "id": "atelier-fashion-01",
196
+ "name": "Atelier starter",
197
+ "description": "A small fashion store",
198
+ "version": "1.0.0",
199
+ "audience": "starter",
200
+ "locale": "en-NG",
201
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
202
+ },
203
+ "summary": {
204
+ "products": 2,
205
+ "collections": 1,
206
+ "contentItems": 0,
207
+ "pages": 1,
208
+ "images": 3,
209
+ "shipping": true
210
+ }
211
+ }
212
+ },
213
+ "sample_product": {
214
+ "id": "id-1",
215
+ "type": "physical",
216
+ "slug": "classic-tee",
217
+ "title": "Classic tee",
218
+ "tags": [
219
+ "demo-pack",
220
+ "demo-pack:atelier-fashion-01"
221
+ ],
222
+ "data": {
223
+ "demoPack": {
224
+ "id": "atelier-fashion-01",
225
+ "ref": "tee"
226
+ }
227
+ },
228
+ "isActive": true
229
+ },
230
+ "sample_collection": {
231
+ "id": "id-3",
232
+ "type": "collections",
233
+ "slug": "tops",
234
+ "title": "Tops",
235
+ "tags": [
236
+ "demo-pack",
237
+ "demo-pack:atelier-fashion-01"
238
+ ],
239
+ "data": {
240
+ "productIds": [
241
+ "id-1"
242
+ ],
243
+ "demoPack": {
244
+ "id": "atelier-fashion-01",
245
+ "ref": "tops"
246
+ }
247
+ },
248
+ "isActive": true
249
+ },
250
+ "adopt_200": {
251
+ "status": 200,
252
+ "body": {
253
+ "adopted": {
254
+ "products": [
255
+ "id-1"
256
+ ],
257
+ "collections": [],
258
+ "contentItems": []
259
+ },
260
+ "notFound": [
261
+ "ghost"
262
+ ]
263
+ }
264
+ },
265
+ "sample_product_after_adopt": {
266
+ "id": "id-1",
267
+ "type": "physical",
268
+ "slug": "classic-tee",
269
+ "title": "Classic tee",
270
+ "tags": [],
271
+ "data": {},
272
+ "isActive": true
273
+ },
274
+ "error_nothing_to_adopt": {
275
+ "status": 400,
276
+ "body": {
277
+ "statusCode": 400,
278
+ "code": "nothing_to_adopt",
279
+ "message": "Name at least one item to keep, or pass all: true."
280
+ }
281
+ },
282
+ "clear_202": {
283
+ "status": 202,
284
+ "body": {
285
+ "jobId": "6764bb70-2a79-4047-9bb8-760802c6a10d",
286
+ "state": "clearing",
287
+ "resumed": false
288
+ }
289
+ },
290
+ "status_clearing": {
291
+ "status": 200,
292
+ "body": {
293
+ "state": "clearing",
294
+ "pack": {
295
+ "id": "atelier-fashion-01",
296
+ "template": "atelier-fashion-01",
297
+ "version": "1.0.0",
298
+ "audience": "starter",
299
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
300
+ },
301
+ "progress": {
302
+ "phase": "catalogue",
303
+ "done": 0,
304
+ "total": 0
305
+ },
306
+ "startedAt": "2026-10-30T05:44:58.984Z",
307
+ "job": "clear",
308
+ "counts": {
309
+ "products": 1,
310
+ "collections": 1,
311
+ "pages": 1,
312
+ "media": 2
313
+ }
314
+ }
315
+ },
316
+ "status_after_clear": {
317
+ "status": 200,
318
+ "body": {
319
+ "state": "none",
320
+ "orphanedMedia": {
321
+ "count": 2,
322
+ "clearedAt": "2026-10-30T05:44:58.985Z"
323
+ }
324
+ }
325
+ },
326
+ "status_failed_apply": {
327
+ "status": 200,
328
+ "body": {
329
+ "state": "failed",
330
+ "pack": {
331
+ "id": "atelier-fashion-01",
332
+ "template": "atelier-fashion-01",
333
+ "version": "1.0.0",
334
+ "audience": "starter",
335
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
336
+ },
337
+ "progress": {
338
+ "phase": "products",
339
+ "done": 1,
340
+ "total": 2
341
+ },
342
+ "startedAt": "2026-10-30T05:44:58.992Z",
343
+ "job": "apply",
344
+ "finishedAt": "2026-10-30T05:44:59.006Z",
345
+ "counts": {
346
+ "products": 1,
347
+ "collections": 0,
348
+ "pages": 0,
349
+ "media": 2
350
+ },
351
+ "error": {
352
+ "code": "internal",
353
+ "message": "Something went wrong while adding the demo content. Try again."
354
+ }
355
+ }
356
+ },
357
+ "apply_resumed": {
358
+ "status": 202,
359
+ "body": {
360
+ "jobId": "ebc57efe-f23d-407b-9136-158bc45b761f",
361
+ "state": "applying",
362
+ "resumed": true
363
+ }
364
+ },
365
+ "status_applied_skipped": {
366
+ "status": 200,
367
+ "body": {
368
+ "state": "applied",
369
+ "pack": {
370
+ "id": "atelier-fashion-01",
371
+ "template": "atelier-fashion-01",
372
+ "version": "1.0.0",
373
+ "audience": "starter",
374
+ "hash": "8a250f256273eae1183683684d411baba581c483f382727fef07872428fb8fe0"
375
+ },
376
+ "progress": {
377
+ "phase": "activating",
378
+ "done": 3,
379
+ "total": 3
380
+ },
381
+ "startedAt": "2026-10-30T05:44:59.017Z",
382
+ "job": "apply",
383
+ "finishedAt": "2026-10-30T05:44:59.019Z",
384
+ "counts": {
385
+ "products": 2,
386
+ "collections": 1,
387
+ "pages": 0,
388
+ "media": 2
389
+ },
390
+ "skippedPages": [
391
+ "home"
392
+ ]
393
+ }
394
+ },
395
+ "error_pack_hash_mismatch": {
396
+ "status": 422,
397
+ "body": {
398
+ "statusCode": 422,
399
+ "code": "pack_hash_mismatch",
400
+ "message": "The downloaded pack does not match its published checksum."
401
+ }
402
+ },
403
+ "error_pack_branded_assets": {
404
+ "status": 422,
405
+ "body": {
406
+ "statusCode": 422,
407
+ "code": "pack_branded_assets",
408
+ "message": "A starter pack cannot contain branded pictures.",
409
+ "details": {
410
+ "assets": [
411
+ "hero",
412
+ "tee",
413
+ "tee2"
414
+ ]
415
+ }
416
+ }
417
+ },
418
+ "error_rate_limited": {
419
+ "status": 429,
420
+ "body": {
421
+ "statusCode": 429,
422
+ "code": "rate_limited",
423
+ "message": "Demo content was applied too often. Try again later.",
424
+ "details": {
425
+ "retryAfterSeconds": 3600
426
+ }
427
+ }
428
+ },
429
+ "enums": {
430
+ "states": [
431
+ "none",
432
+ "applying",
433
+ "applied",
434
+ "clearing",
435
+ "failed"
436
+ ],
437
+ "jobs": [
438
+ "apply",
439
+ "clear"
440
+ ],
441
+ "phases": [
442
+ "fetching",
443
+ "media",
444
+ "products",
445
+ "collections",
446
+ "content",
447
+ "tenant",
448
+ "pages",
449
+ "shipping",
450
+ "pruning",
451
+ "activating",
452
+ "catalogue",
453
+ "settings"
454
+ ],
455
+ "errorCodes": [
456
+ "invalid_body",
457
+ "not_owner_or_admin",
458
+ "showroom_not_allowed",
459
+ "pack_url_forbidden",
460
+ "pack_url_invalid",
461
+ "pack_host_not_allowed",
462
+ "template_mismatch",
463
+ "no_pack",
464
+ "no_demo_content",
465
+ "job_in_progress",
466
+ "other_pack_present",
467
+ "pack_invalid",
468
+ "pack_branded_assets",
469
+ "pack_hash_mismatch",
470
+ "pack_too_large",
471
+ "pack_fetch_failed",
472
+ "quota_exceeded",
473
+ "rate_limited",
474
+ "nothing_to_adopt",
475
+ "interrupted",
476
+ "internal"
477
+ ]
478
+ },
479
+ "marker": {
480
+ "tag": "demo-pack",
481
+ "packTag": "demo-pack:atelier-fashion-01"
482
+ },
483
+ "_about": "Generated by running @withpotter/demo-content (api-next PR #620) through its real controller, guards and use cases against the in-memory world of its own specs."
484
+ }
@@ -252,6 +252,11 @@ export interface PlatformCockpit {
252
252
  performanceSeriesByCurrency?: CurrencySeries[];
253
253
  };
254
254
  attention: {
255
+ /**
256
+ * Feeds whose query failed; their counts are placeholders, not zeros.
257
+ * Absent when talking to an API that predates this field.
258
+ */
259
+ unavailable?: Array<'needsReview' | 'dormantTenants' | 'overdueInvoices' | 'failedSettlements'>;
255
260
  trialsEndingSoon: Array<AttentionTenantRef & {
256
261
  trialEndsAt: string;
257
262
  daysLeft: number;
@@ -299,6 +304,8 @@ export interface PlatformRevenue {
299
304
  };
300
305
  currency: string;
301
306
  recurring: {
307
+ /** False when no billing source feeds the figures below (all are 0). */
308
+ tracked?: boolean;
302
309
  mrr: number;
303
310
  arr: number;
304
311
  arpa: number;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/admin-api.ts"],"sourcesContent":["import {\n apiFetch,\n type DashboardStats,\n type FinanceAnalytics,\n type Transaction,\n} from './api';\n\nexport type {\n AccountRow,\n AccountsPage,\n AccountTenantRow,\n TenantTeamMember,\n AccountsAnalytics,\n} from './admin-account-types';\nimport type {\n AccountRow,\n AccountsPage,\n AccountTenantRow,\n TenantTeamMember,\n AccountsAnalytics,\n} from './admin-account-types';\nimport type { TenantSetup } from './admin-tenant-setup-types';\nimport type { CountryFields } from './admin-country-api';\nimport { tenantsQuery, type TenantListParams } from './admin-tenants-query';\nimport type { AccountOfficer } from './admin-account-officer-api';\nimport type {\n CurrencyAmount,\n CurrencyGmvKpi,\n CurrencyOverdue,\n CurrencyPerformance,\n CurrencyRevenueGmv,\n CurrencyRevenueQuality,\n CurrencySeries,\n CurrencySettlementSummary,\n CurrencyTopTenants,\n CurrencyTotals,\n FinanceByCurrency,\n TenantCurrencyMetrics,\n} from './admin-currency-types';\nexport type * from './admin-currency-types';\n\nexport type { DashboardStats, FinanceAnalytics, Transaction } from './api';\n\n/** A merchant's finance dashboard as the admin API sends it: per currency too. */\nexport type AdminFinanceAnalytics = FinanceAnalytics & FinanceByCurrency;\n\nexport interface TenantTransactionsPage {\n transactions: Transaction[];\n pagination: {\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n };\n}\n\n// ============================================\n// PLATFORM ADMIN API (growth cockpit)\n// Cross-tenant aggregates — admin/superadmin only.\n// ============================================\n\nexport interface PlatformOverview {\n tenants: { total: number; active: number; new30d: number; new7d: number };\n accounts: { total: number; new30d: number };\n orders: { total: number; gmv: number; gmv30d: number };\n subscriptions: { active: number; trialing: number };\n growth: { tenantSeries: Array<{ date: string; count: number }> };\n}\n\nexport interface TenantsAnalytics {\n range: { startDate: string; endDate: string };\n totals: {\n tenants: number;\n accounts: number;\n avgTenantsPerAccount: number;\n activeRate: number;\n gmv: number;\n orders: number;\n aov: number;\n gmvInRange: number;\n ordersInRange: number;\n newTenantsInRange: number;\n };\n statusBreakdown: Array<{ status: string; count: number }>;\n industryBreakdown: Array<{ industry: string; count: number }>;\n onboardingSeries: Array<{ date: string; count: number }>;\n performanceSeries: Array<{ date: string; gmv: number; orders: number }>;\n /** Absent until the matching api deploy lands — guard with `?? []`. */\n previousOnboardingSeries?: Array<{ date: string; count: number }>;\n previousPerformanceSeries?: Array<{\n date: string;\n gmv: number;\n orders: number;\n }>;\n topTenants: Array<{\n id: string;\n businessName: string;\n slug: string;\n gmv: number;\n orders: number;\n }>;\n /** @deprecated adds currencies together, not converted. */\n baseCurrency: string;\n /** @deprecated never convert with these. */\n rates: Record<string, number>;\n currencies?: string[];\n totalsByCurrency?: CurrencyTotals[];\n performanceSeriesByCurrency?: CurrencySeries[];\n topTenantsByCurrency?: CurrencyTopTenants[];\n}\n\nexport interface CockpitKpi {\n current: number;\n previous: number;\n pct: number | null;\n}\n\ninterface AttentionTenantRef {\n id: string;\n businessName: string;\n slug: string;\n}\n\nexport interface PlatformCockpit {\n range: { startDate: string; endDate: string };\n // Base currency all monetary figures are expressed in (USD). `rates` maps a\n // currency code to its rate-to-USD, so the client can render figures in a\n // chosen display currency by dividing a USD amount by the target's rate.\n baseCurrency: string;\n rates: Record<string, number>;\n /** Currencies with money in this payload, A→Z. */\n currencies?: string[];\n topTenantsByCurrency?: CurrencyTopTenants[];\n kpis: {\n newTenants: CockpitKpi;\n newAccounts: CockpitKpi;\n /** @deprecated adds currencies together; use `gmvByCurrency`. */\n gmv: CockpitKpi;\n orders: CockpitKpi;\n gmvByCurrency?: CurrencyGmvKpi[];\n };\n totals: {\n totalTenants: number;\n activeTenants: number;\n totalAccounts: number;\n activeSubscriptions: number;\n trialingSubscriptions: number;\n /** @deprecated adds currencies together; use `lifetimeGmvByCurrency`. */\n lifetimeGmv: number;\n lifetimeGmvByCurrency?: CurrencyAmount[];\n };\n sparklines: {\n tenants: number[];\n accounts: number[];\n gmv: number[];\n orders: number[];\n };\n statusBreakdown: Array<{ status: string; count: number }>;\n industryBreakdown: Array<{ industry: string; count: number }>;\n topTenants: Array<{\n id: string;\n businessName: string;\n slug: string;\n gmv: number;\n orders: number;\n }>;\n health?: {\n mrr: number;\n newMrr: number;\n churnedMrr: number;\n netNewMrr: number;\n grossMrrChurnPct: number;\n grossRevenueRetentionPct: number;\n newLogos: number;\n churnedLogos: number;\n netNewLogos: number;\n logoChurnPct: number;\n activePayingLogos: number;\n gmvConcentrationTop10Pct: number;\n };\n performance?: {\n takeRatePct: number | null;\n annualizedGmvRunRate: number;\n gmvPerActiveTenant: number;\n revenuePerActiveTenant: number;\n transactingTenants: number;\n transactingTenantRatePct: number;\n gmvGrowthPct: number | null;\n ordersGrowthPct: number | null;\n netNewLogos: number;\n byCurrency?: CurrencyPerformance[];\n };\n verticals?: Array<{\n industry: string;\n tenants: number;\n gmv: number;\n orders: number;\n aov: number;\n gmvGrowthPct: number | null;\n share: number;\n }>;\n /** GMV/orders across every service Box offers (sell-products, event-tickets, …). */\n salesByService?: Array<{\n service: string;\n merchants: number;\n gmv: number;\n orders: number;\n aov: number;\n gmvGrowthPct: number | null;\n share: number;\n }>;\n activation?: {\n funnel: Array<{ stage: string; count: number }>;\n medianDaysToFirstOrder: number | null;\n activationRatePct: number;\n orderConversionPct: number;\n };\n accounts?: {\n total: number;\n active: number;\n suspended: number;\n verified: number;\n unverifiedEmail: number;\n neverLoggedIn: number;\n dormant: number;\n new: number;\n };\n trend: {\n performanceSeries: Array<{ date: string; gmv: number; orders: number }>;\n onboardingSeries: Array<{ date: string; count: number }>;\n /** Absent until the matching api deploy lands — guard with `?? []`. */\n previousPerformanceSeries?: Array<{\n date: string;\n gmv: number;\n orders: number;\n }>;\n previousOnboardingSeries?: Array<{ date: string; count: number }>;\n performanceSeriesByCurrency?: CurrencySeries[];\n };\n attention: {\n trialsEndingSoon: Array<\n AttentionTenantRef & { trialEndsAt: string; daysLeft: number }\n >;\n needsReview: Array<\n AttentionTenantRef & { status: string; createdAt: string }\n >;\n dormantTenants: Array<AttentionTenantRef & { createdAt: string }>;\n overdueInvoices: {\n count: number;\n /** @deprecated adds currencies together; use `totalByCurrency`. */\n total: number;\n totalByCurrency?: CurrencyOverdue[];\n sample: Array<{\n id: string;\n invoiceNumber: string;\n tenantId: string;\n tenantName: string;\n customerName: string | null;\n amountDue: number;\n currency: string;\n dueDate: string | null;\n }>;\n };\n failedSettlements: {\n count: number;\n sample: Array<{\n id: string;\n tenantId: string;\n tenantName: string;\n netAmount: number;\n currency: string;\n createdAt: string;\n }>;\n };\n };\n}\n\nexport interface PlatformRevenue {\n range: { startDate: string; endDate: string };\n currency: string;\n recurring: {\n mrr: number;\n arr: number;\n arpa: number;\n activeSubscriptions: number;\n trialingSubscriptions: number;\n pastDueSubscriptions: number;\n nonRenewingSubscriptions: number;\n cancelledSubscriptions: number;\n newInRange: number;\n churnedInRange: number;\n planBreakdown: Array<{\n planId: string;\n name: string;\n interval: string;\n subscribers: number;\n mrr: number;\n }>;\n };\n gmv: {\n lifetime: number;\n inRange: number;\n ordersInRange: number;\n aov: number;\n series: Array<{ date: string; gmv: number; orders: number }>;\n topTenants: Array<{\n id: string;\n businessName: string;\n slug: string;\n gmv: number;\n orders: number;\n }>;\n };\n quality?: {\n quickRatio: number | null;\n nrrPct: number | null;\n expansionMrr: number;\n contractionMrr: number;\n mrrChurnRatePct: number | null;\n logoChurnRatePct: number | null;\n grossGmv: number;\n refundedGmv: number;\n netGmv: number;\n refundRatePct: number;\n refundedOrders: number;\n cohorts: Array<{\n cohort: string;\n size: number;\n retention: Array<number | null>;\n }>;\n gmvByCurrency?: CurrencyRevenueQuality[];\n };\n /** @deprecated never convert with these. */\n rates: Record<string, number>;\n currencies?: string[];\n gmvByCurrency?: CurrencyRevenueGmv[];\n}\n\nexport type TenantStatus = 'active' | 'suspended' | 'pending' | 'deleted';\n\nexport interface TenantRow extends CountryFields {\n id: string;\n slug: string;\n businessName: string;\n status: TenantStatus;\n industryKey: string | null;\n createdAt: string;\n /** @deprecated adds currencies together; use `gmvByCurrency`. */\n gmv: number;\n gmvByCurrency?: CurrencyAmount[];\n /** The merchant's account officer (absent on older APIs). */\n accountOfficer?: Pick<AccountOfficer, 'id' | 'name'> | null;\n}\n\nexport interface TenantsPage {\n rows: TenantRow[];\n total: number;\n page: number;\n pageSize: number;\n}\n\nexport interface TenantActivityOrder {\n id: string;\n orderNumber: string;\n total: number;\n currency: string;\n status: string;\n paymentStatus: string;\n customerEmail: string;\n customerName: string | null;\n createdAt: string;\n}\n\nexport interface TenantDetail extends TenantSetup {\n /** Who runs the workspace and how to reach it (absent on older APIs). */\n contact?: { email: string | null; phone: string | null };\n owner?: TenantTeamMember | null;\n team?: TenantTeamMember[];\n /** The admin in charge of this merchant (absent on older APIs). */\n accountOfficer?: AccountOfficer | null;\n id: string;\n slug: string;\n businessName: string;\n status: TenantStatus;\n industryKey: string | null;\n customDomain: string | null;\n createdAt: string;\n chargeBearer: 'merchant' | 'customer';\n currency: string;\n metrics: {\n totalOrders: number;\n paidOrders: number;\n pendingOrders: number;\n gmv: number;\n gmv30d: number;\n gmv7d: number;\n aov: number;\n totalCustomers: number;\n subscriptionStatus: string | null;\n window: {\n startDate: string;\n endDate: string;\n gmv: number;\n orders: number;\n paidOrders: number;\n aov: number;\n customers: number;\n };\n byCurrency?: TenantCurrencyMetrics[];\n };\n /** Currencies with paid orders, A→Z. */\n currencies?: string[];\n revenueSeries: Array<{ date: string; amount: number }>;\n revenueSeriesByCurrency?: Array<{\n currency: string;\n series: Array<{ date: string; amount: number }>;\n }>;\n recentOrders: TenantActivityOrder[];\n}\n\n// ---- Iteration 3: tenant cockpit domains ----\n\nexport interface TenantCustomer {\n id: string;\n email: string;\n phone: string | null;\n name: string | null;\n type: string;\n source: string | null;\n status: string;\n emailVerified: boolean;\n totalOrders: number;\n totalSpent: number;\n lastOrderAt: string | null;\n createdAt: string;\n}\n\nexport interface TenantCustomersPage {\n members: TenantCustomer[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n\nexport interface TenantProduct {\n id: string;\n type: string;\n slug: string;\n title: string;\n category: string | null;\n tags: string[];\n featured: boolean;\n isActive: boolean;\n image?: { url: string } | null;\n price: number | null;\n compareAtPrice: number | null;\n currency: string | null;\n trackInventory: boolean;\n quantity: number | null;\n reserved: number | null;\n lowStockThreshold: number | null;\n createdAt: string;\n}\n\nexport interface TenantProductStats {\n total: number;\n active: number;\n featured: number;\n lowStock: number;\n outOfStock: number;\n}\n\nexport interface TenantProductsData {\n items: TenantProduct[];\n total: number;\n stats: TenantProductStats;\n}\n\nexport interface ProductRef {\n id: string;\n title: string;\n}\n\nexport interface SellerRow {\n id: string;\n title: string;\n units: number;\n revenue: number;\n}\n\nexport interface CategorySlice {\n category: string;\n count: number;\n value: number;\n}\n\nexport interface ProductInsights {\n currency: string;\n totalProducts: number;\n activeProducts: number;\n featuredProducts: number;\n lowStock: number;\n outOfStock: number;\n trackedProducts: number;\n catalogValue: number;\n inventoryValue: number;\n inventoryAtRiskValue: number;\n avgPrice: number;\n medianPrice: number;\n totalUnitsSold: number;\n totalRevenue: number;\n pctEverSold: number;\n deadStock: number;\n newestProduct: (ProductRef & { createdAt: string }) | null;\n lastUpdatedProduct: (ProductRef & { updatedAt: string }) | null;\n categories: CategorySlice[];\n bestSellers: SellerRow[];\n worstSellers: SellerRow[];\n featuredSellThrough: {\n featuredTotal: number;\n featuredSold: number;\n featuredUnits: number;\n nonFeaturedTotal: number;\n nonFeaturedSold: number;\n nonFeaturedUnits: number;\n };\n unitsSeries: Array<{ date: string; count: number }>;\n}\n\nexport interface SpenderRow {\n id: string;\n name: string | null;\n email: string;\n totalSpent: number;\n totalOrders: number;\n}\n\nexport interface BreakdownSlice {\n label: string;\n count: number;\n}\n\nexport interface CustomerInsights {\n currency: string;\n totalCustomers: number;\n newCustomers30d: number;\n payingCustomers: number;\n repeatCustomers: number;\n repeatRate: number;\n dormantCustomers: number;\n marketingOptInRate: number;\n lifetimeValue: number;\n avgSpendPerCustomer: number;\n avgOrdersPerCustomer: number;\n avgOrderValue: number;\n topSpenders: SpenderRow[];\n bySource: BreakdownSlice[];\n byType: BreakdownSlice[];\n signupsSeries: Array<{ date: string; count: number }>;\n}\n\nexport interface TenantInvoice {\n id: string;\n invoiceNumber: string;\n status: string;\n customerEmail: string | null;\n customerName: string | null;\n total: number;\n amountPaid: number;\n amountDue: number;\n currency: string;\n dueDate: string | null;\n createdAt: string;\n}\n\nexport interface TenantInvoiceStats {\n draft: number;\n sent: number;\n paid: number;\n overdue: number;\n cancelled: number;\n total: number;\n totalRevenue: number;\n totalOutstanding: number;\n}\n\nexport interface TenantInvoicesData {\n invoices: TenantInvoice[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n stats: TenantInvoiceStats;\n}\n\nexport interface TenantLoyaltyProgram {\n id: string;\n name: string;\n pointsName: string;\n pointsPerCurrency: number;\n currencyPerPoint: number;\n isActive: boolean;\n tiers?: Array<{ id: string; name: string; threshold: number }>;\n earnRules?: Array<{ id: string; name: string; points: number }>;\n}\n\nexport interface TenantLoyaltyStats {\n totalMembers: number;\n totalPointsIssued: number;\n totalPointsRedeemed: number;\n activeMembers: number;\n programActive: boolean;\n}\n\nexport interface TenantLoyaltyData {\n program: TenantLoyaltyProgram | null;\n stats: TenantLoyaltyStats;\n}\n\nexport interface TenantDiscount {\n id: string;\n name: string;\n code: string;\n discountType: string;\n value: number;\n maxDiscountAmount: number | null;\n startsAt: string | null;\n endsAt: string | null;\n usageLimit: number | null;\n usageCount: number;\n status: string;\n autoApply: boolean;\n isValid: boolean;\n isExpired: boolean;\n createdAt: string;\n}\n\nexport interface TenantDiscountsPage {\n discounts: TenantDiscount[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n\nexport interface TenantSettlement {\n id: string;\n grossAmount: number;\n paystackFee: number;\n platformFee: number;\n netAmount: number;\n currency: string;\n status: string;\n settlementType: string;\n bankAccount?: { bankName: string; accountNumber: string } | null;\n settledAt: string | null;\n createdAt: string;\n}\n\nexport interface TenantSettlementSummary {\n totalSettled: number;\n totalPending: number;\n totalFailed: number;\n count: number;\n platformFeesCollected: number;\n currency: string;\n currencies?: string[];\n byCurrency?: CurrencySettlementSummary[];\n}\n\nexport interface TenantBankAccount {\n id: string;\n bankCode: string;\n bankName: string;\n accountNumber: string;\n accountName: string;\n splitPercentage: number | null;\n status: string;\n isDefault: boolean;\n /** Paystack subaccount code (ACCT_…). */\n subaccountCode?: string | null;\n failureReason?: string | null;\n createdAt: string;\n}\n\nexport interface TenantSettlementsData {\n settlements: TenantSettlement[];\n total: number;\n page: number;\n limit: number;\n summary: TenantSettlementSummary;\n bankAccounts: TenantBankAccount[];\n}\n\nexport interface TenantSubscriptionPlan {\n id: string;\n name: string;\n interval: string;\n amount: number;\n currency: string;\n}\n\nexport interface TenantSubscription {\n id: string;\n planId: string;\n customerId: string;\n customerEmail: string;\n status: string;\n amount: number;\n currency: string;\n currentPeriodStart: string | null;\n currentPeriodEnd: string | null;\n nextPaymentDate: string | null;\n cancelledAt: string | null;\n paymentsCount: number;\n createdAt: string;\n plan?: TenantSubscriptionPlan | null;\n customer?: { id: string; name: string | null; email: string } | null;\n}\n\nexport interface TenantSubscriptionStats {\n totalActive: number;\n totalNonRenewing: number;\n totalCancelled: number;\n monthlyRecurringRevenue: number;\n}\n\nexport interface TenantSubscriptionsData {\n subscriptions: TenantSubscription[];\n total: number;\n page: number;\n limit: number;\n stats: TenantSubscriptionStats;\n}\n\nexport interface AdminsAnalytics {\n range: { startDate: string; endDate: string };\n totals: {\n admins: number;\n active: number;\n suspended: number;\n superadmins: number;\n standardAdmins: number;\n neverLoggedIn: number;\n activeInRange: number;\n actionsInRange: number;\n };\n roleBreakdown: Array<{ role: string; count: number }>;\n statusBreakdown: Array<{ status: string; count: number }>;\n activitySeries: Array<{ date: string; count: number }>;\n topActions: Array<{ action: string; count: number }>;\n recentActivity: Array<{\n id: string;\n adminEmail: string | null;\n action: string;\n targetType: string | null;\n createdAt: string;\n }>;\n}\n\nexport interface AdminSession {\n id: string;\n email: string;\n name: string | null;\n role: 'admin' | 'superadmin';\n permissions: string[];\n mustChangePassword: boolean;\n}\n\nexport interface AdminLoginCodeRequired {\n codeRequired: true;\n challengeId: string;\n /** The address the code went to, masked: `a***@gmail.com`. */\n sentTo: string;\n}\n\nexport type AdminLoginAnswer =\n | AdminLoginCodeRequired\n | {\n codeRequired?: undefined;\n mustChangePassword: boolean;\n admin: Omit<AdminSession, 'permissions'>;\n };\n\nexport const platformAdminApi = {\n /**\n * Password step. A box that emails sign-in codes answers `codeRequired` and\n * sets no session yet; finish with `verifyLoginCode`.\n */\n login: (email: string, password: string) =>\n apiFetch<AdminLoginAnswer>('/platform-admin/v1/auth/login', {\n method: 'POST',\n body: JSON.stringify({ email, password }),\n }),\n verifyLoginCode: (challengeId: string, code: string) =>\n apiFetch<{\n mustChangePassword: boolean;\n admin: Omit<AdminSession, 'permissions'>;\n }>('/platform-admin/v1/auth/login/verify', {\n method: 'POST',\n body: JSON.stringify({ challengeId, code }),\n }),\n resendLoginCode: (challengeId: string) =>\n apiFetch<AdminLoginCodeRequired>('/platform-admin/v1/auth/login/resend', {\n method: 'POST',\n body: JSON.stringify({ challengeId }),\n }),\n logout: () =>\n apiFetch<{ success: boolean }>('/platform-admin/v1/auth/logout', {\n method: 'POST',\n }),\n me: () => apiFetch<AdminSession>('/platform-admin/v1/auth/me'),\n changePassword: (currentPassword: string, newPassword: string) =>\n apiFetch<{ success: boolean }>('/platform-admin/v1/auth/change-password', {\n method: 'POST',\n body: JSON.stringify({ currentPassword, newPassword }),\n }),\n overview: () => apiFetch<PlatformOverview>('/platform-admin/v1/overview'),\n cockpit: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<PlatformCockpit>(\n `/platform-admin/v1/overview/cockpit${q ? `?${q}` : ''}`,\n );\n },\n revenue: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<PlatformRevenue>(\n `/platform-admin/v1/overview/revenue${q ? `?${q}` : ''}`,\n );\n },\n tenantsAnalytics: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<TenantsAnalytics>(\n `/platform-admin/v1/overview/tenants-analytics${q ? `?${q}` : ''}`,\n );\n },\n tenants: (params: TenantListParams = {}) =>\n apiFetch<TenantsPage>(`/platform-admin/v1/tenants${tenantsQuery(params)}`),\n tenant: (\n id: string,\n range: { startDate?: string; endDate?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n if (range.endDate) qs.set('endDate', range.endDate);\n const query = qs.toString();\n return apiFetch<TenantDetail>(\n `/platform-admin/v1/tenants/${id}${query ? `?${query}` : ''}`,\n );\n },\n tenantAnalytics: (\n id: string,\n range: { startDate?: string; endDate?: string; limit?: number } = {},\n ) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n if (range.endDate) qs.set('endDate', range.endDate);\n if (range.limit) qs.set('limit', String(range.limit));\n const query = qs.toString();\n return apiFetch<DashboardStats>(\n `/platform-admin/v1/tenants/${id}/analytics${query ? `?${query}` : ''}`,\n );\n },\n tenantFinance: (\n id: string,\n range: { startDate?: string; endDate?: string; period?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n if (range.endDate) qs.set('endDate', range.endDate);\n if (range.period) qs.set('period', range.period);\n const query = qs.toString();\n return apiFetch<AdminFinanceAnalytics>(\n `/platform-admin/v1/tenants/${id}/finance${query ? `?${query}` : ''}`,\n );\n },\n tenantTransactions: (\n id: string,\n params: {\n status?: string;\n type?: string;\n search?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.type) qs.set('type', params.type);\n if (params.search) qs.set('search', params.search);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantTransactionsPage>(\n `/platform-admin/v1/tenants/${id}/transactions${\n query ? `?${query}` : ''\n }`,\n );\n },\n tenantCustomers: (\n id: string,\n params: {\n search?: string;\n type?: string;\n status?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.search) qs.set('search', params.search);\n if (params.type) qs.set('type', params.type);\n if (params.status) qs.set('status', params.status);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantCustomersPage>(\n `/platform-admin/v1/tenants/${id}/customers${query ? `?${query}` : ''}`,\n );\n },\n tenantProducts: (\n id: string,\n params: { type?: string; search?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.type) qs.set('type', params.type);\n if (params.search) qs.set('search', params.search);\n const query = qs.toString();\n return apiFetch<TenantProductsData>(\n `/platform-admin/v1/tenants/${id}/products${query ? `?${query}` : ''}`,\n );\n },\n tenantProductInsights: (id: string, range: { startDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n const query = qs.toString();\n return apiFetch<ProductInsights>(\n `/platform-admin/v1/tenants/${id}/products/insights${\n query ? `?${query}` : ''\n }`,\n );\n },\n tenantCustomerInsights: (id: string, range: { startDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n const query = qs.toString();\n return apiFetch<CustomerInsights>(\n `/platform-admin/v1/tenants/${id}/customers/insights${\n query ? `?${query}` : ''\n }`,\n );\n },\n tenantInvoices: (\n id: string,\n params: {\n status?: string;\n search?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.search) qs.set('search', params.search);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantInvoicesData>(\n `/platform-admin/v1/tenants/${id}/invoices${query ? `?${query}` : ''}`,\n );\n },\n tenantLoyalty: (id: string) =>\n apiFetch<TenantLoyaltyData>(`/platform-admin/v1/tenants/${id}/loyalty`),\n tenantDiscounts: (\n id: string,\n params: {\n status?: string;\n discountType?: string;\n search?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.discountType) qs.set('discountType', params.discountType);\n if (params.search) qs.set('search', params.search);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantDiscountsPage>(\n `/platform-admin/v1/tenants/${id}/discounts${query ? `?${query}` : ''}`,\n );\n },\n tenantSettlements: (\n id: string,\n params: {\n page?: number;\n limit?: number;\n startDate?: string;\n endDate?: string;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const query = qs.toString();\n return apiFetch<TenantSettlementsData>(\n `/platform-admin/v1/tenants/${id}/settlements${query ? `?${query}` : ''}`,\n );\n },\n tenantSubscriptions: (\n id: string,\n params: {\n status?: string;\n planId?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.planId) qs.set('planId', params.planId);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantSubscriptionsData>(\n `/platform-admin/v1/tenants/${id}/subscriptions${\n query ? `?${query}` : ''\n }`,\n );\n },\n setTenantStatus: (id: string, status: TenantStatus) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/tenants/${id}/status`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n }),\n accounts: (\n params: {\n page?: number;\n pageSize?: number;\n search?: string;\n joinedFrom?: string;\n joinedTo?: string;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.page) qs.set('page', String(params.page));\n if (params.pageSize) qs.set('pageSize', String(params.pageSize));\n if (params.search) qs.set('search', params.search);\n if (params.joinedFrom) qs.set('joinedFrom', params.joinedFrom);\n if (params.joinedTo) qs.set('joinedTo', params.joinedTo);\n const query = qs.toString();\n return apiFetch<AccountsPage>(\n `/platform-admin/v1/accounts${query ? `?${query}` : ''}`,\n );\n },\n accountsAnalytics: (\n params: { startDate?: string; endDate?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<AccountsAnalytics>(\n `/platform-admin/v1/accounts/analytics${q ? `?${q}` : ''}`,\n );\n },\n setAccountStatus: (id: string, status: string) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/accounts/${id}/status`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n }),\n verifyAccount: (id: string) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/accounts/${id}/verify`, {\n method: 'POST',\n }),\n accountTenants: (id: string) =>\n apiFetch<AccountTenantRow[]>(`/platform-admin/v1/accounts/${id}/tenants`),\n impersonateAccount: (id: string) =>\n apiFetch<{ success: boolean }>(\n `/platform-admin/v1/accounts/${id}/impersonate`,\n { method: 'POST' },\n ),\n admins: () => apiFetch<AdminRow[]>('/platform-admin/v1/admins'),\n adminsAnalytics: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<AdminsAnalytics>(\n `/platform-admin/v1/admins/analytics${q ? `?${q}` : ''}`,\n );\n },\n adminPermissions: () =>\n apiFetch<string[]>('/platform-admin/v1/admins/permissions'),\n inviteAdmin: (\n email: string,\n role: 'admin' | 'superadmin',\n permissions: string[] = [],\n roleIds: string[] = [],\n ) =>\n apiFetch<{ email: string; tempPassword: string }>(\n '/platform-admin/v1/admins',\n {\n method: 'POST',\n body: JSON.stringify({ email, role, permissions, roleIds }),\n },\n ),\n setAdminStatus: (id: string, status: 'active' | 'suspended') =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/admins/${id}/status`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n }),\n setAdminPermissions: (id: string, permissions: string[]) =>\n apiFetch<{ success: boolean }>(\n `/platform-admin/v1/admins/${id}/permissions`,\n { method: 'PATCH', body: JSON.stringify({ permissions }) },\n ),\n setAdminRoles: (id: string, roleIds: string[]) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/admins/${id}/roles`, {\n method: 'PATCH',\n body: JSON.stringify({ roleIds }),\n }),\n resetAdminPassword: (id: string) =>\n apiFetch<{ tempPassword: string }>(\n `/platform-admin/v1/admins/${id}/reset-password`,\n { method: 'POST' },\n ),\n roles: () => apiFetch<RoleRow[]>('/platform-admin/v1/roles'),\n createRole: (name: string, permissions: string[], description?: string) =>\n apiFetch<RoleRow>('/platform-admin/v1/roles', {\n method: 'POST',\n body: JSON.stringify({ name, permissions, description }),\n }),\n updateRole: (\n id: string,\n data: { name?: string; permissions?: string[]; description?: string },\n ) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/roles/${id}`, {\n method: 'PATCH',\n body: JSON.stringify(data),\n }),\n deleteRole: (id: string) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/roles/${id}`, {\n method: 'DELETE',\n }),\n audit: (\n params: {\n page?: number;\n pageSize?: number;\n search?: string;\n action?: string;\n adminEmail?: string;\n from?: string;\n to?: string;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.page) qs.set('page', String(params.page));\n if (params.pageSize) qs.set('pageSize', String(params.pageSize));\n if (params.search) qs.set('search', params.search);\n if (params.action) qs.set('action', params.action);\n if (params.adminEmail) qs.set('adminEmail', params.adminEmail);\n if (params.from) qs.set('from', params.from);\n if (params.to) qs.set('to', params.to);\n const q = qs.toString();\n return apiFetch<AuditPage>(`/platform-admin/v1/audit${q ? `?${q}` : ''}`);\n },\n auditAnalytics: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<AuditAnalytics>(\n `/platform-admin/v1/audit/analytics${q ? `?${q}` : ''}`,\n );\n },\n};\n\nexport interface AdminRow {\n id: string;\n email: string;\n name: string | null;\n role: 'admin' | 'superadmin';\n status: 'active' | 'suspended';\n permissions: string[];\n roleIds: string[];\n mustChangePassword: boolean;\n lastLoginAt: string | null;\n createdAt: string;\n}\n\nexport interface RoleRow {\n id: string;\n key: string;\n name: string;\n description: string | null;\n permissions: string[];\n isSystem: boolean;\n assignedCount: number;\n}\n\nexport interface AuditRow {\n id: string;\n /** Who did it, by name. Absent from an engine that does not send it, or when the admin has none. */\n adminName?: string | null;\n adminEmail: string | null;\n action: string;\n targetType: string | null;\n targetId: string | null;\n metadata: Record<string, unknown>;\n ipAddress: string | null;\n /** The browser or tool the action came from. Absent from an engine that does not send it. */\n userAgent?: string | null;\n createdAt: string;\n}\n\nexport interface AuditPage {\n rows: AuditRow[];\n total: number;\n page: number;\n pageSize: number;\n}\nexport interface AuditAnalytics {\n range: { startDate: string; endDate: string };\n totals: {\n actions: number;\n activeAdmins: number;\n actionTypes: number;\n topAction: string | null;\n };\n actionSeries: Array<{ date: string; count: number }>;\n actionBreakdown: Array<{ action: string; count: number }>;\n adminBreakdown: Array<{ adminEmail: string; count: number }>;\n targetBreakdown: Array<{ targetType: string; count: number }>;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/admin-api.ts"],"sourcesContent":["import {\n apiFetch,\n type DashboardStats,\n type FinanceAnalytics,\n type Transaction,\n} from './api';\n\nexport type {\n AccountRow,\n AccountsPage,\n AccountTenantRow,\n TenantTeamMember,\n AccountsAnalytics,\n} from './admin-account-types';\nimport type {\n AccountRow,\n AccountsPage,\n AccountTenantRow,\n TenantTeamMember,\n AccountsAnalytics,\n} from './admin-account-types';\nimport type { TenantSetup } from './admin-tenant-setup-types';\nimport type { CountryFields } from './admin-country-api';\nimport { tenantsQuery, type TenantListParams } from './admin-tenants-query';\nimport type { AccountOfficer } from './admin-account-officer-api';\nimport type {\n CurrencyAmount,\n CurrencyGmvKpi,\n CurrencyOverdue,\n CurrencyPerformance,\n CurrencyRevenueGmv,\n CurrencyRevenueQuality,\n CurrencySeries,\n CurrencySettlementSummary,\n CurrencyTopTenants,\n CurrencyTotals,\n FinanceByCurrency,\n TenantCurrencyMetrics,\n} from './admin-currency-types';\nexport type * from './admin-currency-types';\n\nexport type { DashboardStats, FinanceAnalytics, Transaction } from './api';\n\n/** A merchant's finance dashboard as the admin API sends it: per currency too. */\nexport type AdminFinanceAnalytics = FinanceAnalytics & FinanceByCurrency;\n\nexport interface TenantTransactionsPage {\n transactions: Transaction[];\n pagination: {\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n };\n}\n\n// ============================================\n// PLATFORM ADMIN API (growth cockpit)\n// Cross-tenant aggregates — admin/superadmin only.\n// ============================================\n\nexport interface PlatformOverview {\n tenants: { total: number; active: number; new30d: number; new7d: number };\n accounts: { total: number; new30d: number };\n orders: { total: number; gmv: number; gmv30d: number };\n subscriptions: { active: number; trialing: number };\n growth: { tenantSeries: Array<{ date: string; count: number }> };\n}\n\nexport interface TenantsAnalytics {\n range: { startDate: string; endDate: string };\n totals: {\n tenants: number;\n accounts: number;\n avgTenantsPerAccount: number;\n activeRate: number;\n gmv: number;\n orders: number;\n aov: number;\n gmvInRange: number;\n ordersInRange: number;\n newTenantsInRange: number;\n };\n statusBreakdown: Array<{ status: string; count: number }>;\n industryBreakdown: Array<{ industry: string; count: number }>;\n onboardingSeries: Array<{ date: string; count: number }>;\n performanceSeries: Array<{ date: string; gmv: number; orders: number }>;\n /** Absent until the matching api deploy lands — guard with `?? []`. */\n previousOnboardingSeries?: Array<{ date: string; count: number }>;\n previousPerformanceSeries?: Array<{\n date: string;\n gmv: number;\n orders: number;\n }>;\n topTenants: Array<{\n id: string;\n businessName: string;\n slug: string;\n gmv: number;\n orders: number;\n }>;\n /** @deprecated adds currencies together, not converted. */\n baseCurrency: string;\n /** @deprecated never convert with these. */\n rates: Record<string, number>;\n currencies?: string[];\n totalsByCurrency?: CurrencyTotals[];\n performanceSeriesByCurrency?: CurrencySeries[];\n topTenantsByCurrency?: CurrencyTopTenants[];\n}\n\nexport interface CockpitKpi {\n current: number;\n previous: number;\n pct: number | null;\n}\n\ninterface AttentionTenantRef {\n id: string;\n businessName: string;\n slug: string;\n}\n\nexport interface PlatformCockpit {\n range: { startDate: string; endDate: string };\n // Base currency all monetary figures are expressed in (USD). `rates` maps a\n // currency code to its rate-to-USD, so the client can render figures in a\n // chosen display currency by dividing a USD amount by the target's rate.\n baseCurrency: string;\n rates: Record<string, number>;\n /** Currencies with money in this payload, A→Z. */\n currencies?: string[];\n topTenantsByCurrency?: CurrencyTopTenants[];\n kpis: {\n newTenants: CockpitKpi;\n newAccounts: CockpitKpi;\n /** @deprecated adds currencies together; use `gmvByCurrency`. */\n gmv: CockpitKpi;\n orders: CockpitKpi;\n gmvByCurrency?: CurrencyGmvKpi[];\n };\n totals: {\n totalTenants: number;\n activeTenants: number;\n totalAccounts: number;\n activeSubscriptions: number;\n trialingSubscriptions: number;\n /** @deprecated adds currencies together; use `lifetimeGmvByCurrency`. */\n lifetimeGmv: number;\n lifetimeGmvByCurrency?: CurrencyAmount[];\n };\n sparklines: {\n tenants: number[];\n accounts: number[];\n gmv: number[];\n orders: number[];\n };\n statusBreakdown: Array<{ status: string; count: number }>;\n industryBreakdown: Array<{ industry: string; count: number }>;\n topTenants: Array<{\n id: string;\n businessName: string;\n slug: string;\n gmv: number;\n orders: number;\n }>;\n health?: {\n mrr: number;\n newMrr: number;\n churnedMrr: number;\n netNewMrr: number;\n grossMrrChurnPct: number;\n grossRevenueRetentionPct: number;\n newLogos: number;\n churnedLogos: number;\n netNewLogos: number;\n logoChurnPct: number;\n activePayingLogos: number;\n gmvConcentrationTop10Pct: number;\n };\n performance?: {\n takeRatePct: number | null;\n annualizedGmvRunRate: number;\n gmvPerActiveTenant: number;\n revenuePerActiveTenant: number;\n transactingTenants: number;\n transactingTenantRatePct: number;\n gmvGrowthPct: number | null;\n ordersGrowthPct: number | null;\n netNewLogos: number;\n byCurrency?: CurrencyPerformance[];\n };\n verticals?: Array<{\n industry: string;\n tenants: number;\n gmv: number;\n orders: number;\n aov: number;\n gmvGrowthPct: number | null;\n share: number;\n }>;\n /** GMV/orders across every service Box offers (sell-products, event-tickets, …). */\n salesByService?: Array<{\n service: string;\n merchants: number;\n gmv: number;\n orders: number;\n aov: number;\n gmvGrowthPct: number | null;\n share: number;\n }>;\n activation?: {\n funnel: Array<{ stage: string; count: number }>;\n medianDaysToFirstOrder: number | null;\n activationRatePct: number;\n orderConversionPct: number;\n };\n accounts?: {\n total: number;\n active: number;\n suspended: number;\n verified: number;\n unverifiedEmail: number;\n neverLoggedIn: number;\n dormant: number;\n new: number;\n };\n trend: {\n performanceSeries: Array<{ date: string; gmv: number; orders: number }>;\n onboardingSeries: Array<{ date: string; count: number }>;\n /** Absent until the matching api deploy lands — guard with `?? []`. */\n previousPerformanceSeries?: Array<{\n date: string;\n gmv: number;\n orders: number;\n }>;\n previousOnboardingSeries?: Array<{ date: string; count: number }>;\n performanceSeriesByCurrency?: CurrencySeries[];\n };\n attention: {\n /**\n * Feeds whose query failed; their counts are placeholders, not zeros.\n * Absent when talking to an API that predates this field.\n */\n unavailable?: Array<\n 'needsReview' | 'dormantTenants' | 'overdueInvoices' | 'failedSettlements'\n >;\n trialsEndingSoon: Array<\n AttentionTenantRef & { trialEndsAt: string; daysLeft: number }\n >;\n needsReview: Array<\n AttentionTenantRef & { status: string; createdAt: string }\n >;\n dormantTenants: Array<AttentionTenantRef & { createdAt: string }>;\n overdueInvoices: {\n count: number;\n /** @deprecated adds currencies together; use `totalByCurrency`. */\n total: number;\n totalByCurrency?: CurrencyOverdue[];\n sample: Array<{\n id: string;\n invoiceNumber: string;\n tenantId: string;\n tenantName: string;\n customerName: string | null;\n amountDue: number;\n currency: string;\n dueDate: string | null;\n }>;\n };\n failedSettlements: {\n count: number;\n sample: Array<{\n id: string;\n tenantId: string;\n tenantName: string;\n netAmount: number;\n currency: string;\n createdAt: string;\n }>;\n };\n };\n}\n\nexport interface PlatformRevenue {\n range: { startDate: string; endDate: string };\n currency: string;\n recurring: {\n /** False when no billing source feeds the figures below (all are 0). */\n tracked?: boolean;\n mrr: number;\n arr: number;\n arpa: number;\n activeSubscriptions: number;\n trialingSubscriptions: number;\n pastDueSubscriptions: number;\n nonRenewingSubscriptions: number;\n cancelledSubscriptions: number;\n newInRange: number;\n churnedInRange: number;\n planBreakdown: Array<{\n planId: string;\n name: string;\n interval: string;\n subscribers: number;\n mrr: number;\n }>;\n };\n gmv: {\n lifetime: number;\n inRange: number;\n ordersInRange: number;\n aov: number;\n series: Array<{ date: string; gmv: number; orders: number }>;\n topTenants: Array<{\n id: string;\n businessName: string;\n slug: string;\n gmv: number;\n orders: number;\n }>;\n };\n quality?: {\n quickRatio: number | null;\n nrrPct: number | null;\n expansionMrr: number;\n contractionMrr: number;\n mrrChurnRatePct: number | null;\n logoChurnRatePct: number | null;\n grossGmv: number;\n refundedGmv: number;\n netGmv: number;\n refundRatePct: number;\n refundedOrders: number;\n cohorts: Array<{\n cohort: string;\n size: number;\n retention: Array<number | null>;\n }>;\n gmvByCurrency?: CurrencyRevenueQuality[];\n };\n /** @deprecated never convert with these. */\n rates: Record<string, number>;\n currencies?: string[];\n gmvByCurrency?: CurrencyRevenueGmv[];\n}\n\nexport type TenantStatus = 'active' | 'suspended' | 'pending' | 'deleted';\n\nexport interface TenantRow extends CountryFields {\n id: string;\n slug: string;\n businessName: string;\n status: TenantStatus;\n industryKey: string | null;\n createdAt: string;\n /** @deprecated adds currencies together; use `gmvByCurrency`. */\n gmv: number;\n gmvByCurrency?: CurrencyAmount[];\n /** The merchant's account officer (absent on older APIs). */\n accountOfficer?: Pick<AccountOfficer, 'id' | 'name'> | null;\n}\n\nexport interface TenantsPage {\n rows: TenantRow[];\n total: number;\n page: number;\n pageSize: number;\n}\n\nexport interface TenantActivityOrder {\n id: string;\n orderNumber: string;\n total: number;\n currency: string;\n status: string;\n paymentStatus: string;\n customerEmail: string;\n customerName: string | null;\n createdAt: string;\n}\n\nexport interface TenantDetail extends TenantSetup {\n /** Who runs the workspace and how to reach it (absent on older APIs). */\n contact?: { email: string | null; phone: string | null };\n owner?: TenantTeamMember | null;\n team?: TenantTeamMember[];\n /** The admin in charge of this merchant (absent on older APIs). */\n accountOfficer?: AccountOfficer | null;\n id: string;\n slug: string;\n businessName: string;\n status: TenantStatus;\n industryKey: string | null;\n customDomain: string | null;\n createdAt: string;\n chargeBearer: 'merchant' | 'customer';\n currency: string;\n metrics: {\n totalOrders: number;\n paidOrders: number;\n pendingOrders: number;\n gmv: number;\n gmv30d: number;\n gmv7d: number;\n aov: number;\n totalCustomers: number;\n subscriptionStatus: string | null;\n window: {\n startDate: string;\n endDate: string;\n gmv: number;\n orders: number;\n paidOrders: number;\n aov: number;\n customers: number;\n };\n byCurrency?: TenantCurrencyMetrics[];\n };\n /** Currencies with paid orders, A→Z. */\n currencies?: string[];\n revenueSeries: Array<{ date: string; amount: number }>;\n revenueSeriesByCurrency?: Array<{\n currency: string;\n series: Array<{ date: string; amount: number }>;\n }>;\n recentOrders: TenantActivityOrder[];\n}\n\n// ---- Iteration 3: tenant cockpit domains ----\n\nexport interface TenantCustomer {\n id: string;\n email: string;\n phone: string | null;\n name: string | null;\n type: string;\n source: string | null;\n status: string;\n emailVerified: boolean;\n totalOrders: number;\n totalSpent: number;\n lastOrderAt: string | null;\n createdAt: string;\n}\n\nexport interface TenantCustomersPage {\n members: TenantCustomer[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n\nexport interface TenantProduct {\n id: string;\n type: string;\n slug: string;\n title: string;\n category: string | null;\n tags: string[];\n featured: boolean;\n isActive: boolean;\n image?: { url: string } | null;\n price: number | null;\n compareAtPrice: number | null;\n currency: string | null;\n trackInventory: boolean;\n quantity: number | null;\n reserved: number | null;\n lowStockThreshold: number | null;\n createdAt: string;\n}\n\nexport interface TenantProductStats {\n total: number;\n active: number;\n featured: number;\n lowStock: number;\n outOfStock: number;\n}\n\nexport interface TenantProductsData {\n items: TenantProduct[];\n total: number;\n stats: TenantProductStats;\n}\n\nexport interface ProductRef {\n id: string;\n title: string;\n}\n\nexport interface SellerRow {\n id: string;\n title: string;\n units: number;\n revenue: number;\n}\n\nexport interface CategorySlice {\n category: string;\n count: number;\n value: number;\n}\n\nexport interface ProductInsights {\n currency: string;\n totalProducts: number;\n activeProducts: number;\n featuredProducts: number;\n lowStock: number;\n outOfStock: number;\n trackedProducts: number;\n catalogValue: number;\n inventoryValue: number;\n inventoryAtRiskValue: number;\n avgPrice: number;\n medianPrice: number;\n totalUnitsSold: number;\n totalRevenue: number;\n pctEverSold: number;\n deadStock: number;\n newestProduct: (ProductRef & { createdAt: string }) | null;\n lastUpdatedProduct: (ProductRef & { updatedAt: string }) | null;\n categories: CategorySlice[];\n bestSellers: SellerRow[];\n worstSellers: SellerRow[];\n featuredSellThrough: {\n featuredTotal: number;\n featuredSold: number;\n featuredUnits: number;\n nonFeaturedTotal: number;\n nonFeaturedSold: number;\n nonFeaturedUnits: number;\n };\n unitsSeries: Array<{ date: string; count: number }>;\n}\n\nexport interface SpenderRow {\n id: string;\n name: string | null;\n email: string;\n totalSpent: number;\n totalOrders: number;\n}\n\nexport interface BreakdownSlice {\n label: string;\n count: number;\n}\n\nexport interface CustomerInsights {\n currency: string;\n totalCustomers: number;\n newCustomers30d: number;\n payingCustomers: number;\n repeatCustomers: number;\n repeatRate: number;\n dormantCustomers: number;\n marketingOptInRate: number;\n lifetimeValue: number;\n avgSpendPerCustomer: number;\n avgOrdersPerCustomer: number;\n avgOrderValue: number;\n topSpenders: SpenderRow[];\n bySource: BreakdownSlice[];\n byType: BreakdownSlice[];\n signupsSeries: Array<{ date: string; count: number }>;\n}\n\nexport interface TenantInvoice {\n id: string;\n invoiceNumber: string;\n status: string;\n customerEmail: string | null;\n customerName: string | null;\n total: number;\n amountPaid: number;\n amountDue: number;\n currency: string;\n dueDate: string | null;\n createdAt: string;\n}\n\nexport interface TenantInvoiceStats {\n draft: number;\n sent: number;\n paid: number;\n overdue: number;\n cancelled: number;\n total: number;\n totalRevenue: number;\n totalOutstanding: number;\n}\n\nexport interface TenantInvoicesData {\n invoices: TenantInvoice[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n stats: TenantInvoiceStats;\n}\n\nexport interface TenantLoyaltyProgram {\n id: string;\n name: string;\n pointsName: string;\n pointsPerCurrency: number;\n currencyPerPoint: number;\n isActive: boolean;\n tiers?: Array<{ id: string; name: string; threshold: number }>;\n earnRules?: Array<{ id: string; name: string; points: number }>;\n}\n\nexport interface TenantLoyaltyStats {\n totalMembers: number;\n totalPointsIssued: number;\n totalPointsRedeemed: number;\n activeMembers: number;\n programActive: boolean;\n}\n\nexport interface TenantLoyaltyData {\n program: TenantLoyaltyProgram | null;\n stats: TenantLoyaltyStats;\n}\n\nexport interface TenantDiscount {\n id: string;\n name: string;\n code: string;\n discountType: string;\n value: number;\n maxDiscountAmount: number | null;\n startsAt: string | null;\n endsAt: string | null;\n usageLimit: number | null;\n usageCount: number;\n status: string;\n autoApply: boolean;\n isValid: boolean;\n isExpired: boolean;\n createdAt: string;\n}\n\nexport interface TenantDiscountsPage {\n discounts: TenantDiscount[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n\nexport interface TenantSettlement {\n id: string;\n grossAmount: number;\n paystackFee: number;\n platformFee: number;\n netAmount: number;\n currency: string;\n status: string;\n settlementType: string;\n bankAccount?: { bankName: string; accountNumber: string } | null;\n settledAt: string | null;\n createdAt: string;\n}\n\nexport interface TenantSettlementSummary {\n totalSettled: number;\n totalPending: number;\n totalFailed: number;\n count: number;\n platformFeesCollected: number;\n currency: string;\n currencies?: string[];\n byCurrency?: CurrencySettlementSummary[];\n}\n\nexport interface TenantBankAccount {\n id: string;\n bankCode: string;\n bankName: string;\n accountNumber: string;\n accountName: string;\n splitPercentage: number | null;\n status: string;\n isDefault: boolean;\n /** Paystack subaccount code (ACCT_…). */\n subaccountCode?: string | null;\n failureReason?: string | null;\n createdAt: string;\n}\n\nexport interface TenantSettlementsData {\n settlements: TenantSettlement[];\n total: number;\n page: number;\n limit: number;\n summary: TenantSettlementSummary;\n bankAccounts: TenantBankAccount[];\n}\n\nexport interface TenantSubscriptionPlan {\n id: string;\n name: string;\n interval: string;\n amount: number;\n currency: string;\n}\n\nexport interface TenantSubscription {\n id: string;\n planId: string;\n customerId: string;\n customerEmail: string;\n status: string;\n amount: number;\n currency: string;\n currentPeriodStart: string | null;\n currentPeriodEnd: string | null;\n nextPaymentDate: string | null;\n cancelledAt: string | null;\n paymentsCount: number;\n createdAt: string;\n plan?: TenantSubscriptionPlan | null;\n customer?: { id: string; name: string | null; email: string } | null;\n}\n\nexport interface TenantSubscriptionStats {\n totalActive: number;\n totalNonRenewing: number;\n totalCancelled: number;\n monthlyRecurringRevenue: number;\n}\n\nexport interface TenantSubscriptionsData {\n subscriptions: TenantSubscription[];\n total: number;\n page: number;\n limit: number;\n stats: TenantSubscriptionStats;\n}\n\nexport interface AdminsAnalytics {\n range: { startDate: string; endDate: string };\n totals: {\n admins: number;\n active: number;\n suspended: number;\n superadmins: number;\n standardAdmins: number;\n neverLoggedIn: number;\n activeInRange: number;\n actionsInRange: number;\n };\n roleBreakdown: Array<{ role: string; count: number }>;\n statusBreakdown: Array<{ status: string; count: number }>;\n activitySeries: Array<{ date: string; count: number }>;\n topActions: Array<{ action: string; count: number }>;\n recentActivity: Array<{\n id: string;\n adminEmail: string | null;\n action: string;\n targetType: string | null;\n createdAt: string;\n }>;\n}\n\nexport interface AdminSession {\n id: string;\n email: string;\n name: string | null;\n role: 'admin' | 'superadmin';\n permissions: string[];\n mustChangePassword: boolean;\n}\n\nexport interface AdminLoginCodeRequired {\n codeRequired: true;\n challengeId: string;\n /** The address the code went to, masked: `a***@gmail.com`. */\n sentTo: string;\n}\n\nexport type AdminLoginAnswer =\n | AdminLoginCodeRequired\n | {\n codeRequired?: undefined;\n mustChangePassword: boolean;\n admin: Omit<AdminSession, 'permissions'>;\n };\n\nexport const platformAdminApi = {\n /**\n * Password step. A box that emails sign-in codes answers `codeRequired` and\n * sets no session yet; finish with `verifyLoginCode`.\n */\n login: (email: string, password: string) =>\n apiFetch<AdminLoginAnswer>('/platform-admin/v1/auth/login', {\n method: 'POST',\n body: JSON.stringify({ email, password }),\n }),\n verifyLoginCode: (challengeId: string, code: string) =>\n apiFetch<{\n mustChangePassword: boolean;\n admin: Omit<AdminSession, 'permissions'>;\n }>('/platform-admin/v1/auth/login/verify', {\n method: 'POST',\n body: JSON.stringify({ challengeId, code }),\n }),\n resendLoginCode: (challengeId: string) =>\n apiFetch<AdminLoginCodeRequired>('/platform-admin/v1/auth/login/resend', {\n method: 'POST',\n body: JSON.stringify({ challengeId }),\n }),\n logout: () =>\n apiFetch<{ success: boolean }>('/platform-admin/v1/auth/logout', {\n method: 'POST',\n }),\n me: () => apiFetch<AdminSession>('/platform-admin/v1/auth/me'),\n changePassword: (currentPassword: string, newPassword: string) =>\n apiFetch<{ success: boolean }>('/platform-admin/v1/auth/change-password', {\n method: 'POST',\n body: JSON.stringify({ currentPassword, newPassword }),\n }),\n overview: () => apiFetch<PlatformOverview>('/platform-admin/v1/overview'),\n cockpit: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<PlatformCockpit>(\n `/platform-admin/v1/overview/cockpit${q ? `?${q}` : ''}`,\n );\n },\n revenue: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<PlatformRevenue>(\n `/platform-admin/v1/overview/revenue${q ? `?${q}` : ''}`,\n );\n },\n tenantsAnalytics: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<TenantsAnalytics>(\n `/platform-admin/v1/overview/tenants-analytics${q ? `?${q}` : ''}`,\n );\n },\n tenants: (params: TenantListParams = {}) =>\n apiFetch<TenantsPage>(`/platform-admin/v1/tenants${tenantsQuery(params)}`),\n tenant: (\n id: string,\n range: { startDate?: string; endDate?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n if (range.endDate) qs.set('endDate', range.endDate);\n const query = qs.toString();\n return apiFetch<TenantDetail>(\n `/platform-admin/v1/tenants/${id}${query ? `?${query}` : ''}`,\n );\n },\n tenantAnalytics: (\n id: string,\n range: { startDate?: string; endDate?: string; limit?: number } = {},\n ) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n if (range.endDate) qs.set('endDate', range.endDate);\n if (range.limit) qs.set('limit', String(range.limit));\n const query = qs.toString();\n return apiFetch<DashboardStats>(\n `/platform-admin/v1/tenants/${id}/analytics${query ? `?${query}` : ''}`,\n );\n },\n tenantFinance: (\n id: string,\n range: { startDate?: string; endDate?: string; period?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n if (range.endDate) qs.set('endDate', range.endDate);\n if (range.period) qs.set('period', range.period);\n const query = qs.toString();\n return apiFetch<AdminFinanceAnalytics>(\n `/platform-admin/v1/tenants/${id}/finance${query ? `?${query}` : ''}`,\n );\n },\n tenantTransactions: (\n id: string,\n params: {\n status?: string;\n type?: string;\n search?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.type) qs.set('type', params.type);\n if (params.search) qs.set('search', params.search);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantTransactionsPage>(\n `/platform-admin/v1/tenants/${id}/transactions${\n query ? `?${query}` : ''\n }`,\n );\n },\n tenantCustomers: (\n id: string,\n params: {\n search?: string;\n type?: string;\n status?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.search) qs.set('search', params.search);\n if (params.type) qs.set('type', params.type);\n if (params.status) qs.set('status', params.status);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantCustomersPage>(\n `/platform-admin/v1/tenants/${id}/customers${query ? `?${query}` : ''}`,\n );\n },\n tenantProducts: (\n id: string,\n params: { type?: string; search?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.type) qs.set('type', params.type);\n if (params.search) qs.set('search', params.search);\n const query = qs.toString();\n return apiFetch<TenantProductsData>(\n `/platform-admin/v1/tenants/${id}/products${query ? `?${query}` : ''}`,\n );\n },\n tenantProductInsights: (id: string, range: { startDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n const query = qs.toString();\n return apiFetch<ProductInsights>(\n `/platform-admin/v1/tenants/${id}/products/insights${\n query ? `?${query}` : ''\n }`,\n );\n },\n tenantCustomerInsights: (id: string, range: { startDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (range.startDate) qs.set('startDate', range.startDate);\n const query = qs.toString();\n return apiFetch<CustomerInsights>(\n `/platform-admin/v1/tenants/${id}/customers/insights${\n query ? `?${query}` : ''\n }`,\n );\n },\n tenantInvoices: (\n id: string,\n params: {\n status?: string;\n search?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.search) qs.set('search', params.search);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantInvoicesData>(\n `/platform-admin/v1/tenants/${id}/invoices${query ? `?${query}` : ''}`,\n );\n },\n tenantLoyalty: (id: string) =>\n apiFetch<TenantLoyaltyData>(`/platform-admin/v1/tenants/${id}/loyalty`),\n tenantDiscounts: (\n id: string,\n params: {\n status?: string;\n discountType?: string;\n search?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.discountType) qs.set('discountType', params.discountType);\n if (params.search) qs.set('search', params.search);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantDiscountsPage>(\n `/platform-admin/v1/tenants/${id}/discounts${query ? `?${query}` : ''}`,\n );\n },\n tenantSettlements: (\n id: string,\n params: {\n page?: number;\n limit?: number;\n startDate?: string;\n endDate?: string;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const query = qs.toString();\n return apiFetch<TenantSettlementsData>(\n `/platform-admin/v1/tenants/${id}/settlements${query ? `?${query}` : ''}`,\n );\n },\n tenantSubscriptions: (\n id: string,\n params: {\n status?: string;\n planId?: string;\n page?: number;\n limit?: number;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.status) qs.set('status', params.status);\n if (params.planId) qs.set('planId', params.planId);\n if (params.page) qs.set('page', String(params.page));\n if (params.limit) qs.set('limit', String(params.limit));\n const query = qs.toString();\n return apiFetch<TenantSubscriptionsData>(\n `/platform-admin/v1/tenants/${id}/subscriptions${\n query ? `?${query}` : ''\n }`,\n );\n },\n setTenantStatus: (id: string, status: TenantStatus) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/tenants/${id}/status`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n }),\n accounts: (\n params: {\n page?: number;\n pageSize?: number;\n search?: string;\n joinedFrom?: string;\n joinedTo?: string;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.page) qs.set('page', String(params.page));\n if (params.pageSize) qs.set('pageSize', String(params.pageSize));\n if (params.search) qs.set('search', params.search);\n if (params.joinedFrom) qs.set('joinedFrom', params.joinedFrom);\n if (params.joinedTo) qs.set('joinedTo', params.joinedTo);\n const query = qs.toString();\n return apiFetch<AccountsPage>(\n `/platform-admin/v1/accounts${query ? `?${query}` : ''}`,\n );\n },\n accountsAnalytics: (\n params: { startDate?: string; endDate?: string } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<AccountsAnalytics>(\n `/platform-admin/v1/accounts/analytics${q ? `?${q}` : ''}`,\n );\n },\n setAccountStatus: (id: string, status: string) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/accounts/${id}/status`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n }),\n verifyAccount: (id: string) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/accounts/${id}/verify`, {\n method: 'POST',\n }),\n accountTenants: (id: string) =>\n apiFetch<AccountTenantRow[]>(`/platform-admin/v1/accounts/${id}/tenants`),\n impersonateAccount: (id: string) =>\n apiFetch<{ success: boolean }>(\n `/platform-admin/v1/accounts/${id}/impersonate`,\n { method: 'POST' },\n ),\n admins: () => apiFetch<AdminRow[]>('/platform-admin/v1/admins'),\n adminsAnalytics: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<AdminsAnalytics>(\n `/platform-admin/v1/admins/analytics${q ? `?${q}` : ''}`,\n );\n },\n adminPermissions: () =>\n apiFetch<string[]>('/platform-admin/v1/admins/permissions'),\n inviteAdmin: (\n email: string,\n role: 'admin' | 'superadmin',\n permissions: string[] = [],\n roleIds: string[] = [],\n ) =>\n apiFetch<{ email: string; tempPassword: string }>(\n '/platform-admin/v1/admins',\n {\n method: 'POST',\n body: JSON.stringify({ email, role, permissions, roleIds }),\n },\n ),\n setAdminStatus: (id: string, status: 'active' | 'suspended') =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/admins/${id}/status`, {\n method: 'PATCH',\n body: JSON.stringify({ status }),\n }),\n setAdminPermissions: (id: string, permissions: string[]) =>\n apiFetch<{ success: boolean }>(\n `/platform-admin/v1/admins/${id}/permissions`,\n { method: 'PATCH', body: JSON.stringify({ permissions }) },\n ),\n setAdminRoles: (id: string, roleIds: string[]) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/admins/${id}/roles`, {\n method: 'PATCH',\n body: JSON.stringify({ roleIds }),\n }),\n resetAdminPassword: (id: string) =>\n apiFetch<{ tempPassword: string }>(\n `/platform-admin/v1/admins/${id}/reset-password`,\n { method: 'POST' },\n ),\n roles: () => apiFetch<RoleRow[]>('/platform-admin/v1/roles'),\n createRole: (name: string, permissions: string[], description?: string) =>\n apiFetch<RoleRow>('/platform-admin/v1/roles', {\n method: 'POST',\n body: JSON.stringify({ name, permissions, description }),\n }),\n updateRole: (\n id: string,\n data: { name?: string; permissions?: string[]; description?: string },\n ) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/roles/${id}`, {\n method: 'PATCH',\n body: JSON.stringify(data),\n }),\n deleteRole: (id: string) =>\n apiFetch<{ success: boolean }>(`/platform-admin/v1/roles/${id}`, {\n method: 'DELETE',\n }),\n audit: (\n params: {\n page?: number;\n pageSize?: number;\n search?: string;\n action?: string;\n adminEmail?: string;\n from?: string;\n to?: string;\n } = {},\n ) => {\n const qs = new URLSearchParams();\n if (params.page) qs.set('page', String(params.page));\n if (params.pageSize) qs.set('pageSize', String(params.pageSize));\n if (params.search) qs.set('search', params.search);\n if (params.action) qs.set('action', params.action);\n if (params.adminEmail) qs.set('adminEmail', params.adminEmail);\n if (params.from) qs.set('from', params.from);\n if (params.to) qs.set('to', params.to);\n const q = qs.toString();\n return apiFetch<AuditPage>(`/platform-admin/v1/audit${q ? `?${q}` : ''}`);\n },\n auditAnalytics: (params: { startDate?: string; endDate?: string } = {}) => {\n const qs = new URLSearchParams();\n if (params.startDate) qs.set('startDate', params.startDate);\n if (params.endDate) qs.set('endDate', params.endDate);\n const q = qs.toString();\n return apiFetch<AuditAnalytics>(\n `/platform-admin/v1/audit/analytics${q ? `?${q}` : ''}`,\n );\n },\n};\n\nexport interface AdminRow {\n id: string;\n email: string;\n name: string | null;\n role: 'admin' | 'superadmin';\n status: 'active' | 'suspended';\n permissions: string[];\n roleIds: string[];\n mustChangePassword: boolean;\n lastLoginAt: string | null;\n createdAt: string;\n}\n\nexport interface RoleRow {\n id: string;\n key: string;\n name: string;\n description: string | null;\n permissions: string[];\n isSystem: boolean;\n assignedCount: number;\n}\n\nexport interface AuditRow {\n id: string;\n /** Who did it, by name. Absent from an engine that does not send it, or when the admin has none. */\n adminName?: string | null;\n adminEmail: string | null;\n action: string;\n targetType: string | null;\n targetId: string | null;\n metadata: Record<string, unknown>;\n ipAddress: string | null;\n /** The browser or tool the action came from. Absent from an engine that does not send it. */\n userAgent?: string | null;\n createdAt: string;\n}\n\nexport interface AuditPage {\n rows: AuditRow[];\n total: number;\n page: number;\n pageSize: number;\n}\nexport interface AuditAnalytics {\n range: { startDate: string; endDate: string };\n totals: {\n actions: number;\n activeAdmins: number;\n actionTypes: number;\n topAction: string | null;\n };\n actionSeries: Array<{ date: string; count: number }>;\n actionBreakdown: Array<{ action: string; count: number }>;\n adminBreakdown: Array<{ adminEmail: string; count: number }>;\n targetBreakdown: Array<{ targetType: string; count: number }>;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Demo content: sample products, collections, pages and pictures a merchant
3
+ * can start a store with, clear in one go, or apply again later.
4
+ *
5
+ * Contract: api-next `DEMO_CONTENT_API.md`. Every call is the current tenant's
6
+ * and needs an owner or admin; anyone else is refused by the server, and the
7
+ * screens hide the actions before that.
8
+ */
9
+ export type DemoContentState = 'none' | 'applying' | 'applied' | 'clearing' | 'failed';
10
+ export interface DemoContentPack {
11
+ id: string;
12
+ template: string;
13
+ /** Absent until the pack has been read, as is `hash` (empty while the job starts). */
14
+ version?: string;
15
+ audience: string;
16
+ hash: string;
17
+ }
18
+ export interface DemoContentProgress {
19
+ phase: string;
20
+ done: number;
21
+ total: number;
22
+ }
23
+ export interface DemoContentCounts {
24
+ products: number;
25
+ collections: number;
26
+ pages: number;
27
+ media: number;
28
+ }
29
+ export interface DemoContentStatus {
30
+ state: DemoContentState;
31
+ pack?: DemoContentPack;
32
+ progress?: DemoContentProgress;
33
+ counts?: DemoContentCounts;
34
+ startedAt?: string;
35
+ finishedAt?: string;
36
+ /** Pages the server left alone because the merchant had already edited them. */
37
+ skippedPages?: string[];
38
+ /** Which job is running, or which one failed. */
39
+ job?: 'apply' | 'clear';
40
+ error?: {
41
+ code: string;
42
+ message: string;
43
+ };
44
+ /** A clear that left uploaded pictures in the media library. */
45
+ orphanedMedia?: {
46
+ count: number;
47
+ clearedAt?: string;
48
+ };
49
+ }
50
+ /** Whether the tenant's template has a pack this caller may apply. */
51
+ export interface DemoContentAvailability {
52
+ available: boolean;
53
+ template?: string;
54
+ /** The current status state, so a screen can hide the offer once content is there. */
55
+ state?: DemoContentState;
56
+ /** Why there is nothing to offer: no_pack, audience_not_permitted, catalogue_unreachable, template_unknown. */
57
+ reason?: string;
58
+ pack?: {
59
+ id: string;
60
+ name?: string;
61
+ description?: string;
62
+ version?: string;
63
+ audience?: string;
64
+ locale?: string;
65
+ hash?: string;
66
+ };
67
+ /** What the pack holds. Only what the server gave is ever shown. */
68
+ summary?: {
69
+ products?: number;
70
+ collections?: number;
71
+ contentItems?: number;
72
+ pages?: number;
73
+ images?: number;
74
+ shipping?: boolean;
75
+ };
76
+ }
77
+ export interface DemoContentApplyInput {
78
+ template?: string;
79
+ dryRun?: boolean;
80
+ }
81
+ /** The calls the screens make, so a test or a preview can stand in for the server. */
82
+ export interface DemoContentClient {
83
+ status(): Promise<DemoContentStatus>;
84
+ available(): Promise<DemoContentAvailability>;
85
+ apply(input?: DemoContentApplyInput): Promise<{
86
+ jobId: string;
87
+ }>;
88
+ clear(options: {
89
+ keepMedia: boolean;
90
+ }): Promise<void>;
91
+ adopt(input: {
92
+ productIds?: string[];
93
+ collectionIds?: string[];
94
+ contentItemIds?: string[];
95
+ all?: boolean;
96
+ }): Promise<void>;
97
+ }
98
+ export declare function createDemoContentApi(tenantSlug: string): DemoContentClient;
99
+ /**
100
+ * The contract's error `code` (`job_in_progress`, `not_owner_or_admin`,
101
+ * `no_demo_content`, ...) when the server sent one. The body is
102
+ * `{ statusCode, code, message, details? }` and `ApiRequestError.details` holds it.
103
+ */
104
+ export declare function apiErrorCode(error: unknown): string | undefined;
@@ -0,0 +1,36 @@
1
+ import { apiFetch, ApiRequestError } from "./core";
2
+ const BASE = "/tenants/current/demo-content";
3
+ function createDemoContentApi(tenantSlug) {
4
+ return {
5
+ status: () => apiFetch(BASE, { tenantSlug }),
6
+ available: () => apiFetch(`${BASE}/available`, { tenantSlug }),
7
+ apply: (input) => apiFetch(BASE, {
8
+ method: "POST",
9
+ tenantSlug,
10
+ body: JSON.stringify(input ?? {})
11
+ }),
12
+ clear: async ({ keepMedia }) => {
13
+ await apiFetch(`${BASE}?keepMedia=${keepMedia ? "true" : "false"}`, {
14
+ method: "DELETE",
15
+ tenantSlug
16
+ });
17
+ },
18
+ adopt: async (input) => {
19
+ await apiFetch(`${BASE}/adopt`, {
20
+ method: "POST",
21
+ tenantSlug,
22
+ body: JSON.stringify(input)
23
+ });
24
+ }
25
+ };
26
+ }
27
+ function apiErrorCode(error) {
28
+ if (!(error instanceof ApiRequestError)) return void 0;
29
+ const code = error.details?.code;
30
+ return typeof code === "string" ? code : void 0;
31
+ }
32
+ export {
33
+ apiErrorCode,
34
+ createDemoContentApi
35
+ };
36
+ //# sourceMappingURL=demo-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/demo-content.ts"],"sourcesContent":["import { apiFetch, ApiRequestError } from './core';\n\n/**\n * Demo content: sample products, collections, pages and pictures a merchant\n * can start a store with, clear in one go, or apply again later.\n *\n * Contract: api-next `DEMO_CONTENT_API.md`. Every call is the current tenant's\n * and needs an owner or admin; anyone else is refused by the server, and the\n * screens hide the actions before that.\n */\nexport type DemoContentState =\n | 'none'\n | 'applying'\n | 'applied'\n | 'clearing'\n | 'failed';\n\nexport interface DemoContentPack {\n id: string;\n template: string;\n /** Absent until the pack has been read, as is `hash` (empty while the job starts). */\n version?: string;\n audience: string;\n hash: string;\n}\n\nexport interface DemoContentProgress {\n phase: string;\n done: number;\n total: number;\n}\n\nexport interface DemoContentCounts {\n products: number;\n collections: number;\n pages: number;\n media: number;\n}\n\nexport interface DemoContentStatus {\n state: DemoContentState;\n pack?: DemoContentPack;\n progress?: DemoContentProgress;\n counts?: DemoContentCounts;\n startedAt?: string;\n finishedAt?: string;\n /** Pages the server left alone because the merchant had already edited them. */\n skippedPages?: string[];\n /** Which job is running, or which one failed. */\n job?: 'apply' | 'clear';\n error?: { code: string; message: string };\n /** A clear that left uploaded pictures in the media library. */\n orphanedMedia?: { count: number; clearedAt?: string };\n}\n\n/** Whether the tenant's template has a pack this caller may apply. */\nexport interface DemoContentAvailability {\n available: boolean;\n template?: string;\n /** The current status state, so a screen can hide the offer once content is there. */\n state?: DemoContentState;\n /** Why there is nothing to offer: no_pack, audience_not_permitted, catalogue_unreachable, template_unknown. */\n reason?: string;\n pack?: {\n id: string;\n name?: string;\n description?: string;\n version?: string;\n audience?: string;\n locale?: string;\n hash?: string;\n };\n /** What the pack holds. Only what the server gave is ever shown. */\n summary?: {\n products?: number;\n collections?: number;\n contentItems?: number;\n pages?: number;\n images?: number;\n shipping?: boolean;\n };\n}\n\nexport interface DemoContentApplyInput {\n template?: string;\n dryRun?: boolean;\n}\n\n/** The calls the screens make, so a test or a preview can stand in for the server. */\nexport interface DemoContentClient {\n status(): Promise<DemoContentStatus>;\n available(): Promise<DemoContentAvailability>;\n apply(input?: DemoContentApplyInput): Promise<{ jobId: string }>;\n clear(options: { keepMedia: boolean }): Promise<void>;\n adopt(input: {\n productIds?: string[];\n collectionIds?: string[];\n contentItemIds?: string[];\n all?: boolean;\n }): Promise<void>;\n}\n\nconst BASE = '/tenants/current/demo-content';\n\nexport function createDemoContentApi(tenantSlug: string): DemoContentClient {\n return {\n status: () => apiFetch<DemoContentStatus>(BASE, { tenantSlug }),\n available: () =>\n apiFetch<DemoContentAvailability>(`${BASE}/available`, { tenantSlug }),\n apply: (input) =>\n apiFetch<{ jobId: string }>(BASE, {\n method: 'POST',\n tenantSlug,\n body: JSON.stringify(input ?? {}),\n }),\n clear: async ({ keepMedia }) => {\n await apiFetch<unknown>(`${BASE}?keepMedia=${keepMedia ? 'true' : 'false'}`, {\n method: 'DELETE',\n tenantSlug,\n });\n },\n adopt: async (input) => {\n await apiFetch<unknown>(`${BASE}/adopt`, {\n method: 'POST',\n tenantSlug,\n body: JSON.stringify(input),\n });\n },\n };\n}\n\n/**\n * The contract's error `code` (`job_in_progress`, `not_owner_or_admin`,\n * `no_demo_content`, ...) when the server sent one. The body is\n * `{ statusCode, code, message, details? }` and `ApiRequestError.details` holds it.\n */\nexport function apiErrorCode(error: unknown): string | undefined {\n if (!(error instanceof ApiRequestError)) return undefined;\n const code = (error.details as { code?: unknown } | undefined)?.code;\n return typeof code === 'string' ? code : undefined;\n}\n"],"mappings":"AAAA,SAAS,UAAU,uBAAuB;AAsG1C,MAAM,OAAO;AAEN,SAAS,qBAAqB,YAAuC;AAC1E,SAAO;AAAA,IACL,QAAQ,MAAM,SAA4B,MAAM,EAAE,WAAW,CAAC;AAAA,IAC9D,WAAW,MACT,SAAkC,GAAG,IAAI,cAAc,EAAE,WAAW,CAAC;AAAA,IACvE,OAAO,CAAC,UACN,SAA4B,MAAM;AAAA,MAChC,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,KAAK,UAAU,SAAS,CAAC,CAAC;AAAA,IAClC,CAAC;AAAA,IACH,OAAO,OAAO,EAAE,UAAU,MAAM;AAC9B,YAAM,SAAkB,GAAG,IAAI,cAAc,YAAY,SAAS,OAAO,IAAI;AAAA,QAC3E,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,OAAO,OAAO,UAAU;AACtB,YAAM,SAAkB,GAAG,IAAI,UAAU;AAAA,QACvC,QAAQ;AAAA,QACR;AAAA,QACA,MAAM,KAAK,UAAU,KAAK;AAAA,MAC5B,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAOO,SAAS,aAAa,OAAoC;AAC/D,MAAI,EAAE,iBAAiB,iBAAkB,QAAO;AAChD,QAAM,OAAQ,MAAM,SAA4C;AAChE,SAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;","names":[]}
@@ -0,0 +1,22 @@
1
+ import type { DemoContentAvailability, DemoContentClient, DemoContentStatus } from './api/demo-content';
2
+ /**
3
+ * A stand-in server for the demo-content screens: tests, and previewing the
4
+ * screens with no backend. It holds the state the real one would, and moves a
5
+ * job along each time the status is read.
6
+ */
7
+ export interface MockDemoOptions {
8
+ status?: DemoContentStatus;
9
+ availability?: DemoContentAvailability;
10
+ /** Reads before a started job finishes. */
11
+ stepsPerJob?: number;
12
+ /** Make the next calls to these fail with the server's message. */
13
+ failNext?: Partial<Record<keyof DemoContentClient, string>>;
14
+ }
15
+ export declare function createMockDemoClient(options?: MockDemoOptions): {
16
+ client: DemoContentClient;
17
+ calls: {
18
+ name: string;
19
+ args: unknown;
20
+ }[];
21
+ setStatus: (s: DemoContentStatus) => DemoContentStatus;
22
+ };
@@ -0,0 +1,70 @@
1
+ function createMockDemoClient(options = {}) {
2
+ let status = options.status ?? { state: "none" };
3
+ const stepsPerJob = options.stepsPerJob ?? 3;
4
+ const failNext = { ...options.failNext ?? {} };
5
+ let step = 0;
6
+ let nextJob = 1;
7
+ const calls = [];
8
+ const maybeFail = (name) => {
9
+ const message = failNext[name];
10
+ if (message) {
11
+ delete failNext[name];
12
+ throw new Error(message);
13
+ }
14
+ };
15
+ const client = {
16
+ async status() {
17
+ calls.push({ name: "status", args: void 0 });
18
+ maybeFail("status");
19
+ if (status.state === "applying" || status.state === "clearing") {
20
+ step += 1;
21
+ if (step >= stepsPerJob) {
22
+ status = status.state === "applying" ? {
23
+ state: "applied",
24
+ pack: status.pack,
25
+ counts: { products: 12, collections: 3, pages: 4, media: 20 },
26
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
27
+ } : { state: "none", finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
28
+ } else {
29
+ status = {
30
+ ...status,
31
+ progress: { phase: "products", done: step, total: stepsPerJob }
32
+ };
33
+ }
34
+ }
35
+ return status;
36
+ },
37
+ async available() {
38
+ calls.push({ name: "available", args: void 0 });
39
+ maybeFail("available");
40
+ return options.availability ?? { available: true };
41
+ },
42
+ async apply(input) {
43
+ calls.push({ name: "apply", args: input });
44
+ maybeFail("apply");
45
+ step = 0;
46
+ status = {
47
+ state: "applying",
48
+ pack: { id: "pack", template: "demo", version: "1", audience: "all", hash: "h" },
49
+ progress: { phase: "media", done: 0, total: stepsPerJob },
50
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
51
+ };
52
+ return { jobId: `job-${nextJob++}` };
53
+ },
54
+ async clear(args) {
55
+ calls.push({ name: "clear", args });
56
+ maybeFail("clear");
57
+ step = 0;
58
+ status = { ...status, state: "clearing", progress: { phase: "products", done: 0, total: stepsPerJob } };
59
+ },
60
+ async adopt(input) {
61
+ calls.push({ name: "adopt", args: input });
62
+ maybeFail("adopt");
63
+ }
64
+ };
65
+ return { client, calls, setStatus: (s) => status = s };
66
+ }
67
+ export {
68
+ createMockDemoClient
69
+ };
70
+ //# sourceMappingURL=demo-content-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/demo-content-mock.ts"],"sourcesContent":["import type {\n DemoContentAvailability,\n DemoContentClient,\n DemoContentStatus,\n} from './api/demo-content';\n\n/**\n * A stand-in server for the demo-content screens: tests, and previewing the\n * screens with no backend. It holds the state the real one would, and moves a\n * job along each time the status is read.\n */\nexport interface MockDemoOptions {\n status?: DemoContentStatus;\n availability?: DemoContentAvailability;\n /** Reads before a started job finishes. */\n stepsPerJob?: number;\n /** Make the next calls to these fail with the server's message. */\n failNext?: Partial<Record<keyof DemoContentClient, string>>;\n}\n\nexport function createMockDemoClient(options: MockDemoOptions = {}) {\n let status: DemoContentStatus = options.status ?? { state: 'none' };\n const stepsPerJob = options.stepsPerJob ?? 3;\n const failNext = { ...(options.failNext ?? {}) };\n let step = 0;\n let nextJob = 1;\n const calls: { name: string; args: unknown }[] = [];\n\n const maybeFail = (name: keyof DemoContentClient) => {\n const message = failNext[name];\n if (message) {\n delete failNext[name];\n throw new Error(message);\n }\n };\n\n const client: DemoContentClient = {\n async status() {\n calls.push({ name: 'status', args: undefined });\n maybeFail('status');\n if (status.state === 'applying' || status.state === 'clearing') {\n step += 1;\n if (step >= stepsPerJob) {\n status =\n status.state === 'applying'\n ? {\n state: 'applied',\n pack: status.pack,\n counts: { products: 12, collections: 3, pages: 4, media: 20 },\n finishedAt: new Date().toISOString(),\n }\n : { state: 'none', finishedAt: new Date().toISOString() };\n } else {\n status = {\n ...status,\n progress: { phase: 'products', done: step, total: stepsPerJob },\n };\n }\n }\n return status;\n },\n async available() {\n calls.push({ name: 'available', args: undefined });\n maybeFail('available');\n return options.availability ?? { available: true };\n },\n async apply(input) {\n calls.push({ name: 'apply', args: input });\n maybeFail('apply');\n step = 0;\n status = {\n state: 'applying',\n pack: { id: 'pack', template: 'demo', version: '1', audience: 'all', hash: 'h' },\n progress: { phase: 'media', done: 0, total: stepsPerJob },\n startedAt: new Date().toISOString(),\n };\n return { jobId: `job-${nextJob++}` };\n },\n async clear(args) {\n calls.push({ name: 'clear', args });\n maybeFail('clear');\n step = 0;\n status = { ...status, state: 'clearing', progress: { phase: 'products', done: 0, total: stepsPerJob } };\n },\n async adopt(input) {\n calls.push({ name: 'adopt', args: input });\n maybeFail('adopt');\n },\n };\n return { client, calls, setStatus: (s: DemoContentStatus) => (status = s) };\n}\n"],"mappings":"AAoBO,SAAS,qBAAqB,UAA2B,CAAC,GAAG;AAClE,MAAI,SAA4B,QAAQ,UAAU,EAAE,OAAO,OAAO;AAClE,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,WAAW,EAAE,GAAI,QAAQ,YAAY,CAAC,EAAG;AAC/C,MAAI,OAAO;AACX,MAAI,UAAU;AACd,QAAM,QAA2C,CAAC;AAElD,QAAM,YAAY,CAAC,SAAkC;AACnD,UAAM,UAAU,SAAS,IAAI;AAC7B,QAAI,SAAS;AACX,aAAO,SAAS,IAAI;AACpB,YAAM,IAAI,MAAM,OAAO;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,SAA4B;AAAA,IAChC,MAAM,SAAS;AACb,YAAM,KAAK,EAAE,MAAM,UAAU,MAAM,OAAU,CAAC;AAC9C,gBAAU,QAAQ;AAClB,UAAI,OAAO,UAAU,cAAc,OAAO,UAAU,YAAY;AAC9D,gBAAQ;AACR,YAAI,QAAQ,aAAa;AACvB,mBACE,OAAO,UAAU,aACb;AAAA,YACE,OAAO;AAAA,YACP,MAAM,OAAO;AAAA,YACb,QAAQ,EAAE,UAAU,IAAI,aAAa,GAAG,OAAO,GAAG,OAAO,GAAG;AAAA,YAC5D,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,UACrC,IACA,EAAE,OAAO,QAAQ,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE;AAAA,QAC9D,OAAO;AACL,mBAAS;AAAA,YACP,GAAG;AAAA,YACH,UAAU,EAAE,OAAO,YAAY,MAAM,MAAM,OAAO,YAAY;AAAA,UAChE;AAAA,QACF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,YAAY;AAChB,YAAM,KAAK,EAAE,MAAM,aAAa,MAAM,OAAU,CAAC;AACjD,gBAAU,WAAW;AACrB,aAAO,QAAQ,gBAAgB,EAAE,WAAW,KAAK;AAAA,IACnD;AAAA,IACA,MAAM,MAAM,OAAO;AACjB,YAAM,KAAK,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC;AACzC,gBAAU,OAAO;AACjB,aAAO;AACP,eAAS;AAAA,QACP,OAAO;AAAA,QACP,MAAM,EAAE,IAAI,QAAQ,UAAU,QAAQ,SAAS,KAAK,UAAU,OAAO,MAAM,IAAI;AAAA,QAC/E,UAAU,EAAE,OAAO,SAAS,MAAM,GAAG,OAAO,YAAY;AAAA,QACxD,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC;AACA,aAAO,EAAE,OAAO,OAAO,SAAS,GAAG;AAAA,IACrC;AAAA,IACA,MAAM,MAAM,MAAM;AAChB,YAAM,KAAK,EAAE,MAAM,SAAS,KAAK,CAAC;AAClC,gBAAU,OAAO;AACjB,aAAO;AACP,eAAS,EAAE,GAAG,QAAQ,OAAO,YAAY,UAAU,EAAE,OAAO,YAAY,MAAM,GAAG,OAAO,YAAY,EAAE;AAAA,IACxG;AAAA,IACA,MAAM,MAAM,OAAO;AACjB,YAAM,KAAK,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC;AACzC,gBAAU,OAAO;AAAA,IACnB;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,OAAO,WAAW,CAAC,MAA0B,SAAS,EAAG;AAC5E;","names":[]}
@@ -0,0 +1,33 @@
1
+ import type { DemoContentStatus } from './api/demo-content';
2
+ /**
3
+ * Keeps asking the server how a demo-content job is going until it stops.
4
+ *
5
+ * No React in it, and the clock and the tab are handed in, so a test moves time
6
+ * by hand. The rules:
7
+ *
8
+ * - Asks once straight away, which is also what picks up a job that was
9
+ * running when the page was closed and opened again.
10
+ * - Keeps asking only while the job is running (`applying` or `clearing`).
11
+ * - A hidden tab stops asking and asks once when it is shown again.
12
+ * - Requests never overlap; the next is timed from the end of the last.
13
+ * - A failed ask is not the end of the job: it backs off and tries again, and
14
+ * reports the error without losing the last good answer.
15
+ */
16
+ export interface DemoPollEnv {
17
+ isVisible: () => boolean;
18
+ onVisibilityChange: (listener: () => void) => () => void;
19
+ setTimer: (run: () => void, ms: number) => unknown;
20
+ clearTimer: (timer: unknown) => void;
21
+ }
22
+ export declare function browserDemoPollEnv(): DemoPollEnv;
23
+ export interface DemoPollHandlers {
24
+ read: () => Promise<DemoContentStatus>;
25
+ onStatus: (status: DemoContentStatus) => void;
26
+ onError: (error: unknown) => void;
27
+ }
28
+ export interface DemoPoller {
29
+ /** Ask now (after the merchant starts or clears), then keep going while busy. */
30
+ refresh: () => void;
31
+ stop: () => void;
32
+ }
33
+ export declare function startDemoPoller(handlers: DemoPollHandlers, env?: DemoPollEnv): DemoPoller;
@@ -0,0 +1,80 @@
1
+ import { isDemoContentBusy, nextPollDelay } from "./demo-content";
2
+ function browserDemoPollEnv() {
3
+ return {
4
+ isVisible: () => document.visibilityState !== "hidden",
5
+ onVisibilityChange: (listener) => {
6
+ document.addEventListener("visibilitychange", listener);
7
+ return () => document.removeEventListener("visibilitychange", listener);
8
+ },
9
+ setTimer: (run, ms) => window.setTimeout(run, ms),
10
+ clearTimer: (timer) => window.clearTimeout(timer)
11
+ };
12
+ }
13
+ function startDemoPoller(handlers, env = browserDemoPollEnv()) {
14
+ let timer = null;
15
+ let inFlight = false;
16
+ let stopped = false;
17
+ let attempt = 0;
18
+ let failures = 0;
19
+ let lastKey = "";
20
+ const clear = () => {
21
+ if (timer !== null) env.clearTimer(timer);
22
+ timer = null;
23
+ };
24
+ const schedule = (ms) => {
25
+ clear();
26
+ if (stopped) return;
27
+ timer = env.setTimer(() => {
28
+ timer = null;
29
+ void ask();
30
+ }, ms);
31
+ };
32
+ async function ask() {
33
+ if (stopped || inFlight) return;
34
+ inFlight = true;
35
+ try {
36
+ const status = await handlers.read();
37
+ if (stopped) return;
38
+ failures = 0;
39
+ const key = `${status.state}:${status.progress?.phase}:${status.progress?.done}`;
40
+ attempt = key === lastKey ? attempt + 1 : 0;
41
+ lastKey = key;
42
+ handlers.onStatus(status);
43
+ if (isDemoContentBusy(status)) schedule(nextPollDelay(attempt, 0));
44
+ } catch (error) {
45
+ if (stopped) return;
46
+ failures += 1;
47
+ handlers.onError(error);
48
+ schedule(nextPollDelay(attempt, failures));
49
+ } finally {
50
+ inFlight = false;
51
+ }
52
+ }
53
+ const stopWatching = env.onVisibilityChange(() => {
54
+ if (stopped) return;
55
+ if (!env.isVisible()) {
56
+ clear();
57
+ return;
58
+ }
59
+ clear();
60
+ void ask();
61
+ });
62
+ void ask();
63
+ return {
64
+ refresh: () => {
65
+ clear();
66
+ attempt = 0;
67
+ void ask();
68
+ },
69
+ stop: () => {
70
+ stopped = true;
71
+ clear();
72
+ stopWatching();
73
+ }
74
+ };
75
+ }
76
+ export {
77
+ browserDemoPollEnv,
78
+ startDemoPoller
79
+ };
80
+ //# sourceMappingURL=demo-content-poller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/demo-content-poller.ts"],"sourcesContent":["import type { DemoContentStatus } from './api/demo-content';\nimport { isDemoContentBusy, nextPollDelay } from './demo-content';\n\n/**\n * Keeps asking the server how a demo-content job is going until it stops.\n *\n * No React in it, and the clock and the tab are handed in, so a test moves time\n * by hand. The rules:\n *\n * - Asks once straight away, which is also what picks up a job that was\n * running when the page was closed and opened again.\n * - Keeps asking only while the job is running (`applying` or `clearing`).\n * - A hidden tab stops asking and asks once when it is shown again.\n * - Requests never overlap; the next is timed from the end of the last.\n * - A failed ask is not the end of the job: it backs off and tries again, and\n * reports the error without losing the last good answer.\n */\nexport interface DemoPollEnv {\n isVisible: () => boolean;\n onVisibilityChange: (listener: () => void) => () => void;\n setTimer: (run: () => void, ms: number) => unknown;\n clearTimer: (timer: unknown) => void;\n}\n\nexport function browserDemoPollEnv(): DemoPollEnv {\n return {\n isVisible: () => document.visibilityState !== 'hidden',\n onVisibilityChange: (listener) => {\n document.addEventListener('visibilitychange', listener);\n return () => document.removeEventListener('visibilitychange', listener);\n },\n setTimer: (run, ms) => window.setTimeout(run, ms),\n clearTimer: (timer) => window.clearTimeout(timer as number),\n };\n}\n\nexport interface DemoPollHandlers {\n read: () => Promise<DemoContentStatus>;\n onStatus: (status: DemoContentStatus) => void;\n onError: (error: unknown) => void;\n}\n\nexport interface DemoPoller {\n /** Ask now (after the merchant starts or clears), then keep going while busy. */\n refresh: () => void;\n stop: () => void;\n}\n\nexport function startDemoPoller(\n handlers: DemoPollHandlers,\n env: DemoPollEnv = browserDemoPollEnv(),\n): DemoPoller {\n let timer: unknown = null;\n let inFlight = false;\n let stopped = false;\n let attempt = 0;\n let failures = 0;\n let lastKey = '';\n\n const clear = () => {\n if (timer !== null) env.clearTimer(timer);\n timer = null;\n };\n\n const schedule = (ms: number) => {\n clear();\n if (stopped) return;\n timer = env.setTimer(() => {\n timer = null;\n void ask();\n }, ms);\n };\n\n async function ask() {\n if (stopped || inFlight) return;\n inFlight = true;\n try {\n const status = await handlers.read();\n if (stopped) return;\n failures = 0;\n const key = `${status.state}:${status.progress?.phase}:${status.progress?.done}`;\n attempt = key === lastKey ? attempt + 1 : 0;\n lastKey = key;\n handlers.onStatus(status);\n if (isDemoContentBusy(status)) schedule(nextPollDelay(attempt, 0));\n } catch (error) {\n if (stopped) return;\n failures += 1;\n handlers.onError(error);\n schedule(nextPollDelay(attempt, failures));\n } finally {\n inFlight = false;\n }\n }\n\n const stopWatching = env.onVisibilityChange(() => {\n if (stopped) return;\n if (!env.isVisible()) {\n clear();\n return;\n }\n // Shown again: ask once now. The answer decides whether to keep going.\n clear();\n void ask();\n });\n\n void ask();\n\n return {\n refresh: () => {\n clear();\n attempt = 0;\n void ask();\n },\n stop: () => {\n stopped = true;\n clear();\n stopWatching();\n },\n };\n}\n"],"mappings":"AACA,SAAS,mBAAmB,qBAAqB;AAuB1C,SAAS,qBAAkC;AAChD,SAAO;AAAA,IACL,WAAW,MAAM,SAAS,oBAAoB;AAAA,IAC9C,oBAAoB,CAAC,aAAa;AAChC,eAAS,iBAAiB,oBAAoB,QAAQ;AACtD,aAAO,MAAM,SAAS,oBAAoB,oBAAoB,QAAQ;AAAA,IACxE;AAAA,IACA,UAAU,CAAC,KAAK,OAAO,OAAO,WAAW,KAAK,EAAE;AAAA,IAChD,YAAY,CAAC,UAAU,OAAO,aAAa,KAAe;AAAA,EAC5D;AACF;AAcO,SAAS,gBACd,UACA,MAAmB,mBAAmB,GAC1B;AACZ,MAAI,QAAiB;AACrB,MAAI,WAAW;AACf,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,WAAW;AACf,MAAI,UAAU;AAEd,QAAM,QAAQ,MAAM;AAClB,QAAI,UAAU,KAAM,KAAI,WAAW,KAAK;AACxC,YAAQ;AAAA,EACV;AAEA,QAAM,WAAW,CAAC,OAAe;AAC/B,UAAM;AACN,QAAI,QAAS;AACb,YAAQ,IAAI,SAAS,MAAM;AACzB,cAAQ;AACR,WAAK,IAAI;AAAA,IACX,GAAG,EAAE;AAAA,EACP;AAEA,iBAAe,MAAM;AACnB,QAAI,WAAW,SAAU;AACzB,eAAW;AACX,QAAI;AACF,YAAM,SAAS,MAAM,SAAS,KAAK;AACnC,UAAI,QAAS;AACb,iBAAW;AACX,YAAM,MAAM,GAAG,OAAO,KAAK,IAAI,OAAO,UAAU,KAAK,IAAI,OAAO,UAAU,IAAI;AAC9E,gBAAU,QAAQ,UAAU,UAAU,IAAI;AAC1C,gBAAU;AACV,eAAS,SAAS,MAAM;AACxB,UAAI,kBAAkB,MAAM,EAAG,UAAS,cAAc,SAAS,CAAC,CAAC;AAAA,IACnE,SAAS,OAAO;AACd,UAAI,QAAS;AACb,kBAAY;AACZ,eAAS,QAAQ,KAAK;AACtB,eAAS,cAAc,SAAS,QAAQ,CAAC;AAAA,IAC3C,UAAE;AACA,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,eAAe,IAAI,mBAAmB,MAAM;AAChD,QAAI,QAAS;AACb,QAAI,CAAC,IAAI,UAAU,GAAG;AACpB,YAAM;AACN;AAAA,IACF;AAEA,UAAM;AACN,SAAK,IAAI;AAAA,EACX,CAAC;AAED,OAAK,IAAI;AAET,SAAO;AAAA,IACL,SAAS,MAAM;AACb,YAAM;AACN,gBAAU;AACV,WAAK,IAAI;AAAA,IACX;AAAA,IACA,MAAM,MAAM;AACV,gBAAU;AACV,YAAM;AACN,mBAAa;AAAA,IACf;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,43 @@
1
+ import type { DemoContentCounts, DemoContentStatus } from './api/demo-content';
2
+ /**
3
+ * What the demo-content screens decide, with no React and no network in it, so
4
+ * every rule is a plain function a test can call.
5
+ */
6
+ /**
7
+ * Who may start, clear or re-apply demo content: the owner, or a team member
8
+ * whose permissions include both managing products and managing settings (the
9
+ * admin-level role). Matches the server's rule; anyone else is refused there
10
+ * with `not_owner_or_admin`, so the screens hide the actions first.
11
+ */
12
+ export declare function canManageDemoContent(role: string | null | undefined, permissions?: Record<string, boolean> | null): boolean;
13
+ /** A job is running on the server; the page keeps asking until it stops. */
14
+ export declare function isDemoContentBusy(status: DemoContentStatus | null): boolean;
15
+ /**
16
+ * How long to wait before asking again.
17
+ *
18
+ * Starts quick so a short job feels live, slows as it runs so a long one does
19
+ * not hammer the server, and backs off further after each failed ask. `attempt`
20
+ * counts asks since the last change in the answer; `failures` counts failed
21
+ * asks in a row.
22
+ */
23
+ export declare const POLL_FIRST_MS = 1500;
24
+ export declare const POLL_MAX_MS = 10000;
25
+ export declare const POLL_ERROR_MAX_MS = 30000;
26
+ export declare function nextPollDelay(attempt: number, failures: number): number;
27
+ /** 0 to 100, or null when the server has not said how big the job is. */
28
+ export declare function progressPercent(status: DemoContentStatus | null): number | null;
29
+ /** The server's phase name as a short phrase; an unknown phase reads plainly. */
30
+ export declare function phaseLabel(phase: string | undefined, job?: 'apply' | 'clear'): string;
31
+ /** "12 products, 3 collections, 5 pages and 20 pictures" from the counts the server gave. */
32
+ export declare function describeCounts(counts: (Partial<DemoContentCounts> & {
33
+ images?: number;
34
+ }) | undefined): string | null;
35
+ /**
36
+ * A product (or collection) the demo pack added and the merchant has not kept.
37
+ * The server marks it with the tag `demo-pack` and `data.demoPack`; Keep this
38
+ * removes both.
39
+ */
40
+ export declare function isDemoProduct(product: {
41
+ tags?: string[] | null;
42
+ data?: Record<string, unknown> | null;
43
+ }): boolean;
@@ -0,0 +1,78 @@
1
+ function canManageDemoContent(role, permissions) {
2
+ if (role === "owner") return true;
3
+ return permissions?.canManageProducts === true && permissions?.canManageSettings === true;
4
+ }
5
+ function isDemoContentBusy(status) {
6
+ return status?.state === "applying" || status?.state === "clearing";
7
+ }
8
+ const POLL_FIRST_MS = 1500;
9
+ const POLL_MAX_MS = 1e4;
10
+ const POLL_ERROR_MAX_MS = 3e4;
11
+ function nextPollDelay(attempt, failures) {
12
+ if (failures > 0) {
13
+ return Math.min(POLL_ERROR_MAX_MS, POLL_MAX_MS * 2 ** (failures - 1));
14
+ }
15
+ return Math.min(POLL_MAX_MS, Math.round(POLL_FIRST_MS * 1.5 ** attempt));
16
+ }
17
+ function progressPercent(status) {
18
+ const p = status?.progress;
19
+ if (!p || !(p.total > 0)) return null;
20
+ return Math.max(0, Math.min(100, Math.round(p.done / p.total * 100)));
21
+ }
22
+ const APPLY_PHASE_LABEL = {
23
+ fetching: "Getting the demo ready",
24
+ media: "Adding pictures",
25
+ products: "Adding products",
26
+ collections: "Adding collections",
27
+ content: "Adding content",
28
+ tenant: "Setting up your store",
29
+ pages: "Setting up pages",
30
+ shipping: "Setting up delivery",
31
+ pruning: "Tidying up",
32
+ activating: "Making it visible"
33
+ };
34
+ const CLEAR_PHASE_LABEL = {
35
+ catalogue: "Removing demo products and collections",
36
+ pages: "Putting pages back",
37
+ shipping: "Removing demo delivery",
38
+ settings: "Restoring settings"
39
+ };
40
+ function phaseLabel(phase, job = "apply") {
41
+ if (!phase) return "Working";
42
+ const table = job === "clear" ? CLEAR_PHASE_LABEL : APPLY_PHASE_LABEL;
43
+ return table[phase] ?? "Working";
44
+ }
45
+ function plural(n, one, many) {
46
+ return `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
47
+ }
48
+ function describeCounts(counts) {
49
+ if (!counts) return null;
50
+ const parts = [];
51
+ if (counts.products) parts.push(plural(counts.products, "product", "products"));
52
+ if (counts.collections)
53
+ parts.push(plural(counts.collections, "collection", "collections"));
54
+ if (counts.pages) parts.push(plural(counts.pages, "page", "pages"));
55
+ const pictures = counts.media ?? counts.images;
56
+ if (pictures) parts.push(plural(pictures, "picture", "pictures"));
57
+ if (parts.length === 0) return null;
58
+ if (parts.length === 1) return parts[0];
59
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
60
+ }
61
+ function isDemoProduct(product) {
62
+ if (product.tags?.includes("demo-pack")) return true;
63
+ const d = product.data?.demoPack;
64
+ return typeof d === "object" && d !== null;
65
+ }
66
+ export {
67
+ POLL_ERROR_MAX_MS,
68
+ POLL_FIRST_MS,
69
+ POLL_MAX_MS,
70
+ canManageDemoContent,
71
+ describeCounts,
72
+ isDemoContentBusy,
73
+ isDemoProduct,
74
+ nextPollDelay,
75
+ phaseLabel,
76
+ progressPercent
77
+ };
78
+ //# sourceMappingURL=demo-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/demo-content.ts"],"sourcesContent":["import type {\n DemoContentCounts,\n DemoContentStatus,\n} from './api/demo-content';\n\n/**\n * What the demo-content screens decide, with no React and no network in it, so\n * every rule is a plain function a test can call.\n */\n\n/**\n * Who may start, clear or re-apply demo content: the owner, or a team member\n * whose permissions include both managing products and managing settings (the\n * admin-level role). Matches the server's rule; anyone else is refused there\n * with `not_owner_or_admin`, so the screens hide the actions first.\n */\nexport function canManageDemoContent(\n role: string | null | undefined,\n permissions?: Record<string, boolean> | null,\n): boolean {\n if (role === 'owner') return true;\n return permissions?.canManageProducts === true && permissions?.canManageSettings === true;\n}\n\n/** A job is running on the server; the page keeps asking until it stops. */\nexport function isDemoContentBusy(status: DemoContentStatus | null): boolean {\n return status?.state === 'applying' || status?.state === 'clearing';\n}\n\n/**\n * How long to wait before asking again.\n *\n * Starts quick so a short job feels live, slows as it runs so a long one does\n * not hammer the server, and backs off further after each failed ask. `attempt`\n * counts asks since the last change in the answer; `failures` counts failed\n * asks in a row.\n */\nexport const POLL_FIRST_MS = 1_500;\nexport const POLL_MAX_MS = 10_000;\nexport const POLL_ERROR_MAX_MS = 30_000;\n\nexport function nextPollDelay(attempt: number, failures: number): number {\n if (failures > 0) {\n return Math.min(POLL_ERROR_MAX_MS, POLL_MAX_MS * 2 ** (failures - 1));\n }\n return Math.min(POLL_MAX_MS, Math.round(POLL_FIRST_MS * 1.5 ** attempt));\n}\n\n/** 0 to 100, or null when the server has not said how big the job is. */\nexport function progressPercent(status: DemoContentStatus | null): number | null {\n const p = status?.progress;\n if (!p || !(p.total > 0)) return null;\n return Math.max(0, Math.min(100, Math.round((p.done / p.total) * 100)));\n}\n\n/** The server's phases, apply and clear, as short phrases. */\nconst APPLY_PHASE_LABEL: Record<string, string> = {\n fetching: 'Getting the demo ready',\n media: 'Adding pictures',\n products: 'Adding products',\n collections: 'Adding collections',\n content: 'Adding content',\n tenant: 'Setting up your store',\n pages: 'Setting up pages',\n shipping: 'Setting up delivery',\n pruning: 'Tidying up',\n activating: 'Making it visible',\n};\nconst CLEAR_PHASE_LABEL: Record<string, string> = {\n catalogue: 'Removing demo products and collections',\n pages: 'Putting pages back',\n shipping: 'Removing demo delivery',\n settings: 'Restoring settings',\n};\n\n/** The server's phase name as a short phrase; an unknown phase reads plainly. */\nexport function phaseLabel(\n phase: string | undefined,\n job: 'apply' | 'clear' = 'apply',\n): string {\n if (!phase) return 'Working';\n const table = job === 'clear' ? CLEAR_PHASE_LABEL : APPLY_PHASE_LABEL;\n return table[phase] ?? 'Working';\n}\n\nfunction plural(n: number, one: string, many: string): string {\n return `${n.toLocaleString('en-US')} ${n === 1 ? one : many}`;\n}\n\n/** \"12 products, 3 collections, 5 pages and 20 pictures\" from the counts the server gave. */\nexport function describeCounts(\n counts:\n | (Partial<DemoContentCounts> & { images?: number })\n | undefined,\n): string | null {\n if (!counts) return null;\n const parts: string[] = [];\n if (counts.products) parts.push(plural(counts.products, 'product', 'products'));\n if (counts.collections)\n parts.push(plural(counts.collections, 'collection', 'collections'));\n if (counts.pages) parts.push(plural(counts.pages, 'page', 'pages'));\n const pictures = counts.media ?? counts.images;\n if (pictures) parts.push(plural(pictures, 'picture', 'pictures'));\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;\n}\n\n/**\n * A product (or collection) the demo pack added and the merchant has not kept.\n * The server marks it with the tag `demo-pack` and `data.demoPack`; Keep this\n * removes both.\n */\nexport function isDemoProduct(product: {\n tags?: string[] | null;\n data?: Record<string, unknown> | null;\n}): boolean {\n if (product.tags?.includes('demo-pack')) return true;\n const d = product.data?.demoPack;\n return typeof d === 'object' && d !== null;\n}\n"],"mappings":"AAgBO,SAAS,qBACd,MACA,aACS;AACT,MAAI,SAAS,QAAS,QAAO;AAC7B,SAAO,aAAa,sBAAsB,QAAQ,aAAa,sBAAsB;AACvF;AAGO,SAAS,kBAAkB,QAA2C;AAC3E,SAAO,QAAQ,UAAU,cAAc,QAAQ,UAAU;AAC3D;AAUO,MAAM,gBAAgB;AACtB,MAAM,cAAc;AACpB,MAAM,oBAAoB;AAE1B,SAAS,cAAc,SAAiB,UAA0B;AACvE,MAAI,WAAW,GAAG;AAChB,WAAO,KAAK,IAAI,mBAAmB,cAAc,MAAM,WAAW,EAAE;AAAA,EACtE;AACA,SAAO,KAAK,IAAI,aAAa,KAAK,MAAM,gBAAgB,OAAO,OAAO,CAAC;AACzE;AAGO,SAAS,gBAAgB,QAAiD;AAC/E,QAAM,IAAI,QAAQ;AAClB,MAAI,CAAC,KAAK,EAAE,EAAE,QAAQ,GAAI,QAAO;AACjC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAO,EAAE,OAAO,EAAE,QAAS,GAAG,CAAC,CAAC;AACxE;AAGA,MAAM,oBAA4C;AAAA,EAChD,UAAU;AAAA,EACV,OAAO;AAAA,EACP,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,YAAY;AACd;AACA,MAAM,oBAA4C;AAAA,EAChD,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AACZ;AAGO,SAAS,WACd,OACA,MAAyB,SACjB;AACR,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,QAAQ,UAAU,oBAAoB;AACpD,SAAO,MAAM,KAAK,KAAK;AACzB;AAEA,SAAS,OAAO,GAAW,KAAa,MAAsB;AAC5D,SAAO,GAAG,EAAE,eAAe,OAAO,CAAC,IAAI,MAAM,IAAI,MAAM,IAAI;AAC7D;AAGO,SAAS,eACd,QAGe;AACf,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAkB,CAAC;AACzB,MAAI,OAAO,SAAU,OAAM,KAAK,OAAO,OAAO,UAAU,WAAW,UAAU,CAAC;AAC9E,MAAI,OAAO;AACT,UAAM,KAAK,OAAO,OAAO,aAAa,cAAc,aAAa,CAAC;AACpE,MAAI,OAAO,MAAO,OAAM,KAAK,OAAO,OAAO,OAAO,QAAQ,OAAO,CAAC;AAClE,QAAM,WAAW,OAAO,SAAS,OAAO;AACxC,MAAI,SAAU,OAAM,KAAK,OAAO,UAAU,WAAW,UAAU,CAAC;AAChE,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,MAAM,WAAW,EAAG,QAAO,MAAM,CAAC;AACtC,SAAO,GAAG,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,QAAQ,MAAM,MAAM,SAAS,CAAC,CAAC;AACxE;AAOO,SAAS,cAAc,SAGlB;AACV,MAAI,QAAQ,MAAM,SAAS,WAAW,EAAG,QAAO;AAChD,QAAM,IAAI,QAAQ,MAAM;AACxB,SAAO,OAAO,MAAM,YAAY,MAAM;AACxC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/lib",
3
- "version": "0.13.0-next.62",
3
+ "version": "0.13.0-next.63",
4
4
  "description": "Box dashboard foundation: API client, contexts, types, intent config, and utilities.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": false,
@@ -51,7 +51,7 @@
51
51
  "typescript": "^5"
52
52
  },
53
53
  "publishConfig": {
54
- "registry": "https://registry.npmjs.org",
54
+ "registry": "https://registry.npmjs.org/",
55
55
  "access": "restricted"
56
56
  }
57
57
  }