# Pre-Save Campaign System

A complete pre-save campaign system for music releases, integrated with streaming platforms like Spotify, Tidal, and Apple Music.

## What is a Pre-Save Campaign?

A pre-save campaign allows fans to register their interest in an upcoming music release before it's officially available. When the release date arrives, the track/album is automatically added to their library on their chosen streaming platform.

This is a common marketing tool in the music industry to:
- Build hype for upcoming releases
- Guarantee first-day streams
- Increase algorithmic visibility on streaming platforms
- Track fan engagement and reach

## System Architecture

```
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  Admin Creates  │───▶│  Fans Register  │───▶│ Release Day     │
│    Campaign     │    │  via OAuth      │    │  Processing     │
└─────────────────┘    └─────────────────┘    └─────────────────┘
        │                       │                       │
   Admin Interface        Public Landing Page    Background Jobs
   CRUD Operations        OAuth Integration      Auto-Add to Library
   Campaign Management    User Registration      Status Tracking
```

## Core Components

### 1. Value Objects (`ValueObjects/`)

**`StreamingPlatform`** - Supported streaming platforms
- SPOTIFY, TIDAL, APPLE_MUSIC, DEEZER, YOUTUBE_MUSIC
- Methods: `getDisplayName()`, `getOAuthProvider()`, `isSupported()`

**`CampaignStatus`** - Campaign lifecycle states
- DRAFT → SCHEDULED → ACTIVE → RELEASED → COMPLETED
- Methods: `acceptsRegistrations()`, `shouldProcess()`, `isEditable()`

**`RegistrationStatus`** - User registration states
- PENDING → COMPLETED / FAILED / REVOKED
- Methods: `canRetry()`, `isFinal()`

**`TrackUrl`** - Platform-specific track URL with ID extraction
- Automatic platform detection from URL
- Track ID extraction for API calls
- Methods: `fromUrl()`, `getPlatformId()`

### 2. Entities

**`PreSaveCampaign`** - Campaign entity
```php
PreSaveCampaign::create(
    title: 'New Album',
    artistName: 'The Artist',
    coverImageUrl: 'https://...',
    releaseDate: Timestamp::fromString('2024-12-01 00:00'),
    trackUrls: [TrackUrl::fromUrl('https://open.spotify.com/track/...')],
    description: 'Optional description',
    startDate: Timestamp::now()
);
```

**Lifecycle Methods:**
- `publish()` - Publish draft campaign (sets status to SCHEDULED or ACTIVE)
- `markAsReleased()` - Mark as released when release date passes
- `markAsCompleted()` - Mark as completed after processing
- `cancel()` - Cancel campaign at any time

**`PreSaveRegistration`** - User registration
```php
PreSaveRegistration::create(
    campaignId: 1,
    userId: 'spotify_user_123',
    platform: StreamingPlatform::SPOTIFY
);
```

**Status Methods:**
- `markAsCompleted()` - Mark as successfully processed
- `markAsFailed()` - Mark as failed with error message
- `resetForRetry()` - Reset for retry attempt

### 3. Repositories

**`PreSaveCampaignRepository`**
- `findById(int $id): ?PreSaveCampaign`
- `findAll(): array`
- `save(PreSaveCampaign $campaign): PreSaveCampaign`
- `delete(int $id): void`

**`PreSaveRegistrationRepository`**
- `findByCampaign(int $campaignId): array`
- `findByUserAndCampaign(string $userId, int $campaignId, StreamingPlatform $platform): ?PreSaveRegistration`
- `findPendingForCampaign(int $campaignId): array`
- `findFailedRegistrations(int $maxRetries = 3): array`
- `save(PreSaveRegistration $registration): PreSaveRegistration`
- `delete(int $id): void`

### 4. Services

**`PreSaveCampaignService`** - Business logic layer
```php
public function getCampaignStats(int $campaignId): array
{
    return [
        'total_registrations' => 150,
        'by_platform' => [
            'spotify' => 100,
            'tidal' => 30,
            'apple_music' => 20
        ],
        'by_status' => [
            'pending' => 120,
            'completed' => 25,
            'failed' => 5
        ]
    ];
}
```

**`PreSaveProcessor`** - Release day processing

**Core Methods:**
- `processReleasedCampaigns()` - Check for campaigns that should be released
- `processPendingRegistrations()` - Process all pending registrations
- `retryFailedRegistrations(int $campaignId, int $maxRetries)` - Retry failed registrations

**Processing Flow:**
```php
// 1. Mark campaigns as released
$releaseResults = $processor->processReleasedCampaigns();

// 2. Process pending registrations
$processingResults = $processor->processPendingRegistrations();

// 3. Retry failed registrations (separate job)
$retryResults = $processor->retryFailedRegistrations($campaignId, 3);
```

### 5. Background Jobs

**`ProcessReleasedCampaignsJob`** - Runs every hour
```php
#[Schedule(interval: Duration::fromMinutes(60))]
public function handle(): array
{
    // Step 1: Mark campaigns as released
    $releaseResults = $this->processor->processReleasedCampaigns();

    // Step 2: Process pending registrations
    $processingResults = $this->processor->processPendingRegistrations();

    return [
        'release_phase' => $releaseResults,
        'processing_phase' => $processingResults
    ];
}
```

**`RetryFailedRegistrationsJob`** - Runs every 6 hours
```php
#[Schedule(interval: Duration::fromHours(6))]
public function handle(): array
{
    $campaigns = $this->campaignRepository->findAll([
        'status' => CampaignStatus::RELEASED->value
    ]);

    foreach ($campaigns as $campaign) {
        $result = $this->processor->retryFailedRegistrations($campaign->id, 3);
        // Process retry results
    }
}
```

### 6. OAuth Integration

The system integrates with the OAuth module for authentication:

```php
// User clicks "Pre-Save on Spotify"
$authUrl = $this->oauthService->getAuthorizationUrl(
    'spotify',
    '/presave/123/oauth/callback/spotify'
);

// After OAuth authorization
$token = $this->oauthService->handleCallback('spotify', $code, $callbackUrl);

// Token is stored and auto-refreshed
$storedToken = $this->oauthService->getTokenForUser($userId, 'spotify');

// Use token to add tracks
$provider->addTracksToLibrary($storedToken->token, [$trackId]);
```

## Admin Interface

### Campaign Management

**Admin Routes:**
- `GET /admin/presave-campaigns` - List all campaigns
- `GET /admin/presave-campaigns/create` - Create campaign form
- `POST /admin/presave-campaigns` - Store new campaign
- `GET /admin/presave-campaigns/{id}` - View campaign stats
- `GET /admin/presave-campaigns/{id}/edit` - Edit campaign form
- `POST /admin/presave-campaigns/{id}` - Update campaign
- `POST /admin/presave-campaigns/{id}/delete` - Delete campaign
- `POST /admin/presave-campaigns/{id}/publish` - Publish draft campaign
- `POST /admin/presave-campaigns/{id}/cancel` - Cancel campaign

**Admin Features:**
- Create campaigns with multiple platform URLs
- Edit campaign details (title, artist, cover image, description)
- Set release date and optional campaign start date
- Manage campaign status (draft, scheduled, active, cancelled)
- View campaign statistics and registrations
- Publish campaigns to make them public
- Cancel campaigns at any time

### Campaign Form Fields

```php
- title: Campaign title (required)
- artist_name: Artist name (required)
- cover_image_url: Album/single artwork URL (required)
- description: Campaign description (optional)
- release_date: When track will be released (required)
- start_date: When to start accepting pre-saves (optional)
- track_url_spotify: Spotify track URL
- track_url_tidal: Tidal track URL (optional)
- track_url_apple_music: Apple Music track URL (optional)
```

## Public Interface

### End-User Flow

**Public Routes:**
- `GET /presave/{id}` - Campaign landing page
- `POST /presave/{id}/register/{platform}` - Register for pre-save
- `GET /presave/{id}/oauth/callback/{platform}` - OAuth callback
- `POST /presave/{id}/cancel/{platform}` - Cancel registration
- `GET /presave/{id}/status` - Get registration status (JSON)

**User Journey:**
1. Visit campaign landing page: `/presave/123`
2. Click "Pre-Save on Spotify"
3. Redirect to Spotify OAuth authorization
4. Callback stores token and creates registration
5. Redirect back to landing page with success message
6. On release day, background job adds track to library
7. User sees "✓ Added to Library" status

### Campaign Landing Page Features

- Album artwork display
- Track/album title and artist
- Release date badge
- Optional campaign description
- Platform selection buttons (Spotify, Tidal, etc.)
- Registration status display
- Success/cancellation messages
- Auto-refresh status for pending registrations
- Mobile-responsive design

## Database Schema

### `presave_campaigns` Table

```sql
CREATE TABLE presave_campaigns (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    title               VARCHAR(255) NOT NULL,
    artist_name         VARCHAR(255) NOT NULL,
    cover_image_url     TEXT NOT NULL,
    description         TEXT,
    release_date        INTEGER NOT NULL,
    start_date          INTEGER,
    track_urls          TEXT NOT NULL,  -- JSON array of TrackUrl objects
    status              VARCHAR(20) NOT NULL DEFAULT 'draft',
    created_at          INTEGER NOT NULL,
    updated_at          INTEGER NOT NULL
);

CREATE INDEX idx_presave_campaigns_status ON presave_campaigns(status);
CREATE INDEX idx_presave_campaigns_release_date ON presave_campaigns(release_date);
```

### `presave_registrations` Table

```sql
CREATE TABLE presave_registrations (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    campaign_id         INTEGER NOT NULL,
    user_id             VARCHAR(255) NOT NULL,
    platform            VARCHAR(50) NOT NULL,
    status              VARCHAR(20) NOT NULL DEFAULT 'pending',
    registered_at       INTEGER NOT NULL,
    processed_at        INTEGER,
    error_message       TEXT,
    retry_count         INTEGER NOT NULL DEFAULT 0,

    UNIQUE(campaign_id, user_id, platform),
    FOREIGN KEY (campaign_id) REFERENCES presave_campaigns(id) ON DELETE CASCADE
);

CREATE INDEX idx_presave_registrations_campaign_id ON presave_registrations(campaign_id);
CREATE INDEX idx_presave_registrations_user_id ON presave_registrations(user_id);
CREATE INDEX idx_presave_registrations_status ON presave_registrations(status);
CREATE INDEX idx_presave_registrations_platform ON presave_registrations(platform);
```

## Campaign Lifecycle

### Status Flow

```
DRAFT
  │
  ├─ publish() → SCHEDULED (if release_date in future)
  ├─ publish() → ACTIVE (if release_date has passed)
  └─ cancel() → CANCELLED

SCHEDULED
  │
  ├─ (on start_date) → ACTIVE
  ├─ (on release_date) → RELEASED
  └─ cancel() → CANCELLED

ACTIVE
  │
  ├─ (on release_date) → RELEASED
  └─ cancel() → CANCELLED

RELEASED
  │
  └─ (after processing) → COMPLETED

COMPLETED / CANCELLED
  │
  └─ [FINAL STATE]
```

### Registration Flow

```
PENDING
  │
  ├─ (on success) → COMPLETED
  ├─ (on error) → FAILED
  └─ (user action) → REVOKED

FAILED
  │
  ├─ (retry success) → COMPLETED
  └─ (max retries) → [STAYS FAILED]

COMPLETED / REVOKED
  │
  └─ [FINAL STATE]
```

## Platform Integration

### Currently Supported

**Spotify** ✅
- OAuth 2.0 authorization
- Pre-save implementation via `addTracksToLibrary()`
- Track ID extraction from Spotify URLs
- Library status checking

### Planned Support

**Tidal** 🔄 (In Development)
**Apple Music** 🔄 (In Development)
**Deezer** 📋 (Planned)
**YouTube Music** 📋 (Planned)

### Adding New Platform

1. Update `StreamingPlatform` enum:
```php
case NEW_PLATFORM = 'new_platform';

public function isSupported(): bool
{
    return match ($this) {
        self::NEW_PLATFORM => true,  // Enable support
        // ...
    };
}
```

2. Implement OAuth provider in `/src/Framework/OAuth/Providers/`:
```php
final readonly class NewPlatformProvider implements OAuthProvider
{
    public function addTracksToLibrary(OAuthToken $token, array $trackIds): bool
    {
        // Platform-specific implementation
    }
}
```

3. Update `TrackUrl::fromUrl()` to support new URL format:
```php
if (str_contains($url, 'newplatform.com/track/')) {
    preg_match('/track\/([a-zA-Z0-9]+)/', $url, $matches);
    return new self(StreamingPlatform::NEW_PLATFORM, $url, $matches[1]);
}
```

## Error Handling

### Failed Registrations

When a registration fails:
1. Status set to `FAILED`
2. Error message stored in `error_message` field
3. Retry count incremented
4. `RetryFailedRegistrationsJob` attempts retry (max 3 attempts)
5. After max retries, registration stays FAILED

### Common Failure Scenarios

