service = app(BoostService::class); $this->wallet = app(CreditWalletService::class); }); function boostableRelease(User $user): PressRelease { return PressRelease::factory()->published()->create([ 'user_id' => $user->id, 'classification' => PressReleaseClassification::Green->value, ]); } test('only published green releases are boostable', function () { $user = User::factory()->create(); expect($this->service->canBoost(boostableRelease($user)))->toBeTrue(); $yellow = PressRelease::factory()->published()->create([ 'classification' => PressReleaseClassification::Yellow->value, ]); expect($this->service->canBoost($yellow))->toBeFalse(); $draftGreen = PressRelease::factory()->create([ 'status' => PressReleaseStatus::Draft->value, 'classification' => PressReleaseClassification::Green->value, ]); expect($this->service->canBoost($draftGreen))->toBeFalse(); }); test('booking a boost debits the wallet and activates the placement', function () { $user = User::factory()->create(); $this->wallet->credit($user, 20); $pr = boostableRelease($user); $boost = $this->service->boost($user, $pr, 14); expect($boost->days)->toBe(14); expect($boost->credits_charged)->toBe(20); expect($this->wallet->balance($user))->toBe(0); expect($this->service->isBoosted($pr))->toBeTrue(); expect($pr->fresh()->isBoosted())->toBeTrue(); }); test('boosting a non-green release is rejected at the gate', function () { $user = User::factory()->create(); $this->wallet->credit($user, 50); $red = PressRelease::factory()->published()->create([ 'classification' => PressReleaseClassification::Red->value, ]); expect(fn () => $this->service->boost($user, $red, 7)) ->toThrow(BoostNotAllowedException::class); expect($this->wallet->balance($user))->toBe(50); }); test('an unaffordable boost throws and creates no boost record', function () { $user = User::factory()->create(); $this->wallet->credit($user, 5); $pr = boostableRelease($user); expect(fn () => $this->service->boost($user, $pr, 7)) ->toThrow(InsufficientCreditsException::class); expect($pr->boosts()->count())->toBe(0); expect($this->wallet->balance($user))->toBe(5); }); test('an unknown duration is rejected', function () { $user = User::factory()->create(); $this->wallet->credit($user, 50); $pr = boostableRelease($user); expect(fn () => $this->service->boost($user, $pr, 99)) ->toThrow(InvalidArgumentException::class); }); test('a second boost extends from the running end instead of overlapping', function () { $user = User::factory()->create(); $this->wallet->credit($user, 100); $pr = boostableRelease($user); $first = $this->service->boost($user, $pr, 7); $second = $this->service->boost($user, $pr, 14); expect($second->starts_at->timestamp)->toBe($first->ends_at->timestamp); expect($second->ends_at->timestamp)->toBe($first->ends_at->copy()->addDays(14)->timestamp); });