Add-on-Panel zeigt freie Boost-Plaetze je Stufe
slotAvailability() liefert zusaetzlich free/limit (knappste Ausgabe, ohne den eigenen gehaltenen Slot); solange buchbar, steht unter den Kauf-Buttons "Noch X von Y Plaetzen frei." — bei laufendem Boost weiterhin nur der Verlaengerungs-Hinweis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
842d6a2bd0
commit
0cddf5fbdc
5 changed files with 65 additions and 12 deletions
|
|
@ -71,18 +71,17 @@ class BoostService
|
||||||
* Slot-Verfügbarkeit der Stufe für diese PM. Eine Verlängerung ist immer
|
* Slot-Verfügbarkeit der Stufe für diese PM. Eine Verlängerung ist immer
|
||||||
* verfügbar (die PM hält ihren Slot); sonst muss auf jeder Ausgabe, auf
|
* verfügbar (die PM hält ihren Slot); sonst muss auf jeder Ausgabe, auf
|
||||||
* der die PM erscheint (Portal × Sprache, Kategorie-Boost zusätzlich je
|
* der die PM erscheint (Portal × Sprache, Kategorie-Boost zusätzlich je
|
||||||
* Rubrik), ein Slot frei sein. `freeAt` nennt bei „ausgebucht" den
|
* Rubrik), ein Slot frei sein. `free` zählt die noch freien Plätze
|
||||||
* frühesten Zeitpunkt, zu dem wieder ein Platz frei wird.
|
* (knappste Ausgabe, ohne einen eigenen gehaltenen Slot); `freeAt` nennt
|
||||||
|
* bei „ausgebucht" den frühesten Zeitpunkt, zu dem ein Platz frei wird.
|
||||||
*
|
*
|
||||||
* @return array{available: bool, freeAt: Carbon|null}
|
* @return array{available: bool, free: int, limit: int, freeAt: Carbon|null}
|
||||||
*/
|
*/
|
||||||
public function slotAvailability(PressRelease $pressRelease, BoostPlacement $placement): array
|
public function slotAvailability(PressRelease $pressRelease, BoostPlacement $placement): array
|
||||||
{
|
{
|
||||||
if ($this->isBoosted($pressRelease, $placement)) {
|
|
||||||
return ['available' => true, 'freeAt' => null];
|
|
||||||
}
|
|
||||||
|
|
||||||
$limit = $this->slotLimit($placement);
|
$limit = $this->slotLimit($placement);
|
||||||
|
$holdsSlot = $this->isBoosted($pressRelease, $placement);
|
||||||
|
$free = $limit;
|
||||||
$freeAt = null;
|
$freeAt = null;
|
||||||
|
|
||||||
foreach ($this->affectedPortals($pressRelease) as $portal) {
|
foreach ($this->affectedPortals($pressRelease) as $portal) {
|
||||||
|
|
@ -106,7 +105,9 @@ class BoostService
|
||||||
->selectRaw('press_release_id, MAX(ends_at) AS held_until')
|
->selectRaw('press_release_id, MAX(ends_at) AS held_until')
|
||||||
->pluck('held_until');
|
->pluck('held_until');
|
||||||
|
|
||||||
if ($heldUntil->count() < $limit) {
|
$free = min($free, max(0, $limit - $heldUntil->count()));
|
||||||
|
|
||||||
|
if ($holdsSlot || $heldUntil->count() < $limit) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +118,12 @@ class BoostService
|
||||||
$freeAt = $freeAt === null ? $portalFreeAt : $freeAt->max($portalFreeAt);
|
$freeAt = $freeAt === null ? $portalFreeAt : $freeAt->max($portalFreeAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['available' => $freeAt === null, 'freeAt' => $freeAt];
|
return [
|
||||||
|
'available' => $holdsSlot || $free > 0,
|
||||||
|
'free' => $free,
|
||||||
|
'limit' => $limit,
|
||||||
|
'freeAt' => $freeAt,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function slotLimit(BoostPlacement $placement): int
|
public function slotLimit(BoostPlacement $placement): int
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ Die zwei offenen Punkte von Montag sind entschieden und umgesetzt.
|
||||||
Startseiten-Boosts füllen nur übrige Plätze).
|
Startseiten-Boosts füllen nur übrige Plätze).
|
||||||
- Kategorie-Boost invalidiert nur die Branchenseiten-Caches, nicht die
|
- Kategorie-Boost invalidiert nur die Branchenseiten-Caches, nicht die
|
||||||
Startseite.
|
Startseite.
|
||||||
- Kunden-UI (PM-Seite): zwei Boost-Angebote im Add-on-Panel; bei vollem
|
- Kunden-UI (PM-Seite): zwei Boost-Angebote im Add-on-Panel; solange buchbar,
|
||||||
|
zeigt jede Stufe „Noch X von Y Plätzen frei." (knappste Ausgabe); bei vollem
|
||||||
Inventar „Derzeit ausgebucht — der nächste Platz wird am TT.MM.JJJJ frei."
|
Inventar „Derzeit ausgebucht — der nächste Platz wird am TT.MM.JJJJ frei."
|
||||||
statt der Kauf-Buttons (serverseitig gegated).
|
statt der Kauf-Buttons (serverseitig gegated).
|
||||||
- Preisseite (`/preise`, beide Staffeln + Leistungsbeschreibungs-Note zu
|
- Preisseite (`/preise`, beide Staffeln + Leistungsbeschreibungs-Note zu
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
||||||
* fürs Add-on-Panel: Preise, laufende Platzierung, Slot-Verfügbarkeit
|
* fürs Add-on-Panel: Preise, laufende Platzierung, Slot-Verfügbarkeit
|
||||||
* („Derzeit ausgebucht — frei ab …", Entscheidung 13.07.).
|
* („Derzeit ausgebucht — frei ab …", Entscheidung 13.07.).
|
||||||
*
|
*
|
||||||
* @return array<string, array{label: string, description: string, options: array<int, int>, isBoosted: bool, activeUntil: \Illuminate\Support\Carbon|null, available: bool, freeAt: \Illuminate\Support\Carbon|null}>
|
* @return array<string, array{label: string, description: string, options: array<int, int>, isBoosted: bool, activeUntil: \Illuminate\Support\Carbon|null, available: bool, free: int, limit: int, freeAt: \Illuminate\Support\Carbon|null}>
|
||||||
*/
|
*/
|
||||||
private function boostAddOns(PressRelease $pr, BoostService $boostService): array
|
private function boostAddOns(PressRelease $pr, BoostService $boostService): array
|
||||||
{
|
{
|
||||||
|
|
@ -335,7 +335,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
||||||
// Slot-Abfrage nur, wenn die PM überhaupt boostbar ist.
|
// Slot-Abfrage nur, wenn die PM überhaupt boostbar ist.
|
||||||
$availability = $canBoost
|
$availability = $canBoost
|
||||||
? $boostService->slotAvailability($pr, $placement)
|
? $boostService->slotAvailability($pr, $placement)
|
||||||
: ['available' => false, 'freeAt' => null];
|
: ['available' => false, 'free' => 0, 'limit' => $boostService->slotLimit($placement), 'freeAt' => null];
|
||||||
|
|
||||||
$addOns[$placement->value] = [
|
$addOns[$placement->value] = [
|
||||||
'label' => $placement->label(),
|
'label' => $placement->label(),
|
||||||
|
|
@ -346,6 +346,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
||||||
'isBoosted' => $boostService->isBoosted($pr, $placement),
|
'isBoosted' => $boostService->isBoosted($pr, $placement),
|
||||||
'activeUntil' => $boostService->activeUntil($pr, $placement),
|
'activeUntil' => $boostService->activeUntil($pr, $placement),
|
||||||
'available' => $availability['available'],
|
'available' => $availability['available'],
|
||||||
|
'free' => $availability['free'],
|
||||||
|
'limit' => $availability['limit'],
|
||||||
'freeAt' => $availability['freeAt'],
|
'freeAt' => $availability['freeAt'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -735,6 +737,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
||||||
</div>
|
</div>
|
||||||
@if ($addOn['isBoosted'])
|
@if ($addOn['isBoosted'])
|
||||||
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}</p>
|
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}</p>
|
||||||
|
@else
|
||||||
|
{{-- Slot-Transparenz (13.07.): knappe Plätze sichtbar machen. --}}
|
||||||
|
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
|
||||||
|
{{ __('Noch :free von :limit Plätzen frei.', ['free' => $addOn['free'], 'limit' => $addOn['limit']]) }}
|
||||||
|
</p>
|
||||||
@endif
|
@endif
|
||||||
@elseif ($canBoost)
|
@elseif ($canBoost)
|
||||||
{{-- Slot-Inventar erschöpft: ehrlich ausweisen statt verkaufen (13.07.). --}}
|
{{-- Slot-Inventar erschöpft: ehrlich ausweisen statt verkaufen (13.07.). --}}
|
||||||
|
|
|
||||||
|
|
@ -169,9 +169,24 @@ test('a full homepage slot inventory rejects new home boosts with the next free
|
||||||
|
|
||||||
$availability = $this->service->slotAvailability($pr, BoostPlacement::Home);
|
$availability = $this->service->slotAvailability($pr, BoostPlacement::Home);
|
||||||
expect($availability['available'])->toBeFalse();
|
expect($availability['available'])->toBeFalse();
|
||||||
|
expect($availability['free'])->toBe(0);
|
||||||
|
expect($availability['limit'])->toBe(5);
|
||||||
expect($availability['freeAt']->timestamp)->toBe(now()->addDay()->timestamp);
|
expect($availability['freeAt']->timestamp)->toBe(now()->addDay()->timestamp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the availability reports the remaining free slots', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$pr = boostableRelease($user, ['portal' => Portal::Presseecho->value]);
|
||||||
|
|
||||||
|
expect($this->service->slotAvailability($pr, BoostPlacement::Home))
|
||||||
|
->toMatchArray(['available' => true, 'free' => 5, 'limit' => 5]);
|
||||||
|
|
||||||
|
occupySlots(2, BoostPlacement::Home, Portal::Presseecho);
|
||||||
|
|
||||||
|
expect($this->service->slotAvailability($pr, BoostPlacement::Home))
|
||||||
|
->toMatchArray(['available' => true, 'free' => 3, 'limit' => 5]);
|
||||||
|
});
|
||||||
|
|
||||||
test('an extension is allowed even when all slots are taken — the release keeps its slot', function () {
|
test('an extension is allowed even when all slots are taken — the release keeps its slot', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->wallet->credit($user, 50);
|
$this->wallet->credit($user, 50);
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,34 @@ test('a published green release shows the boost and proof add-ons', function ()
|
||||||
->assertSee('7 Tage für 12 Credits buchen')
|
->assertSee('7 Tage für 12 Credits buchen')
|
||||||
->assertSee('Kategorie-Boost')
|
->assertSee('Kategorie-Boost')
|
||||||
->assertSee('7 Tage für 6 Credits buchen')
|
->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('Veröffentlichungsnachweis')
|
||||||
->assertSee('Für 3 Credits kaufen');
|
->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 () {
|
test('add-on buttons only debit after the purchase confirmation modal', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
|
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue