- 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.
360 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|