import { EventId, SeriesId } from '@getmicdrop/svelte-components'; export interface PaymentIntentRequest { IP?: string; productQuantities: Record; } export interface PaymentIntentResponse { client_secret: string; stripe_amount?: number; amount_total: number; subtotal?: number; tax?: number; tax_amount_exclusive?: number; service_fee?: number; card_processing?: number; venue_kickback?: number; discount?: number; gift_card_amount?: number; gift_card_code?: string; stripe_publishable_key?: string; currency?: string; /** False when the cart still needs a Stripe charge — i.e. it is NOT free. */ requires_stripe?: boolean; /** 'free' on the in-band $0 finalize path. */ payment_type?: string; /** False when the backend declined to finalize the free order. */ order_finalized?: boolean; } export interface CompleteReservationResponse { success: boolean; message?: string; error?: string; } export interface CancelReservationResponse { success: boolean; message?: string; error?: string; } export interface CreateOrderRequest { eventId: string | number; promoCode?: string; } export interface CreateOrderResponse { uuid: string; } export interface ValidatePaymentRequest { id: string; paymentIntentId: string; tickets: Record; firstName: string; lastName: string; email: string; /** Buyer phone collected at checkout (MIC-1705). Persisted on the order. */ phone?: string; /** Answer to the venue's custom checkout question (MIC-1705). */ questionAnswer?: string; paymentMethod: string; mailingList?: boolean; saleType?: string; attendees?: AttendeeInfo[]; } export interface AttendeeInfo { ticketId: string | number; firstName: string; lastName: string; email: string; } export interface ValidatePaymentResponse { success: boolean; status: string; orderUUID?: string; error?: string; /** * Fail-closed FLOOR fields. When the event is canceled/postponed between * cart-creation and finalize, the backend gate refuses to capture the charge * and sets one of these (alongside `success: false`). Either flag means the * buyer was NOT charged; the checkout raises the charge-aware * EventUnavailableModal instead of navigating to success. Optional because * they are only present on the rejected-finalize path. */ eventCanceled?: boolean; eventPostponed?: boolean; } export interface ExtendSessionRequest { orderUuid: string; } export interface ExtendSessionResponse { success: boolean; newExpiryTime?: string; remainingExtensions?: number; error?: string; /** * HTTP status code from the backend on failure. Populated on non-OK * responses so callers can distinguish "session already expired" * (typically 410 Gone) from "max extensions reached" or generic 5xx. * Undefined on network errors and on success. */ statusCode?: number; } export interface SessionStatus { expiresAt?: string; extensionCount?: number; remainingExtensions?: number; canExtend?: boolean; reservationCount?: number; error?: string; notFound?: boolean; eventStatus?: string; } export interface PromoValidationResponse { valid: boolean; revealHiddenTickets?: boolean; revealTicketIds?: number[]; provideDiscount?: boolean; discountType?: 'percentage' | 'fixed'; amount?: number; code?: string; /** * The code's ticket scope, exactly as the server sends it — `false` here plus * a non-empty `applyToTicketIds` means the code covers only those types. * Dropped by validatePromoCode until MIC-2575, which is why a code scoped to * one type discounted every type. Read through promoTicketIdsFromValidation, * never field-by-field at a call site. */ appliesToAllTickets?: boolean; applyToTicketIds?: number[]; error?: string; } /** * RAW body of GET /api/v2/public/promo-codes/validate/{eventId}/{code}. * * The live endpoint reports the discount magnitude as `discount` and the code * as `name` — NOT `amount`/`code`. validatePromoCode normalises this into the * client-facing PromoValidationResponse (amount/code) so callers read one shape. * The legacy `amount`/`code` keys stay optional here so an older payload (or a * mocked one) still maps through the same fallback. Verified 2026-07-11 against * two live promos (seeded GUARD20PCT + pre-existing EARLY20). */ export interface RawPromoValidationBody { valid?: boolean; revealHiddenTickets?: boolean; revealTicketIds?: number[]; provideDiscount?: boolean; discountType?: 'percentage' | 'fixed'; /** Discount magnitude — real server field. */ discount?: number; /** Legacy/mocked magnitude field — fallback only. */ amount?: number; /** Promo code — real server field. */ name?: string; /** Legacy/mocked code field — fallback only. */ code?: string; appliesToAllTickets?: boolean; applyToTicketIds?: number[]; isGlobal?: boolean; ticketLimit?: number; error?: string; } export interface HasPromoCodesResponse { hasPromoCodes: boolean; } export interface Order { uuid: string; id?: number; customerEmail: string; customerFirstName?: string; customerLastName?: string; status: string; totalAmount: number; subtotal?: number; serviceFeesAmount: number; taxAmount: number; discount?: number; paymentIntentId?: string; paymentMethod?: string; /** What the buyer's card statement reads for this order; '' when none. */ statementDescriptor?: string; purchasedTickets: PurchasedTicket[]; createdAt?: string; updatedAt?: string; } export interface PurchasedTicket { uuid: string; id?: number; ticketNumber?: string; orderId?: string | number; attendeeFirstName?: string; attendeeLastName?: string; attendeeEmail?: string; ticketName: string; ticketTypeId?: number; purchasePrice: number; status?: string; checkedIn?: boolean; checkedInAt?: string; } export interface Event { eventID: number; id?: number; name: string; title?: string; slug?: string; description?: string; date: string; startDateTime?: string; endDateTime?: string; doorsOpenTime?: string; timezone?: string; venueId?: number; venueName?: string; venueAddress?: string; location?: string; imageUrl?: string; imageURL?: string; status?: string; isPublished?: boolean; isCancelled?: boolean; availableTickets?: AvailableTicket[]; ticketsAvailable?: number; ticketsSold?: number; minPrice?: number; maxPrice?: number; ctaText?: string; ctaState?: 'available' | 'sold_out' | 'coming_soon' | 'ended'; showPerformers?: boolean; eventSeriesId?: number; seriesInstanceNumber?: number; timeZone?: string; /** * The operator's sales dial — effective on public payloads (MIC-2455). * `computeCtaState` refuses on `paused` / `ended` / `sold_out` and leaves * `not_started` to the sale-window logic. */ salesStatus?: string; eventSummary?: string; password?: string; hasPassword?: boolean; disclaimer?: string; ticketType?: number; eventTicketingType?: number; ageRestriction?: number; displayAgeRestriction?: boolean; displayStartTime?: boolean; displayEndTime?: boolean; displayDoorsTime?: boolean; /** Operator toggle: show remaining-ticket counts publicly. Absent = show. */ displayTicketsLeft?: boolean; /** Operator's post-purchase note, rendered on the confirmation card. */ postSalesMessage?: string; /** The venue's message to attendees, written when it canceled or postponed. */ canceledReason?: string; postponedReason?: string; collectionId?: number; ticketsRemaining?: number; ticketsTotal?: number; hasHiddenTickets?: boolean; stage?: { name?: string; } | null; stageName?: string; stageId?: number; eventCategoryTypes?: number[] | null; publicPerformers?: Array<{ id?: number; displayName?: string; image?: string; }> | null; collectionIds?: number[] | null; venue?: Record; performers?: Record[]; faqs?: Record[]; showtimes?: Record[]; purchasedTickets?: Record[]; } export interface AvailableTicket { id: number; name: string; description?: string; price: number; quantity: number; quantitySold?: number; quantityAvailable?: number; minPerOrder?: number; maxPerOrder?: number; saleStartDate?: string; saleEndDate?: string; isHidden?: boolean; revealWithPromoCode?: boolean; ticketType?: number; sectionId?: number; sortOrder?: number; } export interface EventPerformersResponse { performers: Performer[]; showPerformers: boolean; } export interface Performer { id: number; displayName: string; avatar?: string; order?: number; } export interface Venue { id: number; name: string; slug?: string; address?: string; googleLocationNameCache?: string; city?: string; state?: string; zipCode?: string; country?: string; timezone?: string; logoUrl?: string; serviceFeePercentage?: number; serviceFeeCents?: number; /** True = the venue absorbs the service fee; false/absent = the buyer pays it. */ absorbServiceFee?: boolean; taxPercentage?: number; organizationId?: number; } export interface SeriesOccurrence { eventId: EventId; date: string; startDateTime: string; endDateTime?: string; instanceNumber: number; status: string; ticketsAvailable?: number; ctaState?: string; } export interface SeriesOccurrencesResponse { seriesId: SeriesId; occurrences: SeriesOccurrence[]; } export interface SeriesPageData { /** * The series id, as `seriesId` (MIC-3838). * * THIS FIELD USED TO BE DECLARED `id`, AND THE WIRE HAS NEVER SENT `id`. * Both doors that answer this shape — `GET /api/v2/public/series/:id/page` * and `GET /api/v2/public/resolve/:slugId` when the id names a series — build * SeriesPageResponseDTO, whose first field carries the json tag `seriesId`. * The payload is cast rather than parsed, so TypeScript never caught the * mismatch and `parseSeries` read `undefined` forever. */ seriesId: number; /** * @deprecated Never present on the wire. Read only so a hand-built fixture or * an older mock that spells it `id` still resolves; see `parseSeries`. */ id?: number; title: string; description: string; eventSummary?: string; image?: string; timeZone: string; venue: { id: number; name: string; location?: string; googleLocationNameCache?: string; faq?: Array<{ question: string; answer: string; }> | string; disclaimer?: string; }; performers: Array<{ displayName: string; firstName?: string; lastName?: string; profileImage?: string; }>; occurrences: Array<{ id: number; slug: string; startDateTime: string; endDateTime: string; ctaState: { text: string; disabled: boolean; reason?: string; }; /** * The occurrence's OWN location. The backend emits this ONLY when the * occurrence has been moved off the series master venue/stage, so * presence === "this showtime is not where the rest of the series is". * Absent for every ordinary occurrence. */ venue?: { id: number; name: string; address?: string; city?: string; state?: string; googleLocationNameCache?: string; stageName?: string; }; }>; } /** * Response shape of `/api/v2/public/resolve/{id}`. The embed deep-link is * type-agnostic (`#{id}-{slug}`), so the backend resolves which kind of entity * the id points at; `data` is the corresponding event / series / collection * payload (same shape the dedicated fetchers return for that type). */ export interface ResolvedEntity { type: 'event' | 'series' | 'collection'; id: string | number; title?: string; data: any; } export interface PublicCollectionData { id: number; collectionTitle: string; summary?: string; description?: string; coverImage?: string; events: Array<{ id: number; title: string; slug?: string; startDateTime?: string; endDateTime?: string; image?: string; status?: string; timeZone?: string; timezone?: string; venue?: { name: string; timeZone?: string; timezone?: string; }; minPrice?: number; }>; passwordRequired?: boolean; error?: { code?: string; }; visibility?: string; } export interface ApiConfig { baseUrl?: string; timeout?: number; /** Number of additional attempts after the first try (default: 2 = 3 total tries). */ retries?: number; /** Base delay before the first retry; doubles each attempt (default: 500ms). */ retryDelay?: number; onError?: (_error: Error) => void; } export interface ApiResponse { success: boolean; data?: T; error?: string; statusCode?: number; }