presseportale/app/Services/Web/PricingTransparencyData.php
Kevin Adametz 106edb1384 web: Transparenzseiten — was kostet, ohne Abo, loeschen/aendern (Roadmap 2.4)
Drei Seiten mit unterschiedlicher Suchintention, DE/EN, Hub + Spokes:
- /was-kostet-eine-pressemitteilung: kurze Antwort im ersten Bildschirm,
  vier Abrechnungsmodelle ohne Anbieternamen und fremde Preise, sechs
  Prueffragen fuer Folgekosten, "Was bei uns gilt" (sieben Zusagen mit
  Einloesung), Rechenbeispiel Einzelabrechnung gegen Starter-Monate mit
  Zeilen fuer beide Seiten.
- /pressemitteilung-veroeffentlichen-ohne-abo: Ablauf, enthalten / nicht
  enthalten, nichts verfaellt, wann sich ein Tarif lohnt.
- /pressemitteilung-loeschen-oder-aendern-lassen: fuenf Wege als
  Tabelle (Korrektur, Aenderung, DSGVO, Depublikation, DSA), Antragsweg
  ueber /melden/pm, Korrektur statt Loeschung, Absatz fuer Meldungen auf
  fremden Portalen ohne Zusagen ueber Dritte.

PricingTransparencyData liefert alle Zahlen aus config/billing, credits,
legal und dem Starter-Tarif; TransparencyPageController leitet die Marke
aus dem Host ab. Tabellen-Typografie fuer .pm-body. Verlinkt aus
Preisuebersicht, Hilfe-Center, Sitemaps und llms.txt.

Test TransparencyPagesTest (9) erzwingt u. a., dass im Rechenbeispiel
beide Modelle Zeilen gewinnen. Protokoll Teil 5, Roadmap 2.4 abgehakt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:00:28 +00:00

87 lines
3.6 KiB
PHP

<?php
namespace App\Services\Web;
use App\Enums\Tier;
use App\Models\Plan;
/**
* Zahlen für die Transparenzseiten (Roadmap 2.4): alles aus config/billing,
* config/credits und dem Starter-Tarif, damit die Seiten nie einen
* anderen Preis nennen als die Preisübersicht. Das Rechenbeispiel stellt
* Einzelabrechnung und einen bei Bedarf gebuchten Starter-Monat gegenüber —
* bewusst mit Zeilen, in denen die Einzelabrechnung gewinnt.
*/
class PricingTransparencyData
{
/**
* @return array<string, mixed>
*/
public function build(): array
{
$singleCents = (int) config('billing.single_pm_price_cents');
$starter = Plan::query()->active()->where('slug', Tier::Starter->value)->first();
$starterMonthlyCents = (int) ($starter->monthly_price_cents ?? 0);
$starterQuota = (int) ($starter->press_release_quota ?? 0);
$vatRate = (float) config('billing.vat_rate');
return [
'singleCents' => $singleCents,
'singleNet' => $this->euro($singleCents),
'singleGross' => $this->euro((int) round($singleCents * (1 + $vatRate))),
'vatPercent' => (int) round($vatRate * 100),
'starterName' => $starter->name ?? 'Starter',
'starterMonthlyNet' => $this->euro($starterMonthlyCents),
'starterYearlyNet' => $this->euro((int) ($starter->yearly_price_cents ?? 0)),
'starterQuota' => $starterQuota,
'extraPmStarterNet' => $this->euro((int) config('credits.extra_pm.'.Tier::Starter->value, 0) * 100),
'changeNet' => $this->euro((int) config('billing.change_price_cents')),
'depublicationNet' => $this->euro((int) config('billing.depublication_price_cents')),
'correctionMaxPercent' => (int) config('legal.correction_max_distance_percent'),
'examples' => $starterMonthlyCents > 0 && $starterQuota > 0
? $this->examples($singleCents, $starterMonthlyCents, $starterQuota)
: [],
];
}
/**
* Rechenbeispiel: Meldungen im Jahr, Verteilung, Kosten je Modell.
* Der Starter-Monat wird nur in Monaten gebucht, in denen veröffentlicht
* wird (monatlich kündbar); je Monat höchstens das Kontingent.
*
* @return list<array{count: int, spread: string, months: int, single: string, starter: string, cheaper: string}>
*/
private function examples(int $singleCents, int $starterMonthlyCents, int $starterQuota): array
{
$cases = [
['count' => 1, 'months' => 1, 'spread' => 'one'],
['count' => $starterQuota, 'months' => 1, 'spread' => 'same_month'],
['count' => $starterQuota, 'months' => $starterQuota, 'spread' => 'spread'],
['count' => 12, 'months' => 12, 'spread' => 'monthly'],
['count' => 12, 'months' => (int) ceil(12 / $starterQuota), 'spread' => 'clustered'],
];
$rows = [];
foreach ($cases as $case) {
$single = $case['count'] * $singleCents;
$starter = $case['months'] * $starterMonthlyCents;
$rows[] = [
'count' => $case['count'],
'spread' => $case['spread'],
'months' => $case['months'],
'single' => $this->euro($single),
'starter' => $this->euro($starter),
'cheaper' => $single === $starter ? 'equal' : ($single < $starter ? 'single' : 'starter'),
];
}
return $rows;
}
private function euro(int $cents): string
{
return number_format($cents / 100, $cents % 100 === 0 ? 0 : 2, ',', '.');
}
}