*/ private array $campaigns = []; private int $nextId = 1; public function save(PreSaveCampaign $campaign): PreSaveCampaign { // If campaign has no ID (new), use a wrapper to create with ID if ($campaign->id === null) { $campaign = $this->assignId($campaign); } $this->campaigns[$campaign->id] = $campaign; return $campaign; } private function assignId(PreSaveCampaign $campaign): PreSaveCampaign { $id = $this->nextId++; return new PreSaveCampaign( id: $id, title: $campaign->title, artistName: $campaign->artistName, coverImageUrl: $campaign->coverImageUrl, releaseDate: $campaign->releaseDate, trackUrls: $campaign->trackUrls, status: $campaign->status, createdAt: $campaign->createdAt, updatedAt: Timestamp::now(), description: $campaign->description, startDate: $campaign->startDate ); } public function findById(int $id): ?PreSaveCampaign { return $this->campaigns[$id] ?? null; } /** @return array */ public function findAll(array $filters = []): array { return array_values($this->campaigns); } /** @return array */ public function findReadyForRelease(): array { $now = Timestamp::now(); return array_filter( $this->campaigns, function (PreSaveCampaign $c) use ($now) { return $c->status->equals(CampaignStatus::ACTIVE) && $c->releaseDate->isBefore($now); } ); } /** @return array */ public function findReadyForProcessing(): array { return array_filter( $this->campaigns, fn(PreSaveCampaign $c) => $c->status->equals(CampaignStatus::RELEASED) ); } public function delete(int $id): bool { if (isset($this->campaigns[$id])) { unset($this->campaigns[$id]); return true; } return false; } /** @return array */ public function getStatistics(int $campaignId): array { return [ 'total_registrations' => 0, 'completed' => 0, 'pending' => 0, 'failed' => 0, ]; } public function clear(): void { $this->campaigns = []; $this->nextId = 1; } } class InMemoryPreSaveRegistrationRepository implements PreSaveRegistrationRepositoryInterface { /** @var array */ private array $registrations = []; private int $nextId = 1; public function save(PreSaveRegistration $registration): PreSaveRegistration { if ($registration->id === null) { $registration = $this->assignId($registration); } $this->registrations[$registration->id] = $registration; return $registration; } private function assignId(PreSaveRegistration $registration): PreSaveRegistration { $id = $this->nextId++; return new PreSaveRegistration( id: $id, campaignId: $registration->campaignId, userId: $registration->userId, platform: $registration->platform, status: $registration->status, registeredAt: $registration->registeredAt, processedAt: $registration->processedAt, errorMessage: $registration->errorMessage, retryCount: $registration->retryCount ); } public function findForUserAndCampaign( string $userId, int $campaignId, StreamingPlatform $platform ): ?PreSaveRegistration { foreach ($this->registrations as $registration) { if ($registration->userId === $userId && $registration->campaignId === $campaignId && $registration->platform->equals($platform) ) { return $registration; } } return null; } /** @return array */ public function findByUserId(string $userId): array { return array_values(array_filter( $this->registrations, fn(PreSaveRegistration $r) => $r->userId === $userId )); } /** @return array */ public function findPendingByCampaign(int $campaignId): array { return array_values(array_filter( $this->registrations, function (PreSaveRegistration $r) use ($campaignId) { return $r->campaignId === $campaignId && $r->status->equals(RegistrationStatus::PENDING); } )); } /** @return array */ public function findRetryable(int $campaignId, int $maxRetries): array { return array_values(array_filter( $this->registrations, function (PreSaveRegistration $r) use ($campaignId, $maxRetries) { return $r->campaignId === $campaignId && $r->status->equals(RegistrationStatus::FAILED) && $r->retryCount < $maxRetries; } )); } /** @return array */ public function getStatusCounts(int $campaignId): array { $registrations = array_filter( $this->registrations, fn(PreSaveRegistration $r) => $r->campaignId === $campaignId ); $counts = [ 'pending' => 0, 'completed' => 0, 'failed' => 0, ]; foreach ($registrations as $registration) { if ($registration->status->equals(RegistrationStatus::PENDING)) { $counts['pending']++; } elseif ($registration->status->equals(RegistrationStatus::COMPLETED)) { $counts['completed']++; } elseif ($registration->status->equals(RegistrationStatus::FAILED)) { $counts['failed']++; } } return $counts; } public function delete(int $id): bool { if (isset($this->registrations[$id])) { unset($this->registrations[$id]); return true; } return false; } public function clear(): void { $this->registrations = []; $this->nextId = 1; } } describe('PreSaveCampaignRepository', function () { beforeEach(function () { $this->repository = new InMemoryPreSaveCampaignRepository(); }); afterEach(function () { $this->repository->clear(); }); it('saves and finds campaigns by id', function () { $releaseDate = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15 00:00:00')); $campaign = PreSaveCampaign::create( title: 'Test Album', artistName: 'Test Artist', coverImageUrl: 'https://example.com/cover.jpg', releaseDate: $releaseDate, trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:123')] ); $saved = $this->repository->save($campaign); expect($saved->id)->not->toBeNull(); $found = $this->repository->findById($saved->id); expect($found)->not->toBeNull(); expect($found->id)->toBe($saved->id); expect($found->title)->toBe('Test Album'); expect($found->artistName)->toBe('Test Artist'); }); it('returns null when campaign not found', function () { $found = $this->repository->findById(999); expect($found)->toBeNull(); }); it('finds all campaigns', function () { $releaseDate1 = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15')); $releaseDate2 = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-20')); $campaign1 = PreSaveCampaign::create( title: 'Album 1', artistName: 'Artist 1', coverImageUrl: 'https://example.com/cover1.jpg', releaseDate: $releaseDate1, trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:1')] ); $campaign2 = PreSaveCampaign::create( title: 'Album 2', artistName: 'Artist 2', coverImageUrl: 'https://example.com/cover2.jpg', releaseDate: $releaseDate2, trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:2')] ); $this->repository->save($campaign1); $this->repository->save($campaign2); $all = $this->repository->findAll(); expect($all)->toHaveCount(2); expect($all[0]->title)->toBe('Album 1'); expect($all[1]->title)->toBe('Album 2'); }); it('updates existing campaigns', function () { $releaseDate = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15')); $campaign = PreSaveCampaign::create( title: 'Original Title', artistName: 'Artist', coverImageUrl: 'https://example.com/cover.jpg', releaseDate: $releaseDate, trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:1')] ); $saved = $this->repository->save($campaign); $updated = $saved->markAsReleased(); $this->repository->save($updated); $found = $this->repository->findById($saved->id); expect($found->status->equals(CampaignStatus::RELEASED))->toBeTrue(); }); it('deletes campaigns', function () { $releaseDate = Timestamp::fromDateTime(new DateTimeImmutable('2025-01-15')); $campaign = PreSaveCampaign::create( title: 'Test Album', artistName: 'Test Artist', coverImageUrl: 'https://example.com/cover.jpg', releaseDate: $releaseDate, trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:123')] ); $saved = $this->repository->save($campaign); expect($this->repository->findById($saved->id))->not->toBeNull(); $deleted = $this->repository->delete($saved->id); expect($deleted)->toBeTrue(); expect($this->repository->findById($saved->id))->toBeNull(); }); }); describe('PreSaveRegistrationRepository', function () { beforeEach(function () { $this->repository = new InMemoryPreSaveRegistrationRepository(); }); afterEach(function () { $this->repository->clear(); }); it('saves and finds registrations', function () { $registration = PreSaveRegistration::create( campaignId: 1, userId: 'user_456', platform: StreamingPlatform::SPOTIFY ); $saved = $this->repository->save($registration); expect($saved->id)->not->toBeNull(); expect($saved->campaignId)->toBe(1); expect($saved->userId)->toBe('user_456'); expect($saved->platform->equals(StreamingPlatform::SPOTIFY))->toBeTrue(); }); it('finds registrations for user and campaign', function () { $reg1 = PreSaveRegistration::create( campaignId: 1, userId: 'user_1', platform: StreamingPlatform::SPOTIFY ); $reg2 = PreSaveRegistration::create( campaignId: 2, userId: 'user_1', platform: StreamingPlatform::SPOTIFY ); $saved1 = $this->repository->save($reg1); $this->repository->save($reg2); $found = $this->repository->findForUserAndCampaign( 'user_1', 1, StreamingPlatform::SPOTIFY ); expect($found)->not->toBeNull(); expect($found->id)->toBe($saved1->id); expect($found->campaignId)->toBe(1); }); it('finds registrations by user id', function () { $reg1 = PreSaveRegistration::create( campaignId: 1, userId: 'user_123', platform: StreamingPlatform::SPOTIFY ); $reg2 = PreSaveRegistration::create( campaignId: 2, userId: 'user_123', platform: StreamingPlatform::SPOTIFY ); $reg3 = PreSaveRegistration::create( campaignId: 1, userId: 'user_456', platform: StreamingPlatform::SPOTIFY ); $this->repository->save($reg1); $this->repository->save($reg2); $this->repository->save($reg3); $userRegistrations = $this->repository->findByUserId('user_123'); expect($userRegistrations)->toHaveCount(2); expect($userRegistrations[0]->campaignId)->toBe(1); expect($userRegistrations[1]->campaignId)->toBe(2); }); it('finds pending registrations by campaign', function () { $pending = PreSaveRegistration::create( campaignId: 1, userId: 'user_1', platform: StreamingPlatform::SPOTIFY ); $completed = PreSaveRegistration::create( campaignId: 1, userId: 'user_2', platform: StreamingPlatform::SPOTIFY ); $savedCompleted = $this->repository->save($completed); $completed = $savedCompleted->markAsCompleted(); $this->repository->save($completed); $savedPending = $this->repository->save($pending); $pendingRegistrations = $this->repository->findPendingByCampaign(1); expect($pendingRegistrations)->toHaveCount(1); expect($pendingRegistrations[0]->id)->toBe($savedPending->id); expect($pendingRegistrations[0]->status->equals(RegistrationStatus::PENDING))->toBeTrue(); }); it('finds retryable registrations', function () { $retryable = PreSaveRegistration::create( campaignId: 1, userId: 'user_1', platform: StreamingPlatform::SPOTIFY ); $savedRetryable = $this->repository->save($retryable); $retryable = $savedRetryable->markAsFailed('First failure'); $this->repository->save($retryable); $maxedOut = PreSaveRegistration::create( campaignId: 1, userId: 'user_2', platform: StreamingPlatform::SPOTIFY ); $savedMaxedOut = $this->repository->save($maxedOut); $maxedOut = $savedMaxedOut->markAsFailed('Failure 1'); $maxedOut = $this->repository->save($maxedOut); $maxedOut = $maxedOut->resetForRetry()->markAsFailed('Failure 2'); $maxedOut = $this->repository->save($maxedOut); $maxedOut = $maxedOut->resetForRetry()->markAsFailed('Failure 3'); $this->repository->save($maxedOut); $retryableList = $this->repository->findRetryable(1, 3); expect($retryableList)->toHaveCount(1); expect($retryableList[0]->id)->toBe($savedRetryable->id); expect($retryableList[0]->retryCount)->toBe(0); }); it('deletes registrations', function () { $registration = PreSaveRegistration::create( campaignId: 1, userId: 'user_456', platform: StreamingPlatform::SPOTIFY ); $saved = $this->repository->save($registration); $deleted = $this->repository->delete($saved->id); expect($deleted)->toBeTrue(); }); it('updates existing registrations', function () { $registration = PreSaveRegistration::create( campaignId: 1, userId: 'user_456', platform: StreamingPlatform::SPOTIFY ); $saved = $this->repository->save($registration); $updated = $saved->markAsCompleted(); $this->repository->save($updated); $found = $this->repository->findForUserAndCampaign( 'user_456', 1, StreamingPlatform::SPOTIFY ); expect($found->status->equals(RegistrationStatus::COMPLETED))->toBeTrue(); }); });