Files
michaelschiemer/src/Application/Api/PreSaveApiController.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

348 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Application\Api;
use App\Framework\Attributes\Route;
use App\Framework\Core\Method;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\JsonResult;
use App\Domain\PreSave\PreSaveCampaignRepository;
use App\Domain\PreSave\PreSaveRegistrationRepository;
use App\Domain\PreSave\PreSaveRegistration;
use App\Domain\PreSave\ValueObjects\StreamingPlatform;
use App\Framework\OAuth\Storage\OAuthTokenRepository;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
/**
* Pre-Save Campaign API Controller
*/
final readonly class PreSaveApiController
{
public function __construct(
private PreSaveCampaignRepository $campaignRepository,
private PreSaveRegistrationRepository $registrationRepository,
private OAuthTokenRepository $tokenRepository
) {}
/**
* Get active campaigns
*/
#[Route(path: '/api/presave/campaigns', method: Method::GET)]
public function getCampaigns(HttpRequest $request): JsonResult
{
try {
$campaigns = $this->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
]);
}
}