@getmicdrop/venue-calendar 4.2.34 → 4.2.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +809 -809
  2. package/dist/{ArrowLeftIcon-D2dcPIYB.js → ArrowLeftIcon-DS-NEije.js} +1 -1
  3. package/dist/{Badge-Bk1dnG89.js → Badge-DIHnRZlw.js} +1 -1
  4. package/dist/{BlockPlaceholder-DJLtnBDw.js → BlockPlaceholder-Dy9fStDr.js} +1 -1
  5. package/dist/{Calendar-CzvJjOKf.js → Calendar-CXwhGAmC.js} +6 -6
  6. package/dist/{CalendarFoundryView-DWjYFSEw.js → CalendarFoundryView-BbVin_52.js} +32 -31
  7. package/dist/{CalendarIcon-CkJFNFxp.js → CalendarIcon-CvZ-GUH_.js} +1 -1
  8. package/dist/{CartView-DidUQzO-.js → CartView-DT8DlMqQ.js} +19 -19
  9. package/dist/{Checkout-eIj6xGgL.js → Checkout-D3VqG09o.js} +15 -15
  10. package/dist/{Checkout-1NyqTUI8.js → Checkout-UU9C7CXV.js} +18 -18
  11. package/dist/{CheckoutTimer-DFIHUIZl.js → CheckoutTimer-2WEgppW1.js} +3 -3
  12. package/dist/{ChevronDownIcon-rsVUoX0r.js → ChevronDownIcon-DXbr-7H6.js} +1 -1
  13. package/dist/{ClockIcon-CIwbKXZX.js → ClockIcon-Dyx1LMo7.js} +1 -1
  14. package/dist/{CloseIcon-B7HoEcON.js → CloseIcon-NMHzwT75.js} +1 -1
  15. package/dist/{CollectionView-Dt3NmdBh.js → CollectionView-DyMB_bPY.js} +7 -7
  16. package/dist/{Event-B0hDsxnc.js → Event-CXCOnkmi.js} +22 -22
  17. package/dist/EventPage-HlhaVqRG.js +981 -0
  18. package/dist/{Heading-CQlZrwry.js → Heading-DcZtRBSB.js} +1 -1
  19. package/dist/{Image-T6dKIfNx.js → Image-BZGbFJLw.js} +1 -1
  20. package/dist/{Input-8KhoOdg-.js → Input-Cgdxdj6y.js} +1 -1
  21. package/dist/{MailIcon-CSR5vhKU.js → MailIcon-D34sBBQX.js} +1 -1
  22. package/dist/{Modal-D3Otve45.js → Modal-BvT64ak1.js} +2 -2
  23. package/dist/{ModalFooter-fv8mC75I.js → ModalFooter-DM1dyl4S.js} +2 -2
  24. package/dist/{PasswordInput-B_6cNrlw.js → PasswordInput-BNKrzXNm.js} +1 -1
  25. package/dist/{ScarcityBadge-CtHuWSRt.js → ScarcityBadge-DmbmsFlY.js} +5 -5
  26. package/dist/{SeriesPage-BFYmtgoi.js → SeriesPage-BXKz7PJV.js} +4 -4
  27. package/dist/{Spinner-fFAXvj_B.js → Spinner-gpYLD9h6.js} +1 -1
  28. package/dist/{Success-BIVQOUqu.js → Success-DieJd6sx.js} +18 -18
  29. package/dist/{TagIcon-Do5sN8xR.js → TagIcon-Q7BC2QeQ.js} +1 -1
  30. package/dist/{ViewTicketsEmbed-CbbE-8EX.js → ViewTicketsEmbed-DqwHARj0.js} +7 -7
  31. package/dist/{WarningCircleIcon-DuqytsVW.js → WarningCircleIcon-Dxi6_NlN.js} +1 -1
  32. package/dist/{WarningTriIcon-CW-j3Vwq.js → WarningTriIcon-BVNO82pk.js} +1 -1
  33. package/dist/{__SKIP_PATHS__-Chv7DW7h.js → __SKIP_PATHS__-BZ2V4IU4.js} +1 -1
  34. package/dist/api/api.cjs +1 -1
  35. package/dist/api/api.mjs +28 -34
  36. package/dist/api/types.d.ts +554 -554
  37. package/dist/api/waitlist.d.ts +6 -1
  38. package/dist/{colors-BND7shPA.js → colors-B3NG0RHG.js} +55 -61
  39. package/dist/{constants-BYtFptYp.js → constants-DqKf8bUn.js} +1 -1
  40. package/dist/{copyToClipboard-CcwaUGGt.js → copyToClipboard-CLo_2bfp.js} +1 -1
  41. package/dist/{de-CgHrjezX.js → de-tOW1Ez0s.js} +6 -0
  42. package/dist/{defaultLabels-CGMGBy-u.js → defaultLabels-D_9CneL4.js} +6 -0
  43. package/dist/{es-D6ThNLUc.js → es-DrYewQpd.js} +6 -0
  44. package/dist/{event-CbAGNxgd.js → event-CFDG8adb.js} +1 -1
  45. package/dist/{event-transform-DnvOiDVn.js → event-transform-jJPAKgYX.js} +4 -4
  46. package/dist/{eventStatusStream-BIn6Dzkw.js → eventStatusStream-wmEyQSgV.js} +1 -1
  47. package/dist/{fr-W0OOohju.js → fr-Bs9xnPu6.js} +6 -0
  48. package/dist/{href-D2-tmkMF.js → href-BfMcUXhM.js} +2 -2
  49. package/dist/{i18n-4k6uF1EX.js → i18n-BgkKrB4o.js} +5 -5
  50. package/dist/i18n-DK3npqzN.js +28 -0
  51. package/dist/{id-xgAFN1EH.js → id-BLwvLobz.js} +6 -0
  52. package/dist/{it-BhuT9BcE.js → it-CQ_Y8YK3.js} +6 -0
  53. package/dist/{ja-yca07ASz.js → ja-CeudRJqE.js} +6 -0
  54. package/dist/{ko-Pfftd2u0.js → ko-DFwVxd-1.js} +6 -0
  55. package/dist/{labels-Cs7xp4ug.js → labels-BHqxbbw9.js} +1 -1
  56. package/dist/locales/{4.2.34 → 4.2.36}/main/de.js +1 -1
  57. package/dist/locales/{4.2.34 → 4.2.36}/main/es.js +1 -1
  58. package/dist/locales/{4.2.34 → 4.2.36}/main/fr.js +1 -1
  59. package/dist/locales/{4.2.34 → 4.2.36}/main/id.js +1 -1
  60. package/dist/locales/{4.2.34 → 4.2.36}/main/it.js +1 -1
  61. package/dist/locales/{4.2.34 → 4.2.36}/main/ja.js +1 -1
  62. package/dist/locales/{4.2.34 → 4.2.36}/main/ko.js +1 -1
  63. package/dist/locales/{4.2.34 → 4.2.36}/main/nl.js +1 -1
  64. package/dist/locales/{4.2.34 → 4.2.36}/main/pl.js +1 -1
  65. package/dist/locales/{4.2.34 → 4.2.36}/main/pt-br.js +1 -1
  66. package/dist/locales/{4.2.34 → 4.2.36}/main/tr.js +1 -1
  67. package/dist/locales/{4.2.34 → 4.2.36}/main/zh.js +1 -1
  68. package/dist/{modalManager.svelte-vILq6Thx.js → modalManager.svelte-CrrDshW7.js} +2 -2
  69. package/dist/{nl-CQEhSA2l.js → nl-CYc0p85Y.js} +6 -0
  70. package/dist/{order-totals-CSDXY65z.js → order-totals-D0JKhd70.js} +3 -3
  71. package/dist/{orders-CH2nnS3m.js → orders-CCWPQsWb.js} +1 -1
  72. package/dist/{phoneUtils-BtJ3FhMK.js → phoneUtils-aQHGh8C_.js} +1 -1
  73. package/dist/{pixels-BK7PGnme.js → pixels-M2q8HRk_.js} +2 -2
  74. package/dist/{pl-CyWqKYE8.js → pl-BSqAasUa.js} +6 -0
  75. package/dist/{promo-Cy4LPBAC.js → promo-CKvS9lia.js} +1 -1
  76. package/dist/{pt-br-BC2I7f8-.js → pt-br-CZs8_gFN.js} +6 -0
  77. package/dist/seo/types.d.ts +156 -156
  78. package/dist/{serverTotals-C3VhWitQ.js → serverTotals-DGd3p1s5.js} +1 -1
  79. package/dist/{shareCopyFeedback.svelte-DTl5jJ2n.js → shareCopyFeedback.svelte-DTOJruVM.js} +4 -4
  80. package/dist/{showcase-state-options-CeCdn8_C.js → showcase-state-options-Da3jPUQb.js} +1 -1
  81. package/dist/{to-event-card-props-BmsDr8T3.js → to-event-card-props-B5bsYKP4.js} +3 -3
  82. package/dist/{tr-CNjaJc_1.js → tr-O80C2r5C.js} +6 -0
  83. package/dist/types/index.d.ts +509 -509
  84. package/dist/{utils-pF15CnBe.js → utils-RE2aEa_D.js} +2 -2
  85. package/dist/venue-calendar.es.js +30 -30
  86. package/dist/venue-calendar.iife.js +28 -28
  87. package/dist/venue-calendar.umd.js +49 -49
  88. package/dist/{zh-DOeb3_A1.js → zh-ydknixkJ.js} +6 -0
  89. package/package.json +3 -3
  90. package/src/lib/theme.js +222 -222
  91. package/dist/EventPage-De-7chl3.js +0 -970
  92. package/dist/i18n-FnFHQ5QU.js +0 -28
  93. /package/dist/locales/{4.2.34 → 4.2.36}/flow/de.js +0 -0
  94. /package/dist/locales/{4.2.34 → 4.2.36}/flow/es.js +0 -0
  95. /package/dist/locales/{4.2.34 → 4.2.36}/flow/fr.js +0 -0
  96. /package/dist/locales/{4.2.34 → 4.2.36}/flow/id.js +0 -0
  97. /package/dist/locales/{4.2.34 → 4.2.36}/flow/it.js +0 -0
  98. /package/dist/locales/{4.2.34 → 4.2.36}/flow/ja.js +0 -0
  99. /package/dist/locales/{4.2.34 → 4.2.36}/flow/ko.js +0 -0
  100. /package/dist/locales/{4.2.34 → 4.2.36}/flow/nl.js +0 -0
  101. /package/dist/locales/{4.2.34 → 4.2.36}/flow/pl.js +0 -0
  102. /package/dist/locales/{4.2.34 → 4.2.36}/flow/pt-br.js +0 -0
  103. /package/dist/locales/{4.2.34 → 4.2.36}/flow/tr.js +0 -0
  104. /package/dist/locales/{4.2.34 → 4.2.36}/flow/zh.js +0 -0
