- LogApiUsage als terminable Middleware: der ApiUsageLog-INSERT laeuft nach gesendeter Antwort und verlaengert nicht mehr die Request-Dauer. Startzeit wird ueber ein Request-Attribut uebergeben (terminate() laeuft auf frischer Instanz). - User::openSinglePurchaseCount() memoisiert die offenen Einmalkaeufe pro Request. Vorher wurde derselbe grantingSubmission-Count im Status-Widget mehrfach (Quota-Total, Quota-Remaining, openSinglePurchases) plus ein exists() in hasActiveBooking abgesetzt -> jetzt eine Query. - CustomerCompanyContext::selectedCompany() nutzt eine einzige first()-Query statt exists()+first(); das Verwerfen nicht (mehr) erreichbarer Auswahlen bleibt erhalten. - LegacySubscriptionService::cancelBundleFor() ist ein einziges Bulk-Update statt Update-pro-Iteration (UserPaymentOption hat keine Observer/Events). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54 lines
1.8 KiB
PHP
54 lines
1.8 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
|
||
{
|
||
return UserPaymentOption::query()
|
||
->where('user_id', $user->getKey())
|
||
->where('status', UserPaymentOptionStatus::Grandfathered->value)
|
||
->whereNull('cancelled_at')
|
||
->update([
|
||
'status' => UserPaymentOptionStatus::Cancelled->value,
|
||
'cancelled_at' => now(),
|
||
]);
|
||
}
|
||
}
|