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>
56 lines
1.7 KiB
PHP
56 lines
1.7 KiB
PHP
<?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();
|
||
}
|
||
}
|