Files
michaelschiemer/src/Application/Admin/PreSaveCampaignController.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

360 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Application\Admin;
use App\Domain\PreSave\PreSaveCampaign;
use App\Domain\PreSave\PreSaveCampaignRepository;
use App\Domain\PreSave\PreSaveRegistrationRepository;
use App\Domain\PreSave\ValueObjects\CampaignStatus;
use App\Framework\Attributes\Auth;
use App\Framework\Attributes\Route;
use App\Framework\Core\Method;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\JsonResult;
use App\Framework\Http\Redirect;
use App\Framework\Http\ViewResult;
/**
* Admin Controller for Pre-Save Campaign Management
*/
#[Auth(strategy: 'ip', allowedIps: ['127.0.0.1', '::1'])]
final readonly class PreSaveCampaignController
{
public function __construct(
private PreSaveCampaignRepository $campaignRepository,
private PreSaveRegistrationRepository $registrationRepository
) {
}
/**
* Show all campaigns overview
*/
#[Route(path: '/admin/presave/campaigns', method: Method::GET)]
public function index(HttpRequest $request): ViewResult
{
$campaigns = $this->campaignRepository->findAll();
return new ViewResult('admin/presave/campaigns/index', [
'campaigns' => $campaigns,
'stats' => $this->getGlobalStats(),
]);
}
/**
* Show create campaign form
*/
#[Route(path: '/admin/presave/campaigns/create', method: Method::GET)]
public function create(): ViewResult
{
return new ViewResult('admin/presave/campaigns/create');
}
/**
* Store new campaign
*/
#[Route(path: '/admin/presave/campaigns', method: Method::POST)]
public function store(HttpRequest $request): Redirect
{
$data = $request->parsedBody->toArray();
// Validate required fields
$this->validateCampaignData($data);
// Parse track URLs from form
$trackUrls = [];
if (! empty($data['spotify_url'])) {
$trackUrls['spotify'] = $data['spotify_url'];
}
if (! empty($data['apple_music_url'])) {
$trackUrls['apple_music'] = $data['apple_music_url'];
}
if (! empty($data['tidal_url'])) {
$trackUrls['tidal'] = $data['tidal_url'];
}
// Create campaign
$campaign = PreSaveCampaign::create(
title: $data['title'],
artistName: $data['artist_name'],
coverImageUrl: $data['cover_image_url'],
description: $data['description'] ?? null,
releaseDate: strtotime($data['release_date']),
trackUrls: $trackUrls,
startDate: ! empty($data['start_date']) ? strtotime($data['start_date']) : null
);
$this->campaignRepository->save($campaign);
return Redirect::to('/admin/presave/campaigns')
->withFlashMessage('success', 'Campaign created successfully!');
}
/**
* Show campaign details with registrations
*/
#[Route(path: '/admin/presave/campaigns/{id}', method: Method::GET)]
public function show(HttpRequest $request): ViewResult
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$registrations = $this->registrationRepository->findByCampaign($campaignId);
$stats = $this->getCampaignStats($campaign, $registrations);
return new ViewResult('admin/presave/campaigns/show', [
'campaign' => $campaign,
'registrations' => $registrations,
'stats' => $stats,
]);
}
/**
* Show edit campaign form
*/
#[Route(path: '/admin/presave/campaigns/{id}/edit', method: Method::GET)]
public function edit(HttpRequest $request): ViewResult
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
return new ViewResult('admin/presave/campaigns/edit', [
'campaign' => $campaign,
]);
}
/**
* Update campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}', method: Method::PUT)]
public function update(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$data = $request->parsedBody->toArray();
$this->validateCampaignData($data);
// Parse track URLs
$trackUrls = [];
if (! empty($data['spotify_url'])) {
$trackUrls['spotify'] = $data['spotify_url'];
}
if (! empty($data['apple_music_url'])) {
$trackUrls['apple_music'] = $data['apple_music_url'];
}
if (! empty($data['tidal_url'])) {
$trackUrls['tidal'] = $data['tidal_url'];
}
// Update campaign
$updatedCampaign = $campaign->updateDetails(
title: $data['title'],
artistName: $data['artist_name'],
coverImageUrl: $data['cover_image_url'],
description: $data['description'] ?? null,
releaseDate: strtotime($data['release_date']),
trackUrls: $trackUrls
);
$this->campaignRepository->save($updatedCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign updated successfully!');
}
/**
* Delete campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}', method: Method::DELETE)]
public function destroy(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);
}
$this->campaignRepository->delete($campaignId);
return new JsonResult([
'success' => true,
'message' => 'Campaign deleted successfully',
]);
}
/**
* Activate campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}/activate', method: Method::POST)]
public function activate(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$activeCampaign = $campaign->updateStatus(CampaignStatus::ACTIVE);
$this->campaignRepository->save($activeCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign activated successfully!');
}
/**
* Pause campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}/pause', method: Method::POST)]
public function pause(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$pausedCampaign = $campaign->updateStatus(CampaignStatus::PAUSED);
$this->campaignRepository->save($pausedCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign paused successfully!');
}
/**
* Complete campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}/complete', method: Method::POST)]
public function complete(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$completedCampaign = $campaign->updateStatus(CampaignStatus::COMPLETED);
$this->campaignRepository->save($completedCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign marked as completed!');
}
/**
* Validate campaign data
*/
private function validateCampaignData(array $data): void
{
$required = ['title', 'artist_name', 'cover_image_url', 'release_date'];
foreach ($required as $field) {
if (empty($data[$field])) {
throw FrameworkException::create(
ErrorCode::VAL_REQUIRED_FIELD_MISSING,
"Field '{$field}' is required"
)->withData(['field' => $field]);
}
}
// Validate at least one track URL
if (empty($data['spotify_url']) && empty($data['apple_music_url']) && empty($data['tidal_url'])) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'At least one track URL (Spotify, Apple Music, or Tidal) is required'
);
}
}
/**
* Get global campaign statistics
*/
private function getGlobalStats(): array
{
$allCampaigns = $this->campaignRepository->findAll();
$stats = [
'total' => count($allCampaigns),
'draft' => 0,
'active' => 0,
'paused' => 0,
'completed' => 0,
'total_registrations' => 0,
];
foreach ($allCampaigns as $campaign) {
$stats[strtolower($campaign->status->value)]++;
$registrations = $this->registrationRepository->findByCampaign($campaign->id);
$stats['total_registrations'] += count($registrations);
}
return $stats;
}
/**
* Get campaign-specific statistics
*/
private function getCampaignStats(PreSaveCampaign $campaign, array $registrations): array
{
$stats = [
'total_registrations' => count($registrations),
'pending' => 0,
'processing' => 0,
'completed' => 0,
'failed' => 0,
'by_platform' => [
'spotify' => 0,
'apple_music' => 0,
'tidal' => 0,
],
];
foreach ($registrations as $registration) {
$stats[strtolower($registration->status->value)]++;
$stats['by_platform'][strtolower($registration->platform->value)]++;
}
return $stats;
}
}