All files / src/services/api eventRegistrationService.ts

91.42% Statements 32/35
80% Branches 16/20
77.77% Functions 7/9
91.17% Lines 31/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239                                                                                          13x                                               3x         12x     12x 2x 2x       12x 4x 3x   4x 2x         12x 2x       12x           9x   9x                           3x           4x   4x         3x                   3x       1x                 3x       2x                       3x       3x                     3x                                 3x     3x     2x                         3x                              
/**
 * Event Registration Service
 *
 * API service for fetching event registrations (participants).
 * Story 3.3: Event Participants Tab
 */
 
import apiClient from './apiClient';
import type {
  EventParticipant,
  EventRegistrationsResponse,
  FetchEventRegistrationsOptions,
} from '@/types/eventParticipant.types';
 
/**
 * Backend API response format (field names differ from frontend)
 */
interface BackendRegistrationItem {
  registrationCode: string;
  eventCode: string;
  status: string;
  registrationDate: string;
  attendeeUsername: string;
  attendeeFirstName: string; // Backend uses attendeeFirstName
  attendeeLastName: string; // Backend uses attendeeLastName
  attendeeEmail: string; // Backend uses attendeeEmail
  attendeeCompany?: string; // Company ID (optional)
  waitlistPosition?: number | null; // Story 10.11 — 1-based; null unless status === WAITLIST
}
 
interface BackendPaginatedResponse {
  data: BackendRegistrationItem[];
  pagination: {
    page: number;
    limit: number;
    totalItems: number;
    totalPages: number;
    hasNext: boolean;
    hasPrev: boolean;
  };
}
 
/**
 * Transform backend registration item to frontend EventParticipant type
 */
const transformRegistration = (backendReg: BackendRegistrationItem): EventParticipant => ({
  registrationCode: backendReg.registrationCode,
  eventCode: backendReg.eventCode,
  attendeeUsername: backendReg.attendeeUsername,
  firstName: backendReg.attendeeFirstName, // Transform from attendeeFirstName
  lastName: backendReg.attendeeLastName, // Transform from attendeeLastName
  email: backendReg.attendeeEmail, // Transform from attendeeEmail
  company: backendReg.attendeeCompany
    ? { id: backendReg.attendeeCompany, name: backendReg.attendeeCompany }
    : undefined,
  status: backendReg.status as EventParticipant['status'],
  registrationDate: backendReg.registrationDate,
  // Epic 14 FR28: carry the authoritative 1-based queue position so the
  // Waitlisted-filter view numbers rows by the backend, not by page index.
  waitlistPosition: backendReg.waitlistPosition ?? null,
});
 
/**
 * Fetch event registrations (participants) for a specific event
 *
 * @param eventCode - Event code to fetch registrations for
 * @param options - Optional filters, pagination, and search
 * @returns Registrations list and total count
 */
export const getEventRegistrations = async (
  eventCode: string,
  options?: Omit<FetchEventRegistrationsOptions, 'eventCode'>
): Promise<EventRegistrationsResponse> => {
  // Build query parameters
  const params: Record<string, unknown> = {};
 
  // Add pagination
  if (options?.pagination) {
    params.page = options.pagination.page;
    params.limit = options.pagination.limit;
  }
 
  // Add filters
  if (options?.filters) {
    if (options.filters.status && options.filters.status.length > 0) {
      params.status = options.filters.status;
    }
    if (options.filters.companyId) {
      params.companyId = options.filters.companyId;
    }
  }
 
  // Add search
  if (options?.search) {
    params.search = options.search;
  }
 
  // Backend returns paginated response: { data: [...], pagination: {...} }
  const response = await apiClient.get<BackendPaginatedResponse>(
    `/events/${eventCode}/registrations`,
    { params }
  );
 
  // Transform backend field names to frontend format
  const data = response.data.data.map(transformRegistration);
 
  return {
    data,
    pagination: response.data.pagination,
  };
};
 
