presseportale/app/Services/Billing/LegacySubscriptionService.php
Kevin Adametz be7d1799a5 WS-4: Bestandsschutz-Abrechnung – gekoppelte Kündigung + Admin-Sichtbarkeit
M1 (portalübergreifend) und M5 (04:30-Worker) verifiziert; M2 + M3 umgesetzt.

- M2 (Entscheidung: getrennte Rechnungen, gekoppelte Kündigung): neuer
  LegacySubscriptionService::cancelBundleFor() kündigt das gesamte
  Bestandsschutz-Bündel eines Kunden gemeinsam – keine Einzelkündigung. Kein
  Rechnungs-Schema-Umbau (jede Kondition behält ihre MAN-Rechnung).
- M3: Admin-Payments-Ansicht zeigt eine "Bestandsschutz-Posten"-Tabelle
  (Badge, Portal, Netto, nächste Fälligkeit, "Bündel (N)") + Bündel-Kündigung
  mit Bestätigungsmodal.
- Tests: tests/Feature/Billing/LegacyBundleTest.php (M1 cross-portal, M2 Bündel-
  Kündigung + Gate, M3 Admin-Liste + Kündigung). Detailplan WS-4 dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:20:18 +00:00

56 lines
1.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Billing;
use App\Enums\UserPaymentOptionStatus;
use App\Models\User;
use App\Models\UserPaymentOption;
use Illuminate\Support\Collection;
/**
* Bestandsschutz-Vereinbarungen (WS-4, M2).
*
* Legacy-Posten eines Kunden gelten als Bündel: Sie werden je Portal als eigene
* Kondition geführt (kein Tarif-Merge), sind aber NUR GEMEINSAM kündbar es gibt
* bewusst keine Einzelkündigung. Die Abrechnung selbst bleibt pro Posten (eigene
* MAN-Rechnung, siehe ManualInvoiceService).
*/
class LegacySubscriptionService
{
/**
* Noch laufende Bestandsschutz-Posten eines Kunden (das „Bündel").
*
* @return Collection<int, UserPaymentOption>
*/
public function grandfatheredFor(User $user): Collection
{
return UserPaymentOption::query()
->where('user_id', $user->getKey())
->where('status', UserPaymentOptionStatus::Grandfathered->value)
->whereNull('cancelled_at')
->get();
}
/**
* Kündigt das GESAMTE Bestandsschutz-Bündel eines Kunden gemeinsam. Es gibt
* keine Einzelkündigung alle laufenden Grandfathered-Posten werden in einem
* Schritt auf `cancelled` gesetzt, womit der 04:30-Worker keine weiteren
* MAN-Rechnungen mehr erzeugt (duePaymentOptions berücksichtigt nur
* Active/Grandfathered).
*
* @return int Anzahl der gekündigten Posten
*/
public function cancelBundleFor(User $user): int
{
$posts = $this->grandfatheredFor($user);
foreach ($posts as $post) {
$post->update([
'status' => UserPaymentOptionStatus::Cancelled->value,
'cancelled_at' => now(),
]);
}
return $posts->count();
}
}