presseportale/app/Console/Commands/GenerateManualInvoices.php
Kevin Adametz 9a88e82baa billing/dev: Vertragsende in der MAN-Abrechnung, Rechnungslauf ohne Tageskappe, Qualitätsgates (Audit Block 5+6, Deploy E)
Block 5 — MAN-Abrechnung

F06 (E6): Ein explizites Legacy-Vertragsende (grandfathered_until) deckelt
jetzt Auswahl und Sperr-Nachprüfung: Beginnt die nächste Periode nach dem
Vertragsende, gibt es keine Rechnung mehr. Neuer täglicher Befehl
billing:expire-grandfathering (04:15, vor dem Rechnungslauf) setzt
abgelaufene Posten auf gekündigt und schickt je Kunde eine Mail
(GrandfatheringExpired). Auf der Dev-Kopie trägt kein Posten ein
Vertragsende; der Fehler war latent.

F16: Nicht abrechenbare Vereinbarungen bekommen einen Sperrvermerk
(billing_hold_reason/-at), sichtbar auf der Admin-Zahlungsseite, und
stehen im Rechnungslauf hinten an. Der Lauf arbeitet den fälligen Bestand
in Seiten ab; --limit deckelt die erstellten Rechnungen (500), nicht die
betrachteten Fälle. Vorher blockierten 50 unvollständige Fälle die 51.
dauerhaft, und der Durchsatz war auf 50 am Tag begrenzt.

Block 6 — Entwicklung

F24: Lint-Workflow prüft mit pint --test statt zu formatieren; keine
Schreibrechte mehr.
F27: composer run dev startet beide Vite-Server (npm run dev:all).
F28: docs/README.md als historischer Stand markiert, toter Link auf das
Hub-Flux-Archiv korrigiert, aktueller Einstieg dev/README.md.
F26: Login-Modal übernimmt den Fokus, hält Tab im Dialog und gibt den
Fokus beim Schließen an den Auslöser zurück – ohne neue Abhängigkeit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:41:04 +00:00

92 lines
3.2 KiB
PHP
Raw Permalink 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\Console\Commands;
use App\Services\Billing\ManualInvoiceService;
use Illuminate\Console\Command;
/**
* Fälligkeitsprüfung des manuellen Rechnungskreises (MAN-).
*
* Läuft täglich über den Scheduler und stellt für aktive Legacy-
* Zahlungsvereinbarungen (ohne Stripe-Subscription) die fälligen
* Rechnungen aus — wie im Legacy-System. Nicht abrechenbare
* Vereinbarungen (fehlende Rechnungsadresse o. ä.) werden geloggt, mit
* Sperrvermerk versehen und beim nächsten Lauf erneut geprüft.
*
* Der Lauf arbeitet den gesamten fälligen Bestand in Seiten ab (Audit
* F16): Vorher nahm er die 50 ältesten Fälligkeiten; standen dort 50
* nicht abrechenbare Fälle, kam die 51. nie dran, und ohne Blocker war
* der Durchsatz auf 50 Rechnungen am Tag begrenzt. `--limit` deckelt jetzt
* die Zahl der erstellten Rechnungen, nicht der betrachteten Fälle.
*/
class GenerateManualInvoices extends Command
{
private const PAGE_SIZE = 100;
protected $signature = 'billing:generate-manual-invoices
{--dry-run : Nur anzeigen, was fällig ist}
{--limit=500 : Maximale Anzahl erstellter Rechnungen pro Lauf}';
protected $description = 'Stellt fällige Rechnungen des manuellen Legacy-Rechnungskreises (MAN-) aus';
public function handle(ManualInvoiceService $service): int
{
$dryRun = (bool) $this->option('dry-run');
$limit = max(1, (int) $this->option('limit'));
$created = 0;
$skipped = 0;
$seen = [];
while ($created < $limit) {
$due = $service->duePaymentOptions(limit: self::PAGE_SIZE, excludeIds: $seen);
if ($due->isEmpty()) {
break;
}
foreach ($due as $option) {
$seen[] = $option->id;
if ($dryRun) {
$this->line(sprintf(
'[dry-run] Fällig: Vereinbarung #%d (User #%s, Periode bis %s)%s',
$option->id,
$option->user_id,
$option->current_period_end->toDateString(),
$option->billing_hold_reason ? ' – gesperrt: '.$option->billing_hold_reason : '',
));
continue;
}
if ($created >= $limit) {
break;
}
$invoice = $service->invoiceFor($option);
if ($invoice) {
$created++;
$this->line(sprintf('Rechnung %s für Vereinbarung #%d erstellt.', $invoice->number, $option->id));
} else {
$skipped++;
$this->warn(sprintf('Vereinbarung #%d übersprungen: %s.', $option->id, $option->refresh()->billing_hold_reason ?? 'nicht abrechenbar'));
}
}
}
if ($seen === []) {
$this->info('Keine fälligen Zahlungsvereinbarungen gefunden.');
return self::SUCCESS;
}
if (! $dryRun) {
$this->info(sprintf('%d Rechnung(en) erstellt, %d übersprungen.', $created, $skipped));
}
return self::SUCCESS;
}
}