presseportale/tests/Feature/PressReleaseShowAddonsTest.php
Kevin Adametz 4f2be1e0a8 PM-Uebersicht: Rubrik unter Portal, Boost-moeglich-Hinweis, gruener Haken
- Rubrik (lokalisiert) direkt unter den Portal-Pills; Kategorie wird in
  accessiblePageFor() eager geladen (kein N+1)
- Status-Spalte: laeuft kein Boost, wirbt eine veroeffentlichte gruene PM
  mit "Boost moeglich ->" (Link zur PM-Seite mit den Add-ons)
- Gruen geprueft: kleiner gruener Haken neben dem Titel (Tooltip
  "Inhaltspruefung: Gruen")

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:23:07 +00:00

366 lines
14 KiB
PHP

<?php
use App\Enums\BoostPlacement;
use App\Enums\PressReleaseClassification;
use App\Models\Boost;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Billing\CreditWalletService;
use App\Services\Billing\ProofPdfService;
use App\Services\PressRelease\BoostService;
use Database\Seeders\RolesAndPermissionsSeeder;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$this->wallet = app(CreditWalletService::class);
});
/**
* @return array{customer: User, pr: PressRelease}
*/
function publishedGreenPrForOwner(): array
{
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$company = Company::factory()->presseecho()->create();
$customer->companies()->attach($company->id, ['role' => 'owner']);
$pr = PressRelease::factory()->published()->create([
'user_id' => $customer->id,
'company_id' => $company->id,
'category_id' => Category::factory()->create()->id,
'portal' => $company->portal->value,
'classification' => PressReleaseClassification::Green->value,
]);
return compact('customer', 'pr');
}
test('a published green release shows the boost and proof add-ons', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Add-ons')
->assertSee('Startseiten-Boost')
->assertSee('7 Tage für 12 Credits buchen')
->assertSee('Kategorie-Boost')
->assertSee('7 Tage für 6 Credits buchen')
// Slot-Transparenz: freie Plätze je Stufe (5 Startseite, 3 Branchenseite).
->assertSee('Noch 5 von 5 Plätzen frei.')
->assertSee('Noch 3 von 3 Plätzen frei.')
->assertSee('Veröffentlichungsnachweis')
->assertSee('Für 3 Credits kaufen');
});
test('the slot hint counts down when competitors occupy places', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
// 2 der 5 Startseiten-Slots der Ausgabe sind belegt.
foreach (range(1, 2) as $i) {
$other = PressRelease::factory()->published()->create([
'portal' => $pr->portal->value,
'classification' => PressReleaseClassification::Green->value,
]);
Boost::factory()->for($other)->create();
}
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Noch 3 von 5 Plätzen frei.')
->assertSee('Noch 3 von 3 Plätzen frei.');
});
test('add-on buttons only debit after the purchase confirmation modal', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
$this->actingAs($customer);
$component = LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('confirmBoost', 14)
->assertSet('pendingPurchase', [
'type' => 'boost',
'placement' => 'home',
'label' => 'Startseiten-Boost — 14 Tage',
'credits' => 20,
'days' => 14,
'extendsUntil' => null,
'newEndsAt' => null,
])
->assertSee('Kauf bestätigen')
->assertSee('Kostenpflichtig buchen');
// Der Klick auf die Laufzeit selbst bucht noch nichts ab.
expect($customer->fresh()->creditBalance())->toBe(40);
expect($pr->fresh()->isBoosted())->toBeFalse();
// Ohne den Pflicht-Schalter bleibt der Kauf aus und das Modal offen.
$component->call('buyPendingAddOn')
->assertSet('pendingPurchase.type', 'boost');
expect($customer->fresh()->creditBalance())->toBe(40);
expect($pr->fresh()->isBoosted())->toBeFalse();
$component->set('purchaseConfirmed', true)
->call('buyPendingAddOn')
->assertSet('pendingPurchase', null)
->assertSet('purchaseConfirmed', false);
expect($customer->fresh()->creditBalance())->toBe(20);
expect($pr->fresh()->isBoosted())->toBeTrue();
});
test('the confirmation modal warns when the purchase extends a running boost', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
$activeUntil = now()->addDays(3)->setTime(12, 0);
Boost::factory()->for($pr)->for($customer)->create(['ends_at' => $activeUntil]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('confirmBoost', 7)
->assertSet('pendingPurchase.extendsUntil', $activeUntil->format('d.m.Y H:i'))
->assertSet('pendingPurchase.newEndsAt', $activeUntil->copy()->addDays(7)->format('d.m.Y H:i'))
->assertSee('Diese Buchung verlängert den laufenden Boost.');
});
test('the proof purchase runs through the confirmation modal as well', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 10);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('confirmProof')
->assertSet('pendingPurchase.type', 'proof')
->assertSet('pendingPurchase.credits', 3)
->set('purchaseConfirmed', true)
->call('buyPendingAddOn')
->assertSee('Nachweis herunterladen');
expect(app(ProofPdfService::class)->hasPurchased($customer->fresh(), $pr))->toBeTrue();
expect($customer->fresh()->creditBalance())->toBe(7);
});
test('an unknown boost duration does not open the confirmation modal', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('confirmBoost', 999)
->assertSet('pendingPurchase', null)
->call('buyPendingAddOn');
expect($customer->fresh()->creditBalance())->toBe(40); // nichts abgebucht
});
test('booking a boost from the detail page debits the wallet and activates it', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('buyBoost', 14);
expect($customer->fresh()->creditBalance())->toBe(20); // 40 - 20 (14 Tage)
expect($pr->fresh()->isBoosted())->toBeTrue();
});
test('a category boost books through the confirmation modal at the half staffel', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 10);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('confirmBoost', 7, 'category')
->assertSet('pendingPurchase.placement', 'category')
->assertSet('pendingPurchase.label', 'Kategorie-Boost — 7 Tage')
->assertSet('pendingPurchase.credits', 6)
->set('purchaseConfirmed', true)
->call('buyPendingAddOn');
expect($customer->fresh()->creditBalance())->toBe(4); // 10 - 6 (7 Tage, halber Satz)
expect(app(BoostService::class)->isBoosted($pr->fresh(), BoostPlacement::Category))->toBeTrue();
expect(app(BoostService::class)->isBoosted($pr->fresh(), BoostPlacement::Home))->toBeFalse();
});
test('sold-out slots replace the buy buttons with the next free date', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
// Alle 5 Startseiten-Slots der Ausgabe belegt (fremde PMs, gleiche Ausgabe).
$freeAt = now()->addDays(2);
foreach (range(1, 5) as $i) {
$other = PressRelease::factory()->published()->create([
'portal' => $pr->portal->value,
'classification' => PressReleaseClassification::Green->value,
]);
Boost::factory()->for($other)->create(['ends_at' => $freeAt->copy()->addDays($i - 1)]);
}
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Derzeit ausgebucht — der nächste Platz wird am '.$freeAt->format('d.m.Y').' frei.')
->assertDontSee('7 Tage für 12 Credits buchen')
// Der Kategorie-Boost hat ein eigenes Slot-Inventar und bleibt buchbar.
->assertSee('7 Tage für 6 Credits buchen');
// Serverseitiges Gate: ein direkter Kauf-Call bucht trotzdem nicht.
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('buyBoost', 7);
expect($customer->fresh()->creditBalance())->toBe(40);
expect($pr->fresh()->isBoosted())->toBeFalse();
});
test('buying a proof from the detail page unlocks the download', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 10);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('buyProof')
->assertSee('Nachweis herunterladen');
expect(app(ProofPdfService::class)->hasPurchased($customer->fresh(), $pr))->toBeTrue();
expect($customer->fresh()->creditBalance())->toBe(7);
});
test('an add-on purchase without enough credits opens the shortfall modal with a topup link', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 1);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('buyProof')
->assertSet('creditShortfall', [
'label' => 'Der Veröffentlichungsnachweis',
'required' => 3,
'available' => 1,
])
->assertSee('Nicht genügend Credits')
->assertSee('Es fehlen 2 Credits.')
->assertSee('Credits aufladen')
->assertSee(route('me.bookings.index').'#credits', escape: false);
expect($customer->fresh()->creditBalance())->toBe(1); // nichts abgebucht
});
test('a boost without enough credits opens the shortfall modal instead of booking', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->call('buyBoost', 7)
->assertSet('creditShortfall.label', 'Der Boost')
->assertSee('Nicht genügend Credits');
expect($pr->fresh()->isBoosted())->toBeFalse();
});
test('the press release list shows a hint with end date for an active boost', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->wallet->credit($customer, 40);
$this->actingAs($customer);
// Ohne laufenden Boost wirbt die grüne, veröffentlichte PM für das Add-on.
LivewireVolt::test('customer.press-releases.index')
->assertDontSee('Boost bis')
->assertSee('Boost möglich');
Boost::factory()->for($pr)->create([
'ends_at' => now()->addDays(7)->setTime(9, 0),
]);
LivewireVolt::test('customer.press-releases.index')
->assertSee('Boost bis '.now()->addDays(7)->format('d.m.Y'))
->assertDontSee('Boost möglich');
});
test('the list shows the category under the portal and a green check for green releases', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$pr->category->translations()->create(['locale' => 'de', 'name' => 'Energie & Klima', 'slug' => 'energie-klima']);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.index')
->assertSee('Energie & Klima')
->assertSee('Inhaltsprüfung: Grün');
});
test('a non-green release shows neither the green check nor the boost hint', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$pr->update(['classification' => PressReleaseClassification::Yellow->value]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.index')
->assertDontSee('Inhaltsprüfung: Grün')
->assertDontSee('Boost möglich');
});
test('an expired boost shows no hint in the press release list', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$this->actingAs($customer);
Boost::factory()->for($pr)->expired()->create();
LivewireVolt::test('customer.press-releases.index')
->assertDontSee('Boost bis');
});
test('a non-green release cannot be boosted from the detail page', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
$pr->update(['classification' => PressReleaseClassification::Yellow->value]);
$this->wallet->credit($customer, 40);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Nur grün geprüfte Pressemitteilungen sind boostbar')
->call('buyBoost', 7);
expect($customer->fresh()->creditBalance())->toBe(40); // nichts abgebucht
expect($pr->fresh()->isBoosted())->toBeFalse();
});