- **Token Expired**: Auto-refresh handled by `OAuthService`
- **API Rate Limit**: Retry with exponential backoff
- **Invalid Track ID**: Permanent failure, needs manual correction
- **Platform API Down**: Retry until success or max attempts

## Performance Considerations

### Background Processing

- Campaigns checked hourly for release
- Registrations processed in batches
- Failed registrations retried every 6 hours
- Auto-refresh tokens prevent auth failures

### Scalability

- Unique constraint prevents duplicate registrations
- Indexes on frequently queried fields (status, campaign_id, user_id)
- CASCADE delete for data integrity
- Efficient batch processing in workers

## Testing

### Unit Tests

```bash
./vendor/bin/pest tests/Domain/PreSave
```

### Integration Tests

```bash
# Test complete flow
./vendor/bin/pest tests/Feature/PreSaveFlowTest.php
```

### Manual Testing

1. Create campaign in admin: `/admin/presave-campaigns/create`
2. Publish campaign: `/admin/presave-campaigns/{id}/publish`
3. Visit public page: `/presave/{id}`
4. Register via OAuth
5. Run worker manually:
```bash
php console.php schedule:run ProcessReleasedCampaignsJob
```
6. Check registration status

## Security Considerations

### OAuth Security

- Tokens stored encrypted in database
- Auto-refresh prevents token expiration
- State parameter prevents CSRF attacks
- Secure callback URL validation

### Input Validation

- Track URLs validated and sanitized
- Campaign dates validated
- Platform enum ensures only supported platforms
- Unique constraint prevents duplicate registrations

### Data Privacy

- User IDs from OAuth providers
- No personally identifiable information stored
- Registrations deleted on campaign deletion (CASCADE)
- Secure token storage and handling

## Framework Compliance

### Value Objects ✅

- All domain concepts as Value Objects
- No primitive obsession (no raw arrays or strings)
- Immutable with transformation methods

### Readonly Classes ✅

- All entities and VOs are `readonly`
- No mutable state
- New instances for changes

### No Inheritance ✅

- Composition over inheritance
- No `extends` used anywhere
- Interface-based contracts

### Explicit DI ✅

- Constructor injection only
- No global state or service locators
- Clear dependency graph

## Monitoring & Metrics

### Key Metrics

- Total campaigns created
- Active campaigns
- Total registrations
- Registrations by platform
- Success/failure rates
- Processing times

### Health Checks

```php
// Check system health
GET /admin/api/presave-campaigns/health

// Response
{
    "status": "healthy",
    "campaigns": {
        "total": 10,
        "active": 3,
        "scheduled": 5
    },
    "registrations": {
        "total": 1500,
        "pending": 200,
        "completed": 1250,
        "failed": 50
    },
    "workers": {
        "last_run": "2024-01-20 15:30:00",
        "status": "running"
    }
}
```

## Future Enhancements

### Planned Features

- [ ] Email notifications for successful pre-saves
- [ ] SMS notifications for release day
- [ ] Social sharing for campaigns
- [ ] Campaign analytics dashboard
- [ ] A/B testing for landing pages
- [ ] Multi-track album pre-saves
- [ ] Playlist pre-saves
- [ ] Fan engagement metrics
- [ ] Export campaign data

### Platform Expansion

- [ ] Complete Tidal integration
- [ ] Complete Apple Music integration
- [ ] Add Deezer support
- [ ] Add YouTube Music support
- [ ] Add SoundCloud support
- [ ] Add Bandcamp support

## Troubleshooting

### Registration Not Processing

**Symptom**: Registrations stay in PENDING status

**Solutions**:
1. Check if scheduled job is running: `php console.php schedule:list`
2. Check campaign status: Must be RELEASED
3. Verify OAuth token is valid: Auto-refresh should handle this
4. Check error logs for API failures

### OAuth Flow Failing

**Symptom**: Authorization redirect fails or callback errors

**Solutions**:
1. Verify OAuth credentials in environment
2. Check callback URL matches registered URL
3. Ensure HTTPS is enabled (required for OAuth)
4. Check provider API status

### Platform API Rate Limits

**Symptom**: Multiple FAILED registrations with rate limit errors

**Solutions**:
1. Reduce processing batch size
2. Increase delay between API calls
3. Implement exponential backoff
4. Contact platform for rate limit increase

## Support

For issues or questions:
- Check logs: `storage/logs/`
- Review error messages in admin interface
- Check background job status
- Contact development team

## License

Part of Custom PHP Framework - Internal Use Only
