All files / src/services speakerPoolService.ts

86.66% Statements 13/15
100% Branches 2/2
85.71% Functions 6/7
86.66% Lines 13/15

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                                                  9x                                       8x         4x                     6x       3x                     3x                     3x       2x                                                                                                     9x         5x                                     3x         2x         9x        
/**
 * Speaker Pool Service (Story 5.2)
 *
 * HTTP client for Speaker Pool Management APIs
 * Features:
 * - Add speaker to event pool (POST /api/v1/events/{eventCode}/speakers/pool)
 * - Get speaker pool for event (GET /api/v1/events/{eventCode}/speakers/pool)
 * - JWT authentication via interceptors
 * - Error handling with correlation IDs
 */
 
import apiClient from '@/services/api/apiClient';
import type {
  SpeakerPoolEntry,
  AddSpeakerToPoolRequest,
  PatchSpeakerPoolRequest,
  PromoteSpeakerRequest,
  SpeakerPoolResponse,
  SendInvitationRequest,
  SendInvitationResponse,
  SendReminderRequest,
  SendReminderResponse,
} from '@/types/speakerPool.types';
 
// API base path
const EVENTS_API_PATH = '/events';
 
/**
 * Speaker Pool Service
 *
 * Handles all HTTP requests for speaker pool management
 */
class SpeakerPoolService {
  /**
   * Add speaker to event speaker pool (AC9-12)
   *
   * @param eventCode Event code (e.g., "BATbern56")
   * @param request Speaker data (name, company, expertise, assigned organizer, notes)
   * @returns Created speaker pool entry
   * @throws Error if event not found, validation fails, or unauthorized
   */
  async addSpeakerToPool(
    eventCode: string,
    request: AddSpeakerToPoolRequest
  ): Promise<SpeakerPoolResponse> {
    const response = await apiClient.post<SpeakerPoolResponse>(
      `${EVENTS_API_PATH}/${eventCode}/speakers/pool`,
      request
    );
 
    return response.data;
  }
 
  /**
   * Get speaker pool for event
   *
   * @param eventCode Event code (e.g., "BATbern56")
   * @returns List of speaker pool entries
   * @throws Error if event not found or unauthorized
   */
  async getSpeakerPool(eventCode: string): Promise<SpeakerPoolEntry[]> {
    const response = await apiClient.get<SpeakerPoolEntry[]>(
      `${EVENTS_API_PATH}/${eventCode}/speakers/pool`
    );
 
    return response.data;
  }
 
  /**
   * Delete speaker from event speaker pool
   *
   * @param eventCode Event code (e.g., "BATbern56")
   * @param speakerId Speaker UUID
   * @throws Error if event or speaker not found, or unauthorized
   */
  async deleteSpeakerFromPool(eventCode: string, speakerId: string): Promise<void> {
    await apiClient.delete(`${EVENTS_API_PATH}/${eventCode}/speakers/pool/${speakerId}`);
  }
 
  /**
   * Partial update of a speaker pool entry (e.g. reassign organizer, update notes/email).
   */
  async patchSpeakerPool(
    eventCode: string,
    speakerId: string,
    request: PatchSpeakerPoolRequest
  ): Promise<SpeakerPoolResponse> {
    const response = await apiClient.patch<SpeakerPoolResponse>(
      `${EVENTS_API_PATH}/${eventCode}/speakers/pool/${speakerId}`,
      request
    );
    return response.data;
  }
 
  /**
   * Promote a CONTACTED speaker to READY (Story 11.D.1).
   *
   * Drives the CONTACTED → READY workflow transition and provisions the speaker as a
   * User with the SPEAKER role server-side. The endpoint returns the updated speaker
   * pool entry; the caller should invalidate the speaker pool query to pick up the new
   * status + username + email fields.
   *
   * Backend behaviour:
   * - 200 OK: happy path or idempotent same-state on READY
   * - 400: missing/malformed email, unknown body field
   * - 409 with `details.code = INVALID_PROMOTION_STATE`: speaker is in a state other
   *   than CONTACTED/READY (e.g., IDENTIFIED, INVITED, ACCEPTED, DECLINED)
   * - 404: event or speaker not found
   *
   * @param eventCode Event code (e.g., "BATbern56")
   * @param speakerId Speaker pool UUID
   * @param request   email (required), firstName/lastName (optional)
   */
  async promoteToSpeaker(
    eventCode: string,
    speakerId: string,
    request: PromoteSpeakerRequest
  ): Promise<SpeakerPoolResponse> {
    const response = await apiClient.post<SpeakerPoolResponse>(
      `${EVENTS_API_PATH}/${eventCode}/speakers/${speakerId}/promote`,
      request
    );
    return response.data;
  }
 
  /**
   * Send invitation to speaker (Story 6.1c)
   *
   * Transitions speaker from IDENTIFIED to INVITED state and sends invitation email
   * with magic link for speaker portal access.
   *
   * @param eventCode Event code (e.g., "BATbern56")
   * @param username Speaker UUID or username
   * @param options Optional invitation options (deadline, personal message)
   * @returns Response with magic link token, new state, and email sent to
   * @throws Error if speaker not found, already invited, or in invalid state
   */
  async sendInvitation(
    eventCode: string,
    username: string,
    options?: SendInvitationRequest
  ): Promise<SendInvitationResponse> {
    const response = await apiClient.post<SendInvitationResponse>(
      `${EVENTS_API_PATH}/${eventCode}/speakers/${username}/send-invitation`,
      options || {}
    );
 
    return response.data;
  }
 
  /**
   * Send reminder to speaker (Story 6.5)
   *
   * Sends a deadline reminder email to an invited or accepted speaker.
   *
   * @param eventCode Event code (e.g., "BATbern56")
   * @param speakerPoolId Speaker pool entry UUID
   * @param request Reminder type and optional tier override
   * @returns Response with tier used and email sent to
   * @throws Error if speaker not found, no email, or reminder cannot be sent
   */
  async sendReminder(
    eventCode: string,
    speakerPoolId: string,
    request: SendReminderRequest
  ): Promise<SendReminderResponse> {
    const response = await apiClient.post<SendReminderResponse>(
      `${EVENTS_API_PATH}/${eventCode}/speaker-pool/${speakerPoolId}/send-reminder`,
      request
    );
 
    return response.data;
  }
}
 
// Export singleton instance
export const speakerPoolService = new SpeakerPoolService();
 
// Export class for testing
export default SpeakerPoolService;