/**
 * Update registration status
 *
 * @param eventCode - Event code
 * @param registrationCode - Registration code (e.g., BAT-2025-000123)
 * @param status - New registration status
 * @returns Updated registration
 */
export const updateRegistrationStatus = async (
  eventCode: string,
  registrationCode: string,
  status: EventParticipant['status']
): Promise<EventParticipant> => {
  // Backend expects lowercase status values to match database constraint
  const lowercaseStatus = status.toLowerCase();
 
  const response = await apiClient.patch<BackendRegistrationItem>(
    `/events/${eventCode}/registrations/${registrationCode}`,
    { status: lowercaseStatus }
  );
 
  return transformRegistration(response.data);
};
 
/**
 * Cancel registration (set status to CANCELLED)
 *
 * @param eventCode - Event code
 * @param registrationCode - Registration code (e.g., BAT-2025-000123)
 * @returns Updated registration with CANCELLED status
 */
export const cancelRegistration = async (
  eventCode: string,
  registrationCode: string
): Promise<EventParticipant> => {
  return updateRegistrationStatus(eventCode, registrationCode, 'CANCELLED');
};
 
/**
 * Delete registration (permanent removal)
 *
 * @param eventCode - Event code
 * @param registrationCode - Registration code (e.g., BAT-2025-000123)
 */
export const deleteRegistration = async (
  eventCode: string,
  registrationCode: string
): Promise<void> => {
  await apiClient.delete(`/events/${eventCode}/registrations/${registrationCode}`);
};
 
/**
 * Promote a waitlisted registration to registered (Story 10.11 — AC3, AC5)
 *
 * ORGANIZER only. Promotes the specified waitlisted registration to status=registered.
 *
 * @param eventCode - Event code
 * @param registrationCode - Registration code of the waitlisted attendee
 * @throws 404 if registration not found; 409 if not on waitlist; 403 if not ORGANIZER
 */
export const promoteFromWaitlist = async (
  eventCode: string,
  registrationCode: string
): Promise<void> => {
  await apiClient.post(`/events/${eventCode}/registrations/${registrationCode}/promote`);
};
 
/**
 * Resend registration confirmation email (organizer only)
 *
 * Only works for registrations in REGISTERED (pending) status.
 *
 * @param eventCode - Event code
 * @param registrationCode - Registration code
 */
export const resendConfirmationEmail = async (
  eventCode: string,
  registrationCode: string
): Promise<void> => {
  await apiClient.post(
    `/events/${eventCode}/registrations/${registrationCode}/resend-confirmation`
  );
};
 
/**
 * Enroll all organizers and partners as confirmed participants for an existing event.
 *
 * ORGANIZER only. Idempotent — already-registered users are counted as skipped.
 *
 * @param eventCode - Event code
 * @returns Summary of enrolled and skipped counts
 */
export const enrollStakeholders = async (
  eventCode: string
): Promise<{ enrolled: number; skipped: number }> => {
  const response = await apiClient.post<{ enrolled: number; skipped: number }>(
    `/events/${eventCode}/enroll-stakeholders`
  );
  return response.data;
};
 
/**
 * Add an existing user as a confirmed participant (organizer only).
 *
 * @param eventCode - Event code
 * @param params - `username` of the chosen existing user; `force` to add over capacity;
 *                 `notify` to email the attendee a "you have a confirmed spot" notice (default true)
 * @returns the created registration's code + status
 * @throws 404 unknown event/user; 409 already-registered (generic) or capacity-full
 *         (`error.response.data.details.code === 'capacity_exceeded'`); 403 if not ORGANIZER
 */
export const addParticipant = async (
  eventCode: string,
  params: { username: string; force?: boolean; notify?: boolean }
): Promise<{ registrationCode: string; status: string; attendeeUsername: string }> => {
  const response = await apiClient.post<{
    registrationCode: string;
    status: string;
    attendeeUsername: string;
  }>(`/events/${eventCode}/participants`, {
    username: params.username,
    force: params.force ?? false,
    notify: params.notify ?? true,
  });
  return response.data;
};