seed(RolesAndPermissionsSeeder::class); $this->wallet = app(CreditWalletService::class); $this->proof = app(ProofPdfService::class); $this->extraPm = app(ExtraPmPurchaseService::class); }); function customerWithRole(): User { $user = User::factory()->create(['is_active' => true, 'email_verified_at' => now()]); $user->assignRole('customer'); return $user; } // ---------------------------------------------------------------- Proof-PDF test('buying a proof debits three credits and records a paid purchase', function () { $user = customerWithRole(); $this->wallet->credit($user, 10); $pr = PressRelease::factory()->published()->create(['user_id' => $user->id]); $purchase = $this->proof->purchase($user, $pr); expect($purchase->type)->toBe(SinglePurchaseType::ProofPdf); expect($purchase->status)->toBe(SinglePurchaseStatus::Paid); expect($this->wallet->balance($user))->toBe(7); expect($this->proof->hasPurchased($user, $pr))->toBeTrue(); }); test('re-buying the same proof does not charge again', function () { $user = customerWithRole(); $this->wallet->credit($user, 10); $pr = PressRelease::factory()->published()->create(['user_id' => $user->id]); $first = $this->proof->purchase($user, $pr); $second = $this->proof->purchase($user, $pr); expect($second->id)->toBe($first->id); expect($this->wallet->balance($user))->toBe(7); expect($user->singlePurchases()->where('type', SinglePurchaseType::ProofPdf->value)->count())->toBe(1); }); test('a proof for an unpublished release is rejected', function () { $user = customerWithRole(); $this->wallet->credit($user, 10); $draft = PressRelease::factory()->create(['user_id' => $user->id, 'status' => PressReleaseStatus::Draft->value]); expect(fn () => $this->proof->purchase($user, $draft)) ->toThrow(ProofPdfNotAvailableException::class); expect($this->wallet->balance($user))->toBe(10); }); test('a proof without enough credits throws and records nothing', function () { $user = customerWithRole(); $this->wallet->credit($user, 2); $pr = PressRelease::factory()->published()->create(['user_id' => $user->id]); expect(fn () => $this->proof->purchase($user, $pr)) ->toThrow(InsufficientCreditsException::class); expect($user->singlePurchases()->count())->toBe(0); }); test('the rendered proof is a non-trivial pdf document', function () { $pr = PressRelease::factory()->published()->create(['title' => 'Testmeldung', 'text' => str_repeat('Inhalt. ', 50)]); $pdf = app(ProofPdfRenderer::class)->render($pr); expect($pdf)->toStartWith('%PDF-1.4'); expect($pdf)->toContain('%%EOF'); expect(strlen($pdf))->toBeGreaterThan(800); }); test('the rendered proof contains the full release, boilerplate, press contact and publisher', function () { $company = Company::factory()->create([ 'name' => 'Musterfirma GmbH', 'address' => 'Beispielweg 1, 10115 Berlin', 'boilerplate' => 'Die Musterfirma ist ein Beispielunternehmen mit Standardabbinder.', 'website' => 'https://musterfirma.example', ]); $contact = Contact::factory()->create([ 'company_id' => $company->id, 'first_name' => 'Max', 'last_name' => 'Mustermann', 'responsibility' => 'Pressesprecher', 'email' => 'max@musterfirma.example', 'phone' => '030 1234567', ]); $pr = PressRelease::factory()->published()->create([ 'company_id' => $company->id, 'title' => 'Grosse Ankuendigung', 'subtitle' => 'Ein aussagekraeftiger Untertitel', 'text' => '

Erster Absatz der Meldung.

Zweiter Absatz mit Schlusswort.

', ]); $pr->contacts()->attach($contact->id); $pdf = app(ProofPdfRenderer::class)->render($pr); expect($pdf) ->toContain('Grosse Ankuendigung') ->toContain('Untertitel') ->toContain('Erster Absatz der Meldung.') ->toContain('Schlusswort.') ->toContain('Standardabbinder.') ->toContain('Mustermann') ->toContain('Pressesprecher') ->toContain('max@musterfirma.example') ->toContain('Musterfirma GmbH') ->toContain('10115 Berlin') ->toContain('https://musterfirma.example'); }); test('a long release flows over multiple numbered pages', function () { $pr = PressRelease::factory()->published()->create([ 'title' => 'Lange Meldung', 'text' => implode('', array_map( static fn (int $i): string => '

Absatz '.$i.': '.str_repeat('Dies ist ein ausfuehrlicher Beispielsatz. ', 12).'

', range(1, 12), )), ]); $pdf = app(ProofPdfRenderer::class)->render($pr); expect(substr_count($pdf, '/Type /Page /Parent'))->toBeGreaterThanOrEqual(2); expect($pdf)->toContain('Seite 1 von'); expect($pdf)->toContain('Fortsetzung'); }); test('the rendered proof documents the real public url of the release', function () { $pr = PressRelease::factory()->published()->create([ 'portal' => Portal::Presseecho, 'slug' => 'nachweis-pm', 'language' => 'de', ]); $pdf = app(ProofPdfRenderer::class)->render($pr); // Vorher fehlte das /de/press-release/-Segment — die URL im Nachweis lief ins Leere. expect($pdf)->toContain(config('domains.domain_presseecho_url').'/de/press-release/nachweis-pm'); }); test('the proof download requires a prior purchase', function () { $user = customerWithRole(); $pr = PressRelease::factory()->published()->create(['user_id' => $user->id]); // Ohne Kauf: 403. $this->actingAs($user) ->get(route('me.press-releases.proof', $pr->id)) ->assertForbidden(); // Nach Kauf: PDF-Download. $this->wallet->credit($user, 5); $this->proof->purchase($user, $pr); $this->actingAs($user) ->get(route('me.press-releases.proof', $pr->id)) ->assertOk() ->assertHeader('content-type', 'application/pdf'); }); // ---------------------------------------------------------------- Extra-PM test('an Einzel user pays the full extra-pm rate from the wallet', function () { $user = customerWithRole(); $this->wallet->credit($user, 25); $purchase = $this->extraPm->purchase($user); expect($purchase->type)->toBe(SinglePurchaseType::ExtraPm); expect($purchase->status)->toBe(SinglePurchaseStatus::Paid); expect($purchase->price_cents)->toBe(1900); expect($this->wallet->balance($user))->toBe(6); }); test('an extra-pm purchase extends the press release quota', function () { config()->set('billing.enforce_booking', true); $user = customerWithRole(); $plan = Plan::factory()->create([ 'slug' => 'business', 'press_release_quota' => 3, 'stripe_price_id_monthly' => 'price_biz_'.fake()->unique()->randomNumber(6), ]); subscribeUserToPlan($user, $plan); $user->update(['press_release_quota_used_this_month' => 3]); // Kontingent voll $this->wallet->credit($user, 20); expect($user->pressReleaseQuotaRemaining())->toBe(0); // Business-Tier zahlt 12 Credits für die Extra-PM. $this->extraPm->purchase($user); expect($this->wallet->balance($user))->toBe(8); expect($user->fresh()->pressReleaseQuotaRemaining())->toBe(1); }); test('an extra-pm without enough credits throws the mini-checkout signal', function () { $user = customerWithRole(); $this->wallet->credit($user, 8); try { $this->extraPm->purchase($user); $this->fail('Expected InsufficientCreditsException.'); } catch (InsufficientCreditsException $e) { expect($e->required)->toBe(19); expect($e->available)->toBe(8); expect($e->shortfall())->toBe(11); } expect($user->singlePurchases()->count())->toBe(0); });