You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
7.5 KiB
TypeScript
293 lines
7.5 KiB
TypeScript
/**
|
|
* Booking Admin TypeScript Types
|
|
*
|
|
* Based on: Booking Admin API Contract v1.1
|
|
* Date: 2025-11-07
|
|
* Phase: Phase 3 (Booking Management)
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Core Booking Types
|
|
// ============================================================================
|
|
|
|
export interface BookingDetail {
|
|
booking_id: number;
|
|
status: BookingStatus;
|
|
slot: SlotInfo;
|
|
provider: ProviderInfo;
|
|
booked_by: UserInfo;
|
|
attendees: Attendee[];
|
|
policies: BookingPolicies;
|
|
created_at: string; // ISO8601
|
|
updated_at: string; // ISO8601
|
|
etag: string;
|
|
cancelled_reason?: string;
|
|
}
|
|
|
|
export type BookingStatus = 'confirmed' | 'cancelled' | 'no_show';
|
|
|
|
export interface SlotInfo {
|
|
slot_instance_id: number;
|
|
starts_at: string; // ISO8601
|
|
ends_at: string; // ISO8601
|
|
capacity: number;
|
|
booked_count: number;
|
|
capacity_remaining: number;
|
|
status: SlotStatus;
|
|
court: CourtInfo;
|
|
}
|
|
|
|
export type SlotStatus = 'open' | 'booked' | 'held' | 'cancelled';
|
|
|
|
export interface CourtInfo {
|
|
court_id: number;
|
|
name: string;
|
|
facility_id: number;
|
|
facility_name: string;
|
|
}
|
|
|
|
export interface ProviderInfo {
|
|
type: 'local' | 'fairplay';
|
|
manages_slot_storage: boolean;
|
|
}
|
|
|
|
export interface UserInfo {
|
|
app_user_id: number;
|
|
email: string;
|
|
display_name: string;
|
|
}
|
|
|
|
export interface Attendee {
|
|
position: number; // 1-based
|
|
type: AttendeeType;
|
|
app_user_id?: number;
|
|
email?: string;
|
|
display_name?: string;
|
|
remote_member_id?: number;
|
|
}
|
|
|
|
export type AttendeeType = 'app_user' | 'guest' | 'remote_member';
|
|
|
|
export interface BookingPolicies {
|
|
cancel_grace_minutes: number;
|
|
cancel_grace_anchor: 'start' | 'end';
|
|
move_window_days: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Request/Response Types
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Cancel Booking Request
|
|
*/
|
|
export interface CancelBookingRequest {
|
|
status: 'cancelled';
|
|
reason?: string; // max 500 chars
|
|
notify_players?: boolean; // default: true
|
|
prev_updated_at?: string; // ISO8601 (fallback for If-Match)
|
|
}
|
|
|
|
export interface CancelBookingResponse {
|
|
booking_id: number;
|
|
status: 'cancelled';
|
|
slot: SlotInfo;
|
|
cancelled_reason?: string;
|
|
updated_at: string; // ISO8601
|
|
etag: string;
|
|
}
|
|
|
|
/**
|
|
* Move Booking Request
|
|
*/
|
|
export interface MoveBookingRequest {
|
|
new_slot_instance_id: number;
|
|
reason?: string; // max 500 chars
|
|
notify_players?: boolean; // default: true
|
|
dry_run?: boolean; // default: false
|
|
prev_updated_at?: string; // ISO8601 (fallback for If-Match)
|
|
}
|
|
|
|
export interface MoveBookingResponse {
|
|
booking_id: number;
|
|
status: 'confirmed';
|
|
old_slot: SlotInfo;
|
|
new_slot: SlotInfo;
|
|
move_reason?: string;
|
|
updated_at: string; // ISO8601
|
|
etag: string;
|
|
}
|
|
|
|
export interface MoveDryRunResponse {
|
|
ok: boolean;
|
|
reasons?: string[]; // error codes if ok=false
|
|
}
|
|
|
|
/**
|
|
* Update Attendees Request
|
|
*/
|
|
export interface UpdateAttendeesRequest {
|
|
attendees: AttendeeInput[];
|
|
reason?: string; // max 500 chars
|
|
notify_players?: boolean; // default: true
|
|
prev_updated_at?: string; // ISO8601 (fallback for If-Match)
|
|
}
|
|
|
|
export interface AttendeeInput {
|
|
position: number; // 1-based
|
|
type: AttendeeType;
|
|
app_user_id?: number;
|
|
remote_member_id?: number;
|
|
display_name?: string;
|
|
}
|
|
|
|
export interface UpdateAttendeesResponse {
|
|
booking_id: number;
|
|
status: 'confirmed';
|
|
attendees: Attendee[];
|
|
slot: SlotInfo;
|
|
updated_at: string; // ISO8601
|
|
etag: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Error Types (RFC-7807)
|
|
// ============================================================================
|
|
|
|
export interface BookingAdminError {
|
|
type: string;
|
|
title: string;
|
|
status: number;
|
|
detail: string;
|
|
code: BookingErrorCode;
|
|
errors?: ValidationError[];
|
|
meta?: Record<string, any>;
|
|
}
|
|
|
|
export interface ValidationError {
|
|
field: string;
|
|
message: string;
|
|
}
|
|
|
|
export type BookingErrorCode =
|
|
| 'validation_error'
|
|
| 'not_found'
|
|
| 'not_admin_for_club'
|
|
| 'booking_already_cancelled'
|
|
| 'invalid_status_transition'
|
|
| 'slot_full'
|
|
| 'slot_wrong_status'
|
|
| 'slot_different_club'
|
|
| 'slot_different_sport'
|
|
| 'slot_in_past'
|
|
| 'exceeds_capacity'
|
|
| 'past_slot_locked'
|
|
| 'remote_provider_managed'
|
|
| 'capacity_conflict'
|
|
| 'booking_already_exists'
|
|
| 'duplicate_attendee'
|
|
| 'precondition_failed'
|
|
| 'precondition_required'
|
|
| 'idempotency_conflict'
|
|
| 'move_window_exceeded'
|
|
| 'internal_error';
|
|
|
|
// ============================================================================
|
|
// API Result Wrapper
|
|
// ============================================================================
|
|
|
|
export type BookingApiResult<T> =
|
|
| { success: true; data: T; etag?: string }
|
|
| { success: false; error: BookingAdminError };
|
|
|
|
// ============================================================================
|
|
// User-Friendly Error Messages
|
|
// ============================================================================
|
|
|
|
export const ERROR_MESSAGES: Record<BookingErrorCode, { title: string; hint: string }> = {
|
|
remote_provider_managed: {
|
|
title: 'Edits are disabled for this club.',
|
|
hint: "This club is managed by an external provider; changes must be made in the provider's system.",
|
|
},
|
|
capacity_conflict: {
|
|
title: 'Someone just took that spot.',
|
|
hint: "The slot's capacity changed while you were editing. Pick another slot and try again.",
|
|
},
|
|
booking_already_exists: {
|
|
title: 'This slot already has a booking.',
|
|
hint: 'Choose a different time or court.',
|
|
},
|
|
slot_different_sport: {
|
|
title: 'Wrong sport for this booking.',
|
|
hint: "Select a slot that matches the booking's sport.",
|
|
},
|
|
slot_different_club: {
|
|
title: 'Different club.',
|
|
hint: 'You can only move bookings within the same club.',
|
|
},
|
|
slot_wrong_status: {
|
|
title: "That slot isn't open.",
|
|
hint: 'Choose an open slot.',
|
|
},
|
|
slot_full: {
|
|
title: 'That slot is full.',
|
|
hint: 'Select a different time.',
|
|
},
|
|
past_slot_locked: {
|
|
title: "This booking can't be changed anymore.",
|
|
hint: "It's beyond the allowed time window.",
|
|
},
|
|
precondition_failed: {
|
|
title: 'This booking changed in the meantime.',
|
|
hint: 'Refresh and try again.',
|
|
},
|
|
precondition_required: {
|
|
title: 'Outdated data.',
|
|
hint: 'Please retry from the latest booking details.',
|
|
},
|
|
validation_error: {
|
|
title: 'Please fix the highlighted fields.',
|
|
hint: '',
|
|
},
|
|
not_admin_for_club: {
|
|
title: 'No admin access.',
|
|
hint: "You don't have permission for this club.",
|
|
},
|
|
booking_already_cancelled: {
|
|
title: 'This booking is already cancelled.',
|
|
hint: '',
|
|
},
|
|
invalid_status_transition: {
|
|
title: 'Cannot change booking status.',
|
|
hint: '',
|
|
},
|
|
slot_in_past: {
|
|
title: 'That slot already started.',
|
|
hint: 'Select a future time.',
|
|
},
|
|
exceeds_capacity: {
|
|
title: 'Too many attendees.',
|
|
hint: 'Reduce the number of attendees to fit the slot capacity.',
|
|
},
|
|
duplicate_attendee: {
|
|
title: 'Duplicate attendee.',
|
|
hint: 'Each person can only be added once.',
|
|
},
|
|
idempotency_conflict: {
|
|
title: 'Request conflict.',
|
|
hint: 'Please retry your action.',
|
|
},
|
|
move_window_exceeded: {
|
|
title: 'Move window exceeded.',
|
|
hint: 'The target slot is too far from the original booking date.',
|
|
},
|
|
not_found: {
|
|
title: 'Not found.',
|
|
hint: 'The booking or slot you requested does not exist.',
|
|
},
|
|
internal_error: {
|
|
title: 'Something went wrong.',
|
|
hint: 'Please try again later.',
|
|
},
|
|
};
|