presseportale/app/Models/LegacyInvoice.php
Kevin Adametz 36691a5b13 billing: MAN-Rechnungen fuer Ueberweisungskunden bearbeiten, hold-Altlast bereinigen
Bestandskunden aus dem Altsystem zahlen per Ueberweisung; nach dem 04:30-Lauf
fehlte bisher der gesamte Bearbeitungsweg (kein PDF, keine Mail, kein
Zahlungseingang, kein Bestandsschutz-Ende). Plan und Befund:
dev/lagacy-rechnungen/entwicklungsplan-2026-09-16.md.

Bereinigung (legacy:settle-unpaid, wiederholbar):
- Legacy-Status hold = nie bezahlt, ersatzlos erledigt. 9 daraus abgeleitete
  Bestandsschutz-Buendel werden gekuendigt, MAN-00001/00002 storniert.
  Archivzeilen bleiben erhalten, sind aber in Admin-Archiv und Kundenkonto
  standardmaessig ausgeblendet.
- pending/reminder (recurring) = offene Vorgaenge: Periode auf den
  Leistungsbeginn der unbezahlten Rechnung zurueckgedreht und als MAN-Rechnung
  neu ausgestellt (Archiv-Status superseded, replaces_legacy_invoice_number).
- pending/reminder (single) = abgeschrieben (uncollectible).
- legacy:grandfather-subscriptions wertet den Zahlungsstatus jetzt selbst aus.

Workflow in /admin/payments (ManualInvoiceDunningService, je Aktion mit
Eintrag im Aktivitaets-Verlauf):
- Rechnung senden: Mail mit PDF-Anhang, Bankverbindung aus billing.issuer,
  Hinweis auf Bestandsschutz-Ende; Zahlungsziel zaehlt ab Versand.
- Erinnerung: gleiche Mail im Mahn-Ton, Nachfrist bei verstrichener Frist.
- Bezahlt: Zahlungseingang laut Kontoauszug verbuchen.
- Bestandsschutz beenden: Rechnung storniert (nie geloescht), Buendel
  gekuendigt, weitere offene MAN-Rechnungen des Kunden storniert, optionale
  Kundenmail.
- Panel "Blockierte Vereinbarungen": faellige Posten ohne Rechnung (meist
  fehlende Rechnungsadresse) mit "Jetzt abrechnen" oder "Entfernen".

Schema: invoices bekommt user_payment_option_id, sent_at, reminded_at,
reminder_count, voided_at, void_reason, replaces_legacy_invoice_number
(inkl. Backfill der bestehenden MAN-Rechnungen). PDF-Primitiven des
Archiv-Renderers in den Trait DrawsPdf ausgelagert; MAN-PDF on-demand ueber
admin.invoices.pdf / me.invoices.manual-pdf (InvoicePolicy).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:56:29 +00:00

110 lines
3 KiB
PHP

<?php
namespace App\Models;
use App\Enums\Portal;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LegacyInvoice extends Model
{
/**
* Legacy-Status: nie bezahlt, überfällig, Kunde meist nicht mehr
* erreichbar. Gilt als ersatzlos erledigt (Einordnung 16.09.2026) und
* wird in Kunden- und Admin-Ansicht standardmäßig ausgeblendet.
*/
public const STATUS_HOLD = 'hold';
/** Offene Alt-Rechnung, im Altsystem noch nicht gemahnt. */
public const STATUS_PENDING = 'pending';
/** Offene Alt-Rechnung, im Altsystem bereits gemahnt. */
public const STATUS_REMINDER = 'reminder';
/** Durch eine MAN-Rechnung des neuen Systems ersetzt (Phase B). */
public const STATUS_SUPERSEDED = 'superseded';
/** Abgeschrieben — Forderung wird nicht weiter verfolgt. */
public const STATUS_UNCOLLECTIBLE = 'uncollectible';
public $timestamps = false;
protected $fillable = [
'legacy_portal',
'legacy_id',
'user_id',
'legacy_user_id',
'number',
'amount_cents',
'tax_cents',
'total_cents',
'status',
'invoice_date',
'due_date',
'paid_at',
'payment_method',
'pdf_path',
'raw_snapshot',
'pdf_payload',
'pdf_generated_at',
'imported_at',
];
protected function casts(): array
{
return [
'legacy_portal' => Portal::class,
'amount_cents' => 'integer',
'tax_cents' => 'integer',
'total_cents' => 'integer',
'invoice_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
'raw_snapshot' => 'array',
'pdf_payload' => 'array',
'pdf_generated_at' => 'datetime',
'imported_at' => 'datetime',
];
}
/**
* @return BelongsTo<User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Neueste Rechnung zuerst: nach Rechnungsdatum, dann ID als Tiebreaker.
* Single Source of Truth für die „latest legacy invoice"-Ordnung.
*/
public function scopeLatestFirst(Builder $query): Builder
{
return $query->orderByDesc('invoice_date')->orderByDesc('id');
}
/**
* Ohne die erledigten `hold`-Rechnungen (Default in allen Ansichten).
*/
public function scopeWithoutHold(Builder $query): Builder
{
return $query->where(function (Builder $query): void {
$query->whereNull('status')->orWhere('status', '!=', self::STATUS_HOLD);
});
}
/**
* Noch offene Alt-Vorgänge, die im neuen System abgearbeitet werden.
*/
public function scopeOpenLegacy(Builder $query): Builder
{
return $query->whereIn('status', [self::STATUS_PENDING, self::STATUS_REMINDER]);
}
public function isHold(): bool
{
return $this->status === self::STATUS_HOLD;
}
}