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>
This commit is contained in:
parent
6668f492ad
commit
be7d1799a5
4 changed files with 372 additions and 0 deletions
56
app/Services/Billing/LegacySubscriptionService.php
Normal file
56
app/Services/Billing/LegacySubscriptionService.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue