campaignRepository->findAll(['status' => 'active']); return new JsonResult([ 'success' => true, 'data' => array_map(fn($campaign) => [ 'id' => $campaign->id, 'title' => $campaign->title, 'artist_name' => $campaign->artistName, 'cover_image_url' => $campaign->coverImageUrl, 'description' => $campaign->description, 'release_date' => $campaign->releaseDate->toTimestamp(), 'status' => $campaign->status->value, 'platforms' => array_map(fn($url) => $url->platform->value, $campaign->trackUrls) ], $campaigns) ]); } catch (\Throwable $e) { throw FrameworkException::create( ErrorCode::DB_QUERY_FAILED, 'Failed to fetch campaigns' )->withContext( $e->getMessage() ); } } /** * Get campaign by ID */ #[Route(path: '/api/presave/campaigns/{id}', method: Method::GET)] public function getCampaign(HttpRequest $request): JsonResult { $campaignId = $request->routeParams->get('id'); // Validate ID is numeric if (!is_numeric($campaignId)) { throw FrameworkException::create( ErrorCode::VAL_INVALID_VALUE, 'Campaign ID must be numeric' )->withData(['provided' => $campaignId]); } try { $campaign = $this->campaignRepository->findById((int) $campaignId); if ($campaign === null) { return new JsonResult([ 'success' => false, 'message' => 'Campaign not found' ], 404); } return new JsonResult([ 'success' => true, 'data' => [ 'id' => $campaign->id, 'title' => $campaign->title, 'artist_name' => $campaign->artistName, 'cover_image_url' => $campaign->coverImageUrl, 'description' => $campaign->description, 'release_date' => $campaign->releaseDate->toTimestamp(), 'status' => $campaign->status->value, 'platforms' => array_map(fn($url) => $url->platform->value, $campaign->trackUrls), 'track_urls' => array_map(fn($url) => $url->toArray(), $campaign->trackUrls) ] ]); } catch (\Throwable $e) { throw FrameworkException::create( ErrorCode::DB_QUERY_FAILED, 'Failed to fetch campaign' )->withContext($e->getMessage()); } } /** * Register for pre-save campaign */ #[Route(path: '/api/presave/campaigns/{id}/register', method: Method::POST)] public function register(HttpRequest $request): JsonResult { $campaignId = $request->routeParams->get('id'); // Validate ID is numeric if (!is_numeric($campaignId)) { throw FrameworkException::create( ErrorCode::VAL_INVALID_VALUE, 'Campaign ID must be numeric' )->withData(['provided' => $campaignId]); } $data = $request->parsedBody->toArray(); // Validate campaign exists $campaign = $this->campaignRepository->findById((int) $campaignId); if ($campaign === null) { return new JsonResult([ 'success' => false, 'message' => 'Campaign not found' ], 404); } // Validate required fields if (empty($data['user_id'])) { throw FrameworkException::create( ErrorCode::VAL_REQUIRED_FIELD_MISSING, 'user_id is required' ); } if (empty($data['platform'])) { throw FrameworkException::create( ErrorCode::VAL_REQUIRED_FIELD_MISSING, 'platform is required' ); } // Validate platform $platform = StreamingPlatform::tryFrom(strtolower($data['platform'])); if ($platform === null) { throw FrameworkException::create( ErrorCode::VAL_INVALID_VALUE, "Invalid platform: {$data['platform']}" )->withData([ 'provided' => $data['platform'], 'allowed' => ['spotify', 'apple_music', 'tidal'] ]); } // Check if user has OAuth token for this platform $platformName = match ($platform) { StreamingPlatform::SPOTIFY => 'spotify', StreamingPlatform::APPLE_MUSIC => 'apple_music', StreamingPlatform::TIDAL => 'tidal', default => $platform->value }; $token = $this->tokenRepository->findForUser($data['user_id'], $platformName); if ($token === null) { return new JsonResult([ 'success' => false, 'message' => "User not connected to {$platformName}", 'oauth_required' => true, 'oauth_url' => "/oauth/{$platformName}/authorize?user_id={$data['user_id']}&redirect_url=/presave/{$campaignId}" ], 401); } // Check for duplicate registration $existingRegistration = $this->registrationRepository->findForUserAndCampaign( $data['user_id'], $campaignId, $platform ); if ($existingRegistration !== null) { return new JsonResult([ 'success' => false, 'message' => 'You have already registered for this campaign', 'registration' => [ 'id' => $existingRegistration->id, 'status' => $existingRegistration->status->value, 'platform' => $existingRegistration->platform->value, 'registered_at' => $existingRegistration->registeredAt->toTimestamp() ] ], 409); } // Create registration $registration = PreSaveRegistration::create( campaignId: (int) $campaignId, userId: $data['user_id'], platform: $platform ); $savedRegistration = $this->registrationRepository->save($registration); return new JsonResult([ 'success' => true, 'message' => 'Successfully registered for pre-save campaign', 'data' => [ 'registration_id' => $savedRegistration->id, 'campaign_id' => $campaignId, 'platform' => $platform->value, 'status' => $savedRegistration->status->value, 'registered_at' => $savedRegistration->registeredAt->toTimestamp() ] ], 201); } /** * Check user's registration status for a campaign */ #[Route(path: '/api/presave/campaigns/{id}/status', method: Method::GET)] public function getRegistrationStatus(HttpRequest $request): JsonResult { $campaignId = $request->routeParams->get('id'); $userId = $request->query->get('user_id'); if ($userId === null) { throw FrameworkException::create( ErrorCode::VAL_REQUIRED_FIELD_MISSING, 'user_id query parameter is required' ); } // Get all registrations for this user and campaign $registrations = $this->registrationRepository->findByCampaign($campaignId); $userRegistrations = array_filter( $registrations, fn($reg) => $reg->userId === $userId ); if (empty($userRegistrations)) { return new JsonResult([ 'success' => true, 'registered' => false, 'registrations' => [] ]); } return new JsonResult([ 'success' => true, 'registered' => true, 'registrations' => array_map(fn($reg) => [ 'id' => $reg->id, 'platform' => $reg->platform->value, 'status' => $reg->status->value, 'registered_at' => $reg->registeredAt->toTimestamp(), 'processed_at' => $reg->processedAt?->toTimestamp(), 'error_message' => $reg->errorMessage ], array_values($userRegistrations)) ]); } /** * Get user's OAuth connection status */ #[Route(path: '/api/presave/oauth-status', method: Method::GET)] public function getOAuthStatus(HttpRequest $request): JsonResult { $userId = $request->query->get('user_id'); if ($userId === null) { throw FrameworkException::create( ErrorCode::VAL_REQUIRED_FIELD_MISSING, 'user_id query parameter is required' ); } $connections = []; $platforms = ['spotify', 'apple_music', 'tidal']; foreach ($platforms as $platform) { $token = $this->tokenRepository->findForUser($userId, $platform); $connections[$platform] = [ 'connected' => $token !== null, 'expires_at' => $token?->expiresAt, 'needs_refresh' => $token !== null && $token->expiresAt < time() ]; } return new JsonResult([ 'success' => true, 'user_id' => $userId, 'connections' => $connections ]); } /** * Get campaign statistics */ #[Route(path: '/api/presave/campaigns/{id}/stats', method: Method::GET)] public function getStats(HttpRequest $request): JsonResult { $campaignId = $request->routeParams->get('id'); $campaign = $this->campaignRepository->findById($campaignId); if ($campaign === null) { return new JsonResult([ 'success' => false, 'message' => 'Campaign not found' ], 404); } $registrations = $this->registrationRepository->findByCampaign($campaignId); $stats = [ 'total_registrations' => count($registrations), 'by_status' => [ 'pending' => 0, 'processing' => 0, 'completed' => 0, 'failed' => 0 ], 'by_platform' => [ 'spotify' => 0, 'apple_music' => 0, 'tidal' => 0 ] ]; foreach ($registrations as $registration) { $stats['by_status'][strtolower($registration->status->value)]++; $stats['by_platform'][strtolower($registration->platform->value)]++; } return new JsonResult([ 'success' => true, 'campaign_id' => $campaignId, 'stats' => $stats ]); } }