@@ -1,554 +1,554 @@
1
- /**
2
- * API Types for MicDrop Public Checkout API
3
- *
4
- * These types define the shape of API requests and responses
5
- * for the public checkout flow (no authentication required).
6
- */
7
-
8
- import type { EventId, SeriesId } from '@getmicdrop/svelte-components';
9
-
10
- // ============================================================================
11
- // Payment & Orders
12
- // ============================================================================
13
-
14
- export interface PaymentIntentRequest {
15
- IP?: string;
16
- productQuantities: Record<string | number, number>;
17
- }
18
-
19
- export interface PaymentIntentResponse {
20
- client_secret: string;
21
- // The residual the buyer is charged via Stripe, in int cents (net of any
22
- // applied gift card). This is the field the checkout Total keys off. The
23
- // orders-service response has NO `amount` key — an earlier `amount` here was
24
- // never populated, so Total rendered $0.00 on every paid order.
25
- stripe_amount?: number;
26
- amount_total: number;
27
- subtotal?: number;
28
- tax?: number;
29
- tax_amount_exclusive?: number;
30
- service_fee?: number;
31
- // The buyer's SECOND fee rail (MIC-1760) — card processing, grossed up and
32
- // passed through at cost, in int cents. orders-service reports it separately
33
- // from `service_fee` and folds it into `stripe_amount`. The PUBLIC receipt
34
- // merges the two into one "Service fees" row (Gus, 2026-08-16), which is also
35
- // what the pre-server estimate (`estimateBuyerFee`) has always shown; leaving
36
- // it unread made the priced total exceed the rows on screen by this amount.
37
- card_processing?: number;
38
- // The venue's OWN buyer-paid fee (the third rail), in int cents: charged to
39
- // the buyer and routed to the venue, not the platform. Rides the same merged
40
- // "Service fees" row on the public receipt; absent/0 when the venue
41
- // configures none.
42
- venue_kickback?: number;
43
- discount?: number;
44
- gift_card_amount?: number;
45
- gift_card_code?: string;
46
- stripe_publishable_key?: string;
47
- // ISO-4217 currency (Stripe returns it lowercase). Feeds the checkout totals.
48
- currency?: string;
49
- // The three fields the $0 path answers with. orders-service finalizes a free
50
- // order in-band on this same endpoint — it generates the tickets, marks the
51
- // cart completed and sends the confirmation — and reports that back here
52
- // instead of on a client secret. Views/Checkout.svelte reads all three to
53
- // decide whether the cart really is still free and really did finalize, so a
54
- // cart that gained a chargeable amount between selection and submit is caught
55
- // rather than waved through.
56
- //
57
- // They were undeclared, which is not a cosmetic gap: this interface has no
58
- // index signature, so every one of those three reads was a hard svelte-check
59
- // error against the shape the server actually sends.
60
- /** False when the cart still needs a Stripe charge — i.e. it is NOT free. */
61
- requires_stripe?: boolean;
62
- /** 'free' on the in-band $0 finalize path. */
63
- payment_type?: string;
64
- /** False when the backend declined to finalize the free order. */
65
- order_finalized?: boolean;
66
- }
67
-
68
- export interface CompleteReservationResponse {
69
- success: boolean;
70
- message?: string;
71
- error?: string;
72
- }
73
-
74
- export interface CancelReservationResponse {
75
- success: boolean;
76
- message?: string;
77
- error?: string;
78
- }
79
-
80
- export interface CreateOrderRequest {
81
- eventId: string | number;
82
- promoCode?: string;
83
- }
84
-
85
- export interface CreateOrderResponse {
86
- uuid: string;
87
- }
88
-
89
- export interface ValidatePaymentRequest {
90
- id: string;
91
- paymentIntentId: string;
92
- tickets: Record<string | number, number>;
93
- firstName: string;
94
- lastName: string;
95
- email: string;
96
- /** Buyer phone collected at checkout (MIC-1705). Persisted on the order. */
97
- phone?: string;
98
- /** Answer to the venue's custom checkout question (MIC-1705). */
99
- questionAnswer?: string;
100
- paymentMethod: string;
101
- mailingList?: boolean;
102
- saleType?: string;
103
- attendees?: AttendeeInfo[];
104
- }
105
-
106
- export interface AttendeeInfo {
107
- ticketId: string | number;
108
- firstName: string;
109
- lastName: string;
110
- email: string;
111
- }
112
-
113
- export interface ValidatePaymentResponse {
114
- success: boolean;
115
- status: string;
116
- orderUUID?: string;
117
- error?: string;
118
- /**
119
- * Fail-closed FLOOR fields. When the event is canceled/postponed between
120
- * cart-creation and finalize, the backend gate refuses to capture the charge
121
- * and sets one of these (alongside `success: false`). Either flag means the
122
- * buyer was NOT charged; the checkout raises the charge-aware
123
- * EventUnavailableModal instead of navigating to success. Optional because
124
- * they are only present on the rejected-finalize path.
125
- */
126
- eventCanceled?: boolean;
127
- eventPostponed?: boolean;
128
- }
129
-
130
- // ============================================================================
131
- // Session Management
132
- // ============================================================================
133
-
134
- export interface ExtendSessionRequest {
135
- orderUuid: string;
136
- }
137
-
138
- export interface ExtendSessionResponse {
139
- success: boolean;
140
- newExpiryTime?: string;
141
- remainingExtensions?: number;
142
- error?: string;
143
- /**
144
- * HTTP status code from the backend on failure. Populated on non-OK
145
- * responses so callers can distinguish "session already expired"
146
- * (typically 410 Gone) from "max extensions reached" or generic 5xx.
147
- * Undefined on network errors and on success.
148
- */
149
- statusCode?: number;
150
- }
151
-
152
- export interface SessionStatus {
153
- expiresAt?: string;
154
- extensionCount?: number;
155
- remainingExtensions?: number;
156
- canExtend?: boolean;
157
- reservationCount?: number;
158
- error?: string;
159
- // 404 from /orders/session/{uuid}: the cart's reservations are
160
- // gone (expired or never existed). Callers polling on an interval
161
- // MUST treat this as terminal and stop — see CartView.svelte.
162
- notFound?: boolean;
163
- // Live event status the session heartbeat used to piggyback so the public
164
- // checkout could flip the buy button on cancel/postpone. As of MIC-1408 the
165
- // checkout rides a dedicated per-event SSE stream (subscribeEventStatus)
166
- // instead, flipping instantly rather than within ~30s — this field is no
167
- // longer consumed there. One of the auth-service Event statuses
168
- // (draft|live|started|ended|canceled|postponed). Absent if the lookup failed.
169
- eventStatus?: string;
170
- }
171
-
172
- // ============================================================================
173
- // Promo Codes
174
- // ============================================================================
175
-
176
- export interface PromoValidationResponse {
177
- valid: boolean;
178
- revealHiddenTickets?: boolean;
179
- revealTicketIds?: number[];
180
- provideDiscount?: boolean;
181
- discountType?: 'percentage' | 'fixed';
182
- amount?: number;
183
- code?: string;
184
- /**
185
- * The code's ticket scope, exactly as the server sends it — `false` here plus
186
- * a non-empty `applyToTicketIds` means the code covers only those types.
187
- * Dropped by validatePromoCode until MIC-2575, which is why a code scoped to
188
- * one type discounted every type. Read through promoTicketIdsFromValidation,
189
- * never field-by-field at a call site.
190
- */
191
- appliesToAllTickets?: boolean;
192
- applyToTicketIds?: number[];
193
- error?: string;
194
- }
195
-
196
- /**
197
- * RAW body of GET /api/v2/public/promo-codes/validate/{eventId}/{code}.
198
- *
199
- * The live endpoint reports the discount magnitude as `discount` and the code
200
- * as `name` — NOT `amount`/`code`. validatePromoCode normalises this into the
201
- * client-facing PromoValidationResponse (amount/code) so callers read one shape.
202
- * The legacy `amount`/`code` keys stay optional here so an older payload (or a
203
- * mocked one) still maps through the same fallback. Verified 2026-07-11 against
204
- * two live promos (seeded GUARD20PCT + pre-existing EARLY20).
205
- */
206
- export interface RawPromoValidationBody {
207
- valid?: boolean;
208
- revealHiddenTickets?: boolean;
209
- revealTicketIds?: number[];
210
- provideDiscount?: boolean;
211
- discountType?: 'percentage' | 'fixed';
212
- /** Discount magnitude — real server field. */
213
- discount?: number;
214
- /** Legacy/mocked magnitude field — fallback only. */
215
- amount?: number;
216
- /** Promo code — real server field. */
217
- name?: string;
218
- /** Legacy/mocked code field — fallback only. */
219
- code?: string;
220
- appliesToAllTickets?: boolean;
221
- applyToTicketIds?: number[];
222
- isGlobal?: boolean;
223
- ticketLimit?: number;
224
- error?: string;
225
- }
226
-
227
- export interface HasPromoCodesResponse {
228
- hasPromoCodes: boolean;
229
- }
230
-
231
- // ============================================================================
232
- // Orders
233
- // ============================================================================
234
-
235
- export interface Order {
236
- uuid: string;
237
- id?: number;
238
- customerEmail: string;
239
- customerFirstName?: string;
240
- customerLastName?: string;
241
- status: string;
242
- totalAmount: number;
243
- subtotal?: number;
244
- serviceFeesAmount: number;
245
- taxAmount: number;
246
- discount?: number;
247
- paymentIntentId?: string;
248
- paymentMethod?: string;
249
- purchasedTickets: PurchasedTicket[];
250
- createdAt?: string;
251
- updatedAt?: string;
252
- }
253
-
254
- export interface PurchasedTicket {
255
- uuid: string;
256
- id?: number;
257
- ticketNumber?: string;
258
- orderId?: string | number;
259
- attendeeFirstName?: string;
260
- attendeeLastName?: string;
261
- attendeeEmail?: string;
262
- ticketName: string;
263
- ticketTypeId?: number;
264
- purchasePrice: number;
265
- status?: string;
266
- checkedIn?: boolean;
267
- checkedInAt?: string;
268
- }
269
-
270
- // ============================================================================
271
- // Events
272
- // ============================================================================
273
-
274
- export interface Event {
275
- eventID: number;
276
- id?: number;
277
- name: string;
278
- title?: string;
279
- slug?: string;
280
- description?: string;
281
- date: string;
282
- startDateTime?: string;
283
- endDateTime?: string;
284
- doorsOpenTime?: string;
285
- timezone?: string;
286
- venueId?: number;
287
- venueName?: string;
288
- venueAddress?: string;
289
- location?: string;
290
- imageUrl?: string;
291
- imageURL?: string;
292
- status?: string;
293
- isPublished?: boolean;
294
- isCancelled?: boolean;
295
- availableTickets?: AvailableTicket[];
296
- ticketsAvailable?: number;
297
- ticketsSold?: number;
298
- minPrice?: number;
299
- maxPrice?: number;
300
- ctaText?: string;
301
- ctaState?: 'available' | 'sold_out' | 'coming_soon' | 'ended';
302
- showPerformers?: boolean;
303
- eventSeriesId?: number;
304
- seriesInstanceNumber?: number;
305
- // ── Passthrough seams (events convergence) ─────────────────────────────
306
- // Carried verbatim from the wire by parseEvent so view-shape projections
307
- // (utils/event-transform.js, public-calendar-flow/transform.ts) compose the
308
- // canonical instead of reading wire fields directly. The object-valued
309
- // fields are intentionally raw (see composition note in
310
- // api/transformers/event.ts) — their consumers own the wire variance.
311
- timeZone?: string;
312
- /**
313
- * The operator's sales dial — effective on public payloads (MIC-2455).
314
- * `computeCtaState` refuses on `paused` / `ended` / `sold_out` and leaves
315
- * `not_started` to the sale-window logic.
316
- */
317
- salesStatus?: string;
318
- eventSummary?: string;
319
- password?: string;
320
- hasPassword?: boolean;
321
- disclaimer?: string;
322
- ticketType?: number;
323
- eventTicketingType?: number;
324
- ageRestriction?: number;
325
- displayAgeRestriction?: boolean;
326
- displayStartTime?: boolean;
327
- displayEndTime?: boolean;
328
- displayDoorsTime?: boolean;
329
- /** Operator toggle: show remaining-ticket counts publicly. Absent = show. */
330
- displayTicketsLeft?: boolean;
331
- /** Operator's post-purchase note, rendered on the confirmation card. */
332
- postSalesMessage?: string;
333
- collectionId?: number;
334
- ticketsRemaining?: number;
335
- ticketsTotal?: number;
336
- hasHiddenTickets?: boolean;
337
- stage?: { name?: string } | null;
338
- stageName?: string;
339
- // Widget-filter dimensions on the public month feeds (saved-widget engine):
340
- // stage/category/performer/collection data the embed adapts onto the SC
341
- // FilterableEvent view (lib/widget-filters.js).
342
- stageId?: number;
343
- eventCategoryTypes?: number[] | null;
344
- publicPerformers?: Array<{
345
- id?: number;
346
- displayName?: string;
347
- image?: string;
348
- }> | null;
349
- collectionIds?: number[] | null;
350
- venue?: Record<string, any>;
351
- performers?: Record<string, any>[];
352
- faqs?: Record<string, any>[];
353
- showtimes?: Record<string, any>[];
354
- purchasedTickets?: Record<string, any>[];
355
- }
356
-
357
- export interface AvailableTicket {
358
- id: number;
359
- name: string;
360
- description?: string;
361
- price: number;
362
- quantity: number;
363
- quantitySold?: number;
364
- quantityAvailable?: number;
365
- minPerOrder?: number;
366
- maxPerOrder?: number;
367
- saleStartDate?: string;
368
- saleEndDate?: string;
369
- isHidden?: boolean;
370
- revealWithPromoCode?: boolean;
371
- ticketType?: number; // 0 = GA, 1 = assigned
372
- sectionId?: number;
373
- sortOrder?: number;
374
- }
375
-
376
- export interface EventPerformersResponse {
377
- performers: Performer[];
378
- showPerformers: boolean;
379
- }
380
-
381
- export interface Performer {
382
- id: number;
383
- displayName: string;
384
- avatar?: string;
385
- order?: number;
386
- }
387
-
388
- // ============================================================================
389
- // Venues
390
- // ============================================================================
391
-
392
- export interface Venue {
393
- id: number;
394
- name: string;
395
- slug?: string;
396
- address?: string;
397
- googleLocationNameCache?: string;
398
- city?: string;
399
- state?: string;
400
- zipCode?: string;
401
- country?: string;
402
- timezone?: string;
403
- logoUrl?: string;
404
- serviceFeePercentage?: number;
405
- serviceFeeCents?: number;
406
- /** True = the venue absorbs the service fee; false/absent = the buyer pays it. */
407
- absorbServiceFee?: boolean;
408
- taxPercentage?: number;
409
- organizationId?: number;
410
- }
411
-
412
- // ============================================================================
413
- // Series
414
- // ============================================================================
415
-
416
- export interface SeriesOccurrence {
417
- eventId: EventId;
418
- date: string;
419
- startDateTime: string;
420
- endDateTime?: string;
421
- instanceNumber: number;
422
- status: string;
423
- ticketsAvailable?: number;
424
- ctaState?: string;
425
- }
426
-
427
- export interface SeriesOccurrencesResponse {
428
- seriesId: SeriesId;
429
- occurrences: SeriesOccurrence[];
430
- }
431
-
432
- // ============================================================================
433
- // Series Page
434
- // ============================================================================
435
-
436
- export interface SeriesPageData {
437
- id: number;
438
- title: string;
439
- description: string;
440
- eventSummary?: string;
441
- image?: string;
442
- timeZone: string;
443
- venue: {
444
- id: number;
445
- name: string;
446
- location?: string;
447
- googleLocationNameCache?: string;
448
- faq?: Array<{ question: string; answer: string }> | string;
449
- disclaimer?: string;
450
- };
451
- performers: Array<{
452
- displayName: string;
453
- firstName?: string;
454
- lastName?: string;
455
- profileImage?: string;
456
- }>;
457
- occurrences: Array<{
458
- id: number;
459
- slug: string;
460
- startDateTime: string;
461
- endDateTime: string;
462
- ctaState: {
463
- text: string;
464
- disabled: boolean;
465
- reason?: string;
466
- };
467
- /**
468
- * The occurrence's OWN location. The backend emits this ONLY when the
469
- * occurrence has been moved off the series master venue/stage, so
470
- * presence === "this showtime is not where the rest of the series is".
471
- * Absent for every ordinary occurrence.
472
- */
473
- venue?: {
474
- id: number;
475
- name: string;
476
- address?: string;
477
- city?: string;
478
- state?: string;
479
- googleLocationNameCache?: string;
480
- stageName?: string;
481
- };
482
- }>;
483
- }
484
-
485
- // ============================================================================
486
- // Entity resolution (type-agnostic public id → concrete entity)
487
- // ============================================================================
488
-
489
- /**
490
- * Response shape of `/api/v2/public/resolve/{id}`. The embed deep-link is
491
- * type-agnostic (`#{id}-{slug}`), so the backend resolves which kind of entity
492
- * the id points at; `data` is the corresponding event / series / collection
493
- * payload (same shape the dedicated fetchers return for that type).
494
- */
495
- export interface ResolvedEntity {
496
- type: 'event' | 'series' | 'collection';
497
- id: string | number;
498
- title?: string;
499
- data: any;
500
- }
501
-
502
- // ============================================================================
503
- // Public Collection
504
- // ============================================================================
505
-
506
- export interface PublicCollectionData {
507
- id: number;
508
- collectionTitle: string;
509
- summary?: string;
510
- description?: string;
511
- coverImage?: string;
512
- events: Array<{
513
- id: number;
514
- title: string;
515
- slug?: string;
516
- startDateTime?: string;
517
- endDateTime?: string;
518
- image?: string;
519
- status?: string;
520
- venue?: { name: string };
521
- minPrice?: number;
522
- }>;
523
- // Password-gate variant: a protected collection responds with a gate signal
524
- // instead of the collection body (either `passwordRequired: true` or an
525
- // `error.code === 'PASSWORD_REQUIRED'`). Modelled here as optional wire
526
- // fields so the view can read them without an `as any` escape.
527
- passwordRequired?: boolean;
528
- error?: { code?: string };
529
- }
530
-
531
- // ============================================================================
532
- // API Configuration
533
- // ============================================================================
534
-
535
- export interface ApiConfig {
536
- baseUrl?: string;
537
- timeout?: number;
538
- /** Number of additional attempts after the first try (default: 2 = 3 total tries). */
539
- retries?: number;
540
- /** Base delay before the first retry; doubles each attempt (default: 500ms). */
541
- retryDelay?: number;
542
- onError?: (_error: Error) => void;
543
- }
544
-
545
- // ============================================================================
546
- // Generic API Response
547
- // ============================================================================
548
-
549
- export interface ApiResponse<T> {
550
- success: boolean;
551
- data?: T;
552
- error?: string;
553
- statusCode?: number;
554
- }
1
+ /**
2
+ * API Types for MicDrop Public Checkout API
3
+ *
4
+ * These types define the shape of API requests and responses
5
+ * for the public checkout flow (no authentication required).
6
+ */
7
+
8
+ import type { EventId, SeriesId } from '@getmicdrop/svelte-components';
9
+
10
+ // ============================================================================
11
+ // Payment & Orders
12
+ // ============================================================================
13
+
14
+ export interface PaymentIntentRequest {
15
+ IP?: string;
16
+ productQuantities: Record<string | number, number>;
17
+ }
18
+
19
+ export interface PaymentIntentResponse {
20
+ client_secret: string;
21
+ // The residual the buyer is charged via Stripe, in int cents (net of any
22
+ // applied gift card). This is the field the checkout Total keys off. The
23
+ // orders-service response has NO `amount` key — an earlier `amount` here was
24
+ // never populated, so Total rendered $0.00 on every paid order.
25
+ stripe_amount?: number;
26
+ amount_total: number;
27
+ subtotal?: number;
28
+ tax?: number;
29
+ tax_amount_exclusive?: number;
30
+ service_fee?: number;
31
+ // The buyer's SECOND fee rail (MIC-1760) — card processing, grossed up and
32
+ // passed through at cost, in int cents. orders-service reports it separately
33
+ // from `service_fee` and folds it into `stripe_amount`. The PUBLIC receipt
34
+ // merges the two into one "Service fees" row (Gus, 2026-08-16), which is also
35
+ // what the pre-server estimate (`estimateBuyerFee`) has always shown; leaving
36
+ // it unread made the priced total exceed the rows on screen by this amount.
37
+ card_processing?: number;
38
+ // The venue's OWN buyer-paid fee (the third rail), in int cents: charged to
39
+ // the buyer and routed to the venue, not the platform. Rides the same merged
40
+ // "Service fees" row on the public receipt; absent/0 when the venue
41
+ // configures none.
42
+ venue_kickback?: number;
43
+ discount?: number;
44
+ gift_card_amount?: number;
45
+ gift_card_code?: string;
46
+ stripe_publishable_key?: string;
47
+ // ISO-4217 currency (Stripe returns it lowercase). Feeds the checkout totals.
48
+ currency?: string;
49
+ // The three fields the $0 path answers with. orders-service finalizes a free
50
+ // order in-band on this same endpoint — it generates the tickets, marks the
51
+ // cart completed and sends the confirmation — and reports that back here
52
+ // instead of on a client secret. Views/Checkout.svelte reads all three to
53
+ // decide whether the cart really is still free and really did finalize, so a
54
+ // cart that gained a chargeable amount between selection and submit is caught
55
+ // rather than waved through.
56
+ //
57
+ // They were undeclared, which is not a cosmetic gap: this interface has no
58
+ // index signature, so every one of those three reads was a hard svelte-check
59
+ // error against the shape the server actually sends.
60
+ /** False when the cart still needs a Stripe charge — i.e. it is NOT free. */
61
+ requires_stripe?: boolean;
62
+ /** 'free' on the in-band $0 finalize path. */
63
+ payment_type?: string;
64
+ /** False when the backend declined to finalize the free order. */
65
+ order_finalized?: boolean;
66
+ }
67
+
68
+ export interface CompleteReservationResponse {
69
+ success: boolean;
70
+ message?: string;
71
+ error?: string;
72
+ }
73
+
74
+ export interface CancelReservationResponse {
75
+ success: boolean;
76
+ message?: string;
77
+ error?: string;
78
+ }
79
+
80
+ export interface CreateOrderRequest {
81
+ eventId: string | number;
82
+ promoCode?: string;
83
+ }
84
+
85
+ export interface CreateOrderResponse {
86
+ uuid: string;
87
+ }
88
+
89
+ export interface ValidatePaymentRequest {
90
+ id: string;
91
+ paymentIntentId: string;
92
+ tickets: Record<string | number, number>;
93
+ firstName: string;
94
+ lastName: string;
95
+ email: string;
96
+ /** Buyer phone collected at checkout (MIC-1705). Persisted on the order. */
97
+ phone?: string;
98
+ /** Answer to the venue's custom checkout question (MIC-1705). */
99
+ questionAnswer?: string;
100
+ paymentMethod: string;
101
+ mailingList?: boolean;
102
+ saleType?: string;
103
+ attendees?: AttendeeInfo[];
104
+ }
105
+
106
+ export interface AttendeeInfo {
107
+ ticketId: string | number;
108
+ firstName: string;
109
+ lastName: string;
110
+ email: string;
111
+ }
112
+
113
+ export interface ValidatePaymentResponse {
114
+ success: boolean;
115
+ status: string;
116
+ orderUUID?: string;
117
+ error?: string;
118
+ /**
119
+ * Fail-closed FLOOR fields. When the event is canceled/postponed between
120
+ * cart-creation and finalize, the backend gate refuses to capture the charge
121
+ * and sets one of these (alongside `success: false`). Either flag means the
122
+ * buyer was NOT charged; the checkout raises the charge-aware
123
+ * EventUnavailableModal instead of navigating to success. Optional because
124
+ * they are only present on the rejected-finalize path.
125
+ */
126
+ eventCanceled?: boolean;
127
+ eventPostponed?: boolean;
128
+ }
129
+
130
+ // ============================================================================
131
+ // Session Management
132
+ // ============================================================================
133
+
134
+ export interface ExtendSessionRequest {
135
+ orderUuid: string;
136
+ }
137
+
138
+ export interface ExtendSessionResponse {
139
+ success: boolean;
140
+ newExpiryTime?: string;
141
+ remainingExtensions?: number;
142
+ error?: string;
143
+ /**
144
+ * HTTP status code from the backend on failure. Populated on non-OK
145
+ * responses so callers can distinguish "session already expired"
146
+ * (typically 410 Gone) from "max extensions reached" or generic 5xx.
147
+ * Undefined on network errors and on success.
148
+ */
149
+ statusCode?: number;
150
+ }
151
+
152
+ export interface SessionStatus {
153
+ expiresAt?: string;
154
+ extensionCount?: number;
155
+ remainingExtensions?: number;
156
+ canExtend?: boolean;
157
+ reservationCount?: number;
158
+ error?: string;
159
+ // 404 from /orders/session/{uuid}: the cart's reservations are
160
+ // gone (expired or never existed). Callers polling on an interval
161
+ // MUST treat this as terminal and stop — see CartView.svelte.
162
+ notFound?: boolean;
163
+ // Live event status the session heartbeat used to piggyback so the public
164
+ // checkout could flip the buy button on cancel/postpone. As of MIC-1408 the
165
+ // checkout rides a dedicated per-event SSE stream (subscribeEventStatus)
166
+ // instead, flipping instantly rather than within ~30s — this field is no
167
+ // longer consumed there. One of the auth-service Event statuses
168
+ // (draft|live|started|ended|canceled|postponed). Absent if the lookup failed.
169
+ eventStatus?: string;
170
+ }
171
+
172
+ // ============================================================================
173
+ // Promo Codes
174
+ // ============================================================================
175
+
176
+ export interface PromoValidationResponse {
177
+ valid: boolean;
178
+ revealHiddenTickets?: boolean;
179
+ revealTicketIds?: number[];
180
+ provideDiscount?: boolean;
181
+ discountType?: 'percentage' | 'fixed';
182
+ amount?: number;
183
+ code?: string;
184
+ /**
185
+ * The code's ticket scope, exactly as the server sends it — `false` here plus
186
+ * a non-empty `applyToTicketIds` means the code covers only those types.
187
+ * Dropped by validatePromoCode until MIC-2575, which is why a code scoped to
188
+ * one type discounted every type. Read through promoTicketIdsFromValidation,
189
+ * never field-by-field at a call site.
190
+ */
191
+ appliesToAllTickets?: boolean;
192
+ applyToTicketIds?: number[];
193
+ error?: string;
194
+ }
195
+
196
+ /**
197
+ * RAW body of GET /api/v2/public/promo-codes/validate/{eventId}/{code}.
198
+ *
199
+ * The live endpoint reports the discount magnitude as `discount` and the code
200
+ * as `name` — NOT `amount`/`code`. validatePromoCode normalises this into the
201
+ * client-facing PromoValidationResponse (amount/code) so callers read one shape.
202
+ * The legacy `amount`/`code` keys stay optional here so an older payload (or a
203
+ * mocked one) still maps through the same fallback. Verified 2026-07-11 against
204
+ * two live promos (seeded GUARD20PCT + pre-existing EARLY20).
205
+ */
206
+ export interface RawPromoValidationBody {
207
+ valid?: boolean;
208
+ revealHiddenTickets?: boolean;
209
+ revealTicketIds?: number[];
210
+ provideDiscount?: boolean;
211
+ discountType?: 'percentage' | 'fixed';
212
+ /** Discount magnitude — real server field. */
213
+ discount?: number;
214
+ /** Legacy/mocked magnitude field — fallback only. */
215
+ amount?: number;
216
+ /** Promo code — real server field. */
217
+ name?: string;
218
+ /** Legacy/mocked code field — fallback only. */
219
+ code?: string;
220
+ appliesToAllTickets?: boolean;
221
+ applyToTicketIds?: number[];
222
+ isGlobal?: boolean;
223
+ ticketLimit?: number;
224
+ error?: string;
225
+ }
226
+
227
+ export interface HasPromoCodesResponse {
228
+ hasPromoCodes: boolean;
229
+ }
230
+
231
+ // ============================================================================
232
+ // Orders
233
+ // ============================================================================
234
+
235
+ export interface Order {
236
+ uuid: string;
237
+ id?: number;
238
+ customerEmail: string;
239
+ customerFirstName?: string;
240
+ customerLastName?: string;
241
+ status: string;
242
+ totalAmount: number;
243
+ subtotal?: number;
244
+ serviceFeesAmount: number;
245
+ taxAmount: number;
246
+ discount?: number;
247
+ paymentIntentId?: string;
248
+ paymentMethod?: string;
249
+ purchasedTickets: PurchasedTicket[];
250
+ createdAt?: string;
251
+ updatedAt?: string;
252
+ }
253
+
254
+ export interface PurchasedTicket {
255
+ uuid: string;
256
+ id?: number;
257
+ ticketNumber?: string;
258
+ orderId?: string | number;
259
+ attendeeFirstName?: string;
260
+ attendeeLastName?: string;
261
+ attendeeEmail?: string;
262
+ ticketName: string;
263
+ ticketTypeId?: number;
264
+ purchasePrice: number;
265
+ status?: string;
266
+ checkedIn?: boolean;
267
+ checkedInAt?: string;
268
+ }
269
+
270
+ // ============================================================================
271
+ // Events
272
+ // ============================================================================
273
+
274
+ export interface Event {
275
+ eventID: number;
276
+ id?: number;
277
+ name: string;
278
+ title?: string;
279
+ slug?: string;
280
+ description?: string;
281
+ date: string;
282
+ startDateTime?: string;
283
+ endDateTime?: string;
284
+ doorsOpenTime?: string;
285
+ timezone?: string;
286
+ venueId?: number;
287
+ venueName?: string;
288
+ venueAddress?: string;
289
+ location?: string;
290
+ imageUrl?: string;
291
+ imageURL?: string;
292
+ status?: string;
293
+ isPublished?: boolean;
294
+ isCancelled?: boolean;
295
+ availableTickets?: AvailableTicket[];
296
+ ticketsAvailable?: number;
297
+ ticketsSold?: number;
298
+ minPrice?: number;
299
+ maxPrice?: number;
300
+ ctaText?: string;
301
+ ctaState?: 'available' | 'sold_out' | 'coming_soon' | 'ended';
302
+ showPerformers?: boolean;
303
+ eventSeriesId?: number;
304
+ seriesInstanceNumber?: number;
305
+ // ── Passthrough seams (events convergence) ─────────────────────────────
306
+ // Carried verbatim from the wire by parseEvent so view-shape projections
307
+ // (utils/event-transform.js, public-calendar-flow/transform.ts) compose the
308
+ // canonical instead of reading wire fields directly. The object-valued
309
+ // fields are intentionally raw (see composition note in
310
+ // api/transformers/event.ts) — their consumers own the wire variance.
311
+ timeZone?: string;
312
+ /**
313
+ * The operator's sales dial — effective on public payloads (MIC-2455).
314
+ * `computeCtaState` refuses on `paused` / `ended` / `sold_out` and leaves
315
+ * `not_started` to the sale-window logic.
316
+ */
317
+ salesStatus?: string;
318
+ eventSummary?: string;
319
+ password?: string;
320
+ hasPassword?: boolean;
321
+ disclaimer?: string;
322
+ ticketType?: number;
323
+ eventTicketingType?: number;
324
+ ageRestriction?: number;
325
+ displayAgeRestriction?: boolean;
326
+ displayStartTime?: boolean;
327
+ displayEndTime?: boolean;
328
+ displayDoorsTime?: boolean;
329
+ /** Operator toggle: show remaining-ticket counts publicly. Absent = show. */
330
+ displayTicketsLeft?: boolean;
331
+ /** Operator's post-purchase note, rendered on the confirmation card. */
332
+ postSalesMessage?: string;
333
+ collectionId?: number;
334
+ ticketsRemaining?: number;
335
+ ticketsTotal?: number;
336
+ hasHiddenTickets?: boolean;
337
+ stage?: { name?: string } | null;
338
+ stageName?: string;
339
+ // Widget-filter dimensions on the public month feeds (saved-widget engine):
340
+ // stage/category/performer/collection data the embed adapts onto the SC
341
+ // FilterableEvent view (lib/widget-filters.js).
342
+ stageId?: number;
343
+ eventCategoryTypes?: number[] | null;
344
+ publicPerformers?: Array<{
345
+ id?: number;
346
+ displayName?: string;
347
+ image?: string;
348
+ }> | null;
349
+ collectionIds?: number[] | null;
350
+ venue?: Record<string, any>;
351
+ performers?: Record<string, any>[];
352
+ faqs?: Record<string, any>[];
353
+ showtimes?: Record<string, any>[];
354
+ purchasedTickets?: Record<string, any>[];
355
+ }
356
+
357
+ export interface AvailableTicket {
358
+ id: number;
359
+ name: string;
360
+ description?: string;
361
+ price: number;
362
+ quantity: number;
363
+ quantitySold?: number;
364
+ quantityAvailable?: number;
365
+ minPerOrder?: number;
366
+ maxPerOrder?: number;
367
+ saleStartDate?: string;
368
+ saleEndDate?: string;
369
+ isHidden?: boolean;
370
+ revealWithPromoCode?: boolean;
371
+ ticketType?: number; // 0 = GA, 1 = assigned
372
+ sectionId?: number;
373
+ sortOrder?: number;
374
+ }
375
+
376
+ export interface EventPerformersResponse {
377
+ performers: Performer[];
378
+ showPerformers: boolean;
379
+ }
380
+
381
+ export interface Performer {
382
+ id: number;
383
+ displayName: string;
384
+ avatar?: string;
385
+ order?: number;
386
+ }
387
+
388
+ // ============================================================================
389
+ // Venues
390
+ // ============================================================================
391
+
392
+ export interface Venue {
393
+ id: number;
394
+ name: string;
395
+ slug?: string;
396
+ address?: string;
397
+ googleLocationNameCache?: string;
398
+ city?: string;
399
+ state?: string;
400
+ zipCode?: string;
401
+ country?: string;
402
+ timezone?: string;
403
+ logoUrl?: string;
404
+ serviceFeePercentage?: number;
405
+ serviceFeeCents?: number;
406
+ /** True = the venue absorbs the service fee; false/absent = the buyer pays it. */
407
+ absorbServiceFee?: boolean;
408
+ taxPercentage?: number;
409
+ organizationId?: number;
410
+ }
411
+
412
+ // ============================================================================
413
+ // Series
414
+ // ============================================================================
415
+
416
+ export interface SeriesOccurrence {
417
+ eventId: EventId;
418
+ date: string;
419
+ startDateTime: string;
420
+ endDateTime?: string;
421
+ instanceNumber: number;
422
+ status: string;
423
+ ticketsAvailable?: number;
424
+ ctaState?: string;
425
+ }
426
+
427
+ export interface SeriesOccurrencesResponse {
428
+ seriesId: SeriesId;
429
+ occurrences: SeriesOccurrence[];
430
+ }
431
+
432
+ // ============================================================================
433
+ // Series Page
434
+ // ============================================================================
435
+
436
+ export interface SeriesPageData {
437
+ id: number;
438
+ title: string;
439
+ description: string;
440
+ eventSummary?: string;
441
+ image?: string;
442
+ timeZone: string;
443
+ venue: {
444
+ id: number;
445
+ name: string;
446
+ location?: string;
447
+ googleLocationNameCache?: string;
448
+ faq?: Array<{ question: string; answer: string }> | string;
449
+ disclaimer?: string;
450
+ };
451
+ performers: Array<{
452
+ displayName: string;
453
+ firstName?: string;
454
+ lastName?: string;
455
+ profileImage?: string;
456
+ }>;
457
+ occurrences: Array<{
458
+ id: number;
459
+ slug: string;
460
+ startDateTime: string;
461
+ endDateTime: string;
462
+ ctaState: {
463
+ text: string;
464
+ disabled: boolean;
465
+ reason?: string;
466
+ };
467
+ /**
468
+ * The occurrence's OWN location. The backend emits this ONLY when the
469
+ * occurrence has been moved off the series master venue/stage, so
470
+ * presence === "this showtime is not where the rest of the series is".
471
+ * Absent for every ordinary occurrence.
472
+ */
473
+ venue?: {
474
+ id: number;
475
+ name: string;
476
+ address?: string;
477
+ city?: string;
478
+ state?: string;
479
+ googleLocationNameCache?: string;
480
+ stageName?: string;
481
+ };
482
+ }>;
483
+ }
484
+
485
+ // ============================================================================
486
+ // Entity resolution (type-agnostic public id → concrete entity)
487
+ // ============================================================================
488
+
489
+ /**
490
+ * Response shape of `/api/v2/public/resolve/{id}`. The embed deep-link is
491
+ * type-agnostic (`#{id}-{slug}`), so the backend resolves which kind of entity
492
+ * the id points at; `data` is the corresponding event / series / collection
493
+ * payload (same shape the dedicated fetchers return for that type).
494
+ */
495
+ export interface ResolvedEntity {
496
+ type: 'event' | 'series' | 'collection';
497
+ id: string | number;
498
+ title?: string;
499
+ data: any;
500
+ }
501
+
502
+ // ============================================================================
503
+ // Public Collection
504
+ // ============================================================================
505
+
506
+ export interface PublicCollectionData {
507
+ id: number;
508
+ collectionTitle: string;
509
+ summary?: string;
510
+ description?: string;
511
+ coverImage?: string;
512
+ events: Array<{
513
+ id: number;
514
+ title: string;
515
+ slug?: string;
516
+ startDateTime?: string;
517
+ endDateTime?: string;
518
+ image?: string;
519
+ status?: string;
520
+ venue?: { name: string };
521
+ minPrice?: number;
522
+ }>;
523
+ // Password-gate variant: a protected collection responds with a gate signal
524
+ // instead of the collection body (either `passwordRequired: true` or an
525
+ // `error.code === 'PASSWORD_REQUIRED'`). Modelled here as optional wire
526
+ // fields so the view can read them without an `as any` escape.
527
+ passwordRequired?: boolean;
528
+ error?: { code?: string };
529
+ }
530
+
531
+ // ============================================================================
532
+ // API Configuration
533
+ // ============================================================================
534
+
535
+ export interface ApiConfig {
536
+ baseUrl?: string;
537
+ timeout?: number;
538
+ /** Number of additional attempts after the first try (default: 2 = 3 total tries). */
539
+ retries?: number;
540
+ /** Base delay before the first retry; doubles each attempt (default: 500ms). */
541
+ retryDelay?: number;
542
+ onError?: (_error: Error) => void;
543
+ }
544
+
545
+ // ============================================================================
546
+ // Generic API Response
547
+ // ============================================================================
548
+
549
+ export interface ApiResponse<T> {
550
+ success: boolean;
551
+ data?: T;
552
+ error?: string;
553
+ statusCode?: number;
554
+ }