@wallfar/ocd-studio-core-sdk
Version:
Helper SDK for our OneClick Studio modules
485 lines (481 loc) • 17.9 kB
text/typescript
import { ShallowRef, Ref, ComputedRef } from 'vue';
import { CalendarDate } from '@internationalized/date';
import { Firestore } from 'firebase-admin/firestore';
import { FirestoreEvent, Change, DocumentSnapshot } from 'firebase-functions/v2/firestore';
interface BookerServerConfig {
defaultCurrency?: string;
defaultLocale?: string;
provider: 'firebase';
firebase?: {
db: Firestore;
agendas: string;
reserved_spots: string;
processed_events: string;
orders: string;
};
paymentProvider?: 'stripe';
stripe?: {
key: string;
webhook_secret: string;
/**
* Optional discriminator stored in Stripe metadata so shared Stripe webhooks
* can route booking payments to the right handler.
*/
webhookType?: string;
/**
* Additional webhook_type values this BookerServer should accept when
* handling Stripe webhooks. The default webhookType is accepted automatically.
*/
allowedWebhookTypes?: string[];
/**
* Metadata key used for webhookType. Defaults to "webhook_type".
*/
webhookTypeMetadataKey?: string;
};
}
interface ReservationValidationInput {
id?: string;
agendaId: string;
date: string;
timeslot: Timeslot;
resourceId?: string;
spots: number;
addOns?: {
[addOnId: string]: any;
};
pricingOptionId?: string;
adjustments?: BookingAdjustment[];
metadata?: BookingMetadata;
}
interface ValidatedReservation extends ReservationValidationInput {
status: 'needs_approval' | 'approved' | 'rejected' | 'cancelled';
basePrice: number;
addOnsPrice: number;
totalPrice: number;
resource?: AgendaResource;
pricingOption?: AppointmentPricingOption;
resolvedAddOns: AppointmentAddOn[];
}
type BookingValidationErrorCode = 'validation_error' | 'reservation_required' | 'agenda_not_found' | 'invalid_date_format' | 'minimum_advance_notice' | 'booking_window_exceeded' | 'timeslot_unavailable' | 'resource_required' | 'resource_unavailable' | 'spots_required' | 'spots_unavailable' | 'pricing_option_unavailable' | 'add_on_required' | 'add_on_unavailable' | 'customer_email_required' | 'customer_first_name_required' | 'customer_last_name_required';
type BookingValidationErrorScope = 'order' | 'reservation' | 'customer';
interface BookingValidationError {
code: BookingValidationErrorCode;
message: string;
scope: BookingValidationErrorScope;
field?: string;
reservationIndex?: number;
reservationId?: string;
agendaId?: string;
date?: string;
timeslot?: Timeslot;
resourceId?: string;
requestedSpots?: number;
availableSpots?: number;
minimumAdvanceNotice?: {
unit: 'minutes' | 'hours' | 'days';
value: number;
};
minimumBookingDateTime?: string;
maximumBookingDate?: string;
addOnId?: string;
addOnName?: string;
pricingOptionId?: string;
}
interface ReservationValidationResult {
valid: boolean;
reservation?: ValidatedReservation;
errors: string[];
errorDetails?: BookingValidationError[];
}
type BookingOrderReservation = Omit<ValidatedReservation, 'agendaId'> & {
agendaId?: string;
};
interface ProcessBookingChangeReservation {
date: string;
timeslot: Timeslot;
resourceId?: string;
spots: number;
status?: string;
agendaId?: string;
}
interface ProcessBookingChangeOrder<Reservation extends ProcessBookingChangeReservation = BookingOrderReservation> {
id: string;
status: string;
agendaId?: string;
reservations: Reservation[];
}
interface BookingOrderInput {
reservations: ReservationValidationInput[];
customerInfo: CustomerInfo;
adjustments?: BookingAdjustment[];
paymentType?: 'full' | 'partial' | 'on-site';
metadata?: BookingMetadata;
}
interface CustomerInfo {
email: string;
firstName: string;
lastName: string;
phone?: string;
customFields?: {
[fieldId: string]: any;
};
}
interface BookingOrder {
id: string;
status: 'pending' | 'confirmed' | 'cancelled';
agendaId?: string;
reservations: BookingOrderReservation[];
flattenedReservations?: string[];
flattenedReservationDates?: string[];
adjustments?: BookingAdjustment[];
customerInfo: CustomerInfo;
metadata?: BookingMetadata;
subtotal: number;
discount: number;
total: number;
amountDue: number;
amountPaid: number;
paymentType: 'full' | 'partial' | 'on-site';
paymentStatus: 'unpaid' | 'partial' | 'paid' | 'refunded' | 'partially_refunded';
createdAt: string;
updatedAt: string;
}
interface CreateOrderResult {
success: boolean;
order?: BookingOrder;
errors: string[];
errorDetails?: BookingValidationError[];
}
interface PaymentLinkOptions {
successUrl: string;
cancelUrl: string;
customerEmail?: string;
metadata?: Record<string, string>;
/**
* Per-payment override for stripe.webhookType. If the same BookerServer
* handles the webhook, include this value in stripe.allowedWebhookTypes.
*/
webhookType?: string;
/**
* Per-payment override for stripe.webhookTypeMetadataKey.
*/
webhookTypeMetadataKey?: string;
}
interface PaymentLinkResult {
url: string;
sessionId: string;
}
interface PaymentWebhookResult {
handled: boolean;
orderId?: string;
eventType?: string;
webhookType?: string | null;
expectedWebhookTypes?: string[];
ignoredReason?: 'unsupported_event_type' | 'unexpected_webhook_type' | 'missing_order_id';
}
type ProcessBookingChangeEvent<Params = Record<string, string>> = FirestoreEvent<Change<DocumentSnapshot> | undefined, Params>;
type MaybePromise<T> = T | Promise<T>;
interface ProcessBookingChangeReservationContext<Order extends ProcessBookingChangeOrder<ProcessBookingChangeReservation> = BookingOrder, Params = Record<string, string>> {
event: ProcessBookingChangeEvent<Params>;
order: Order;
}
interface ProcessBookingChangeCallbackContext<Order extends ProcessBookingChangeOrder<ProcessBookingChangeReservation> = BookingOrder, Params = Record<string, string>> {
event: ProcessBookingChangeEvent<Params>;
order: Order;
previousOrder: Order | null;
beforeData: Order | null;
afterData: Order;
addedReservations: Array<Order['reservations'][number]>;
removedReservations: Array<Order['reservations'][number]>;
}
type ProcessBookingConfirmedContext<Order extends ProcessBookingChangeOrder<ProcessBookingChangeReservation> = BookingOrder, Params = Record<string, string>> = ProcessBookingChangeCallbackContext<Order, Params> & {
order: Order & {
status: 'confirmed';
};
afterData: Order & {
status: 'confirmed';
};
};
type ProcessBookingCancelledContext<Order extends ProcessBookingChangeOrder<ProcessBookingChangeReservation> = BookingOrder, Params = Record<string, string>> = ProcessBookingChangeCallbackContext<Order, Params> & {
order: Order & {
status: 'cancelled';
};
afterData: Order & {
status: 'cancelled';
};
};
type ProcessBookingChangeOptions<Order extends ProcessBookingChangeOrder<ProcessBookingChangeReservation> = BookingOrder, Params = Record<string, string>> = {
event: ProcessBookingChangeEvent<Params>;
onConfirmed?: (context: ProcessBookingConfirmedContext<Order, Params>) => MaybePromise<void>;
onCancelled?: (context: ProcessBookingCancelledContext<Order, Params>) => MaybePromise<void>;
shouldProcessReservation?: (reservation: Order['reservations'][number], context: ProcessBookingChangeReservationContext<Order, Params>) => boolean;
};
type CalendarDateRef = ShallowRef<CalendarDate | null>;
type BookingMetadataValue = string | number | boolean | null;
type BookingMetadata = Record<string, BookingMetadataValue>;
interface BookingAdjustment {
type: 'fixed' | 'percent';
value: number;
label: string;
}
interface BookerConfig {
defaultLocale?: string;
defaultCurrency?: string;
defaultQueryLimit?: number;
previewWhiteList?: string | string[];
persistCart?: boolean;
provider: 'firebase';
firebase?: {
db: any;
agendas: string;
reserved_spots: string;
orders: string;
};
createOrderRequest?: (bookingOrderData: BookingOrderRequestData) => Promise<string>;
createPaymentLinkRequest?: (orderId: string, options?: PaymentLinkOptions) => Promise<string>;
}
interface Timeslot {
startTime: string;
endTime: string;
}
/**
* A reactive reservation instance with its own selection state.
* Each reservation manages date, timeslot, spots, add-ons independently.
* This allows multiple reservations to be edited simultaneously on the same screen.
*/
interface ReactiveReservation {
/** Unique identifier for this reservation */
readonly id: string;
/** Selected date for this reservation */
date: CalendarDateRef;
/** Selected timeslot */
hour: Ref<Timeslot | null>;
/** Selected resource (if agenda has resources) */
resource: Ref<string | null>;
/** Number of spots to reserve */
spots: Ref<number>;
/** Selected add-ons for this reservation */
addOns: Ref<{
[addOnId: string]: any;
}>;
/** Selected pricing option */
pricingOption: Ref<string | null>;
/** Additional custom metadata for this reservation */
metadata: Ref<BookingMetadata>;
/** Available timeslots for the selected date */
availableHours: ComputedRef<Timeslot[]>;
/** Available spots for the selected timeslot (considering other reservations) */
availableSpots: ComputedRef<number>;
/** Availability for each resource for the selected date and timeslot */
resourceAvailability: ComputedRef<ResourceAvailability[]>;
/** Base price (spots × unit price) */
basePrice: ComputedRef<number>;
/** Add-ons price */
addOnsPrice: ComputedRef<number>;
/** Total price for this reservation */
totalPrice: ComputedRef<number>;
/** Whether this reservation has valid selections */
isValid: ComputedRef<boolean>;
/** Validation errors for this reservation */
validationErrors: ComputedRef<string[]>;
/** Toggle an add-on on/off */
toggleAddOn: (addOnId: string, value?: any) => void;
/** Replace reservation metadata */
setMetadata: (metadata: BookingMetadata) => void;
/** Merge metadata into the existing reservation metadata */
patchMetadata: (metadata: Partial<BookingMetadata>) => void;
/** Remove a single metadata key */
removeMetadata: (key: string) => void;
/** Clear all metadata */
clearMetadata: () => void;
/** Clear all add-ons */
clearAddOns: () => void;
/** Reset all selections to defaults */
reset: () => void;
/** Convert to plain data object (for persistence/API calls) */
toJSON: () => ReservationData;
}
/**
* Serialized reservation data for persistence and API calls
*/
interface ReservationData {
id: string;
agendaId: string;
date: string;
timeslot: Timeslot;
resourceId?: string;
spots: number;
addOns: {
[addOnId: string]: any;
};
pricingOptionId?: string;
basePrice: number;
addOnsPrice: number;
totalPrice: number;
adjustments?: BookingAdjustment[];
metadata?: BookingMetadata;
}
interface BookingOrderRequestData {
reservations: ReservationData[];
adjustments?: BookingAdjustment[];
customerInfo: {
[key: string]: any;
};
paymentType: 'full' | 'partial' | 'on-site';
metadata?: BookingMetadata;
}
interface ResourceAvailability extends AgendaResource {
availableSpots: number;
isFull: boolean;
isAvailable: boolean;
}
type BookingResourceSelectionMode = 'resource-first' | 'time-first';
interface UseAppointmentBookerReturn {
agenda: ComputedRef<Agenda | null>;
resources: ComputedRef<AgendaResource[]>;
customerInformationFields: ComputedRef<CustomerInformationField[]>;
customerInformation: Ref<{
[key: string]: any;
}>;
availableAddOns: ComputedRef<AppointmentAddOn[]>;
availablePricingOptions: ComputedRef<AppointmentPricingOption[]>;
minimumBookingDate: ComputedRef<CalendarDate>;
maximumBookingDate: ComputedRef<CalendarDate>;
/** Array of reactive reservation objects */
reservations: Ref<ReactiveReservation[]>;
/** Create a new reservation and add it to the list */
createReservation: (initialDate?: CalendarDate) => ReactiveReservation;
/** Remove a reservation by ID */
removeReservation: (reservationId: string) => boolean;
/** Clear all reservations */
clearReservations: () => void;
/** Custom metadata attached to the entire order */
orderMetadata: Ref<BookingMetadata>;
/** Replace order metadata */
setOrderMetadata: (metadata: BookingMetadata) => void;
/** Merge metadata into the existing order metadata */
patchOrderMetadata: (metadata: Partial<BookingMetadata>) => void;
/** Remove a single order metadata key */
removeOrderMetadata: (key: string) => void;
/** Clear all order metadata */
clearOrderMetadata: () => void;
cartItemCount: ComputedRef<number>;
cartTotalSpots: ComputedRef<number>;
cartSubtotal: ComputedRef<number>;
cartTotal: ComputedRef<number>;
/** Whether all reservations in cart are valid */
isCartValid: ComputedRef<boolean>;
/** Whether required customer information is valid */
isUserDetailsValid: ComputedRef<boolean>;
/** List of invalid reservations */
invalidReservations: ComputedRef<ReactiveReservation[]>;
/** Get all reservations as plain data (for checkout/persistence) */
getCartData: () => ReservationData[];
/** Generate Zod schema fields from customer information fields (requires Zod to be installed) */
/** Generate complete Zod object schema from customer information fields (requires Zod to be installed) */
isLoading: ComputedRef<boolean>;
error: ComputedRef<Error | null>;
refresh: () => Promise<void>;
refreshAvailability: (dates?: string | string[]) => Promise<void>;
createOrderRequest: () => Promise<string>;
}
interface Agenda {
serviceName: string;
type: 'regular' | 'full-day' | 'multi-day';
resources?: AgendaResource[];
serviceVisibility: number;
maxTicketsPerReservation?: number;
customerInformationFields: CustomerInformationField[];
minimumAdvanceNotice: {
unit: 'minutes' | 'hours' | 'days';
value: number;
};
exceptions: AgendaException[];
addOns: AppointmentAddOn[];
currency: string;
partialPayment?: {
enabled: boolean;
amount: number;
type: 'fixed' | 'percentage';
};
paymentType: 'full' | 'partial' | 'on-site';
pricingOptions: AppointmentPricingOption[];
pricingRules?: AppointmentPricingRule[];
needsApproval?: boolean;
}
interface AgendaResource {
id: string;
name: string;
publicLabel?: string;
avatarLabel?: string;
description?: string;
capacity: number;
interval: number;
openingHours: {
[key: number]: TimeRange[];
};
color?: string;
isActive: boolean;
}
interface TimeRange {
start: string;
end: string;
}
interface CustomerInformationField {
id: string;
fieldName: string;
fieldDescription?: string;
fieldType: 'text' | 'tel' | 'email' | 'checkbox' | 'radio' | 'select';
required?: boolean;
visible?: boolean;
options?: string[];
}
interface TimeslotEvent {
startTime: string;
endTime: string;
capacity?: number;
}
interface AgendaException {
startDate: string;
endDate: string;
isClosed: boolean;
timeslots?: TimeslotEvent[];
resourceIds?: string[] | null;
}
interface AppointmentAddOn {
id: string;
name: string;
description?: string;
price: number;
required: boolean;
scope: AddOnScope;
limit?: number;
}
type AddOnScope = 'BOOKING' | 'RESERVATION' | 'TICKET' | 'UNLIMITED' | 'CUSTOM_LIMIT';
interface AppointmentPricingOption {
id: string;
name: string;
description?: string;
price: number;
duration: number;
isDefault?: boolean;
}
interface AppointmentPricingRule {
id: string;
name: string;
condition: 'time_after' | 'time_before' | 'time_between' | 'day_of_week' | 'date_range';
conditionValue: string | number[] | {
start: string;
end: string;
} | {
startDate: string;
endDate: string;
};
modifier: 'fixed' | 'percentage';
amount: number;
}
interface UseAppointmentBookerConfig {
autoCreateReservation?: boolean;
resourceSelectionMode?: BookingResourceSelectionMode;
}
export type { Agenda as A, BookerServerConfig as B, CreateOrderResult as C, ReservationData as D, ReservationValidationInput as E, ReservationValidationResult as F, ResourceAvailability as G, UseAppointmentBookerReturn as H, BookerConfig as I, PaymentLinkOptions as P, ReactiveReservation as R, Timeslot as T, UseAppointmentBookerConfig as U, ValidatedReservation as V, AgendaResource as a, AppointmentAddOn as b, AppointmentPricingOption as c, AppointmentPricingRule as d, BookingAdjustment as e, BookingMetadata as f, BookingMetadataValue as g, BookingOrder as h, BookingOrderInput as i, BookingOrderRequestData as j, BookingOrderReservation as k, BookingResourceSelectionMode as l, BookingValidationError as m, BookingValidationErrorCode as n, BookingValidationErrorScope as o, CustomerInfo as p, PaymentLinkResult as q, PaymentWebhookResult as r, ProcessBookingCancelledContext as s, ProcessBookingChangeCallbackContext as t, ProcessBookingChangeEvent as u, ProcessBookingChangeOptions as v, ProcessBookingChangeOrder as w, ProcessBookingChangeReservation as x, ProcessBookingChangeReservationContext as y, ProcessBookingConfirmedContext as z };