presseportale/app/Models/Invoice.php
Kevin Adametz a00f9dc79a 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

127 lines
3.2 KiB
PHP

<?php
namespace App\Models;
use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Invoice extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'user_id',
'user_payment_id',
'user_payment_option_id',
'invoice_billing_address_id',
'number',
'status',
'amount_cents',
'tax_cents',
'total_cents',
'currency',
'is_netto',
'tax_note',
'invoice_date',
'due_date',
'paid_at',
'sent_at',
'reminded_at',
'reminder_count',
'voided_at',
'void_reason',
'replaces_legacy_invoice_number',
'stripe_invoice_id',
'pdf_path',
];
protected function casts(): array
{
return [
'status' => InvoiceStatus::class,
'amount_cents' => 'integer',
'tax_cents' => 'integer',
'total_cents' => 'integer',
'is_netto' => 'boolean',
'invoice_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
'sent_at' => 'datetime',
'reminded_at' => 'datetime',
'reminder_count' => 'integer',
'voided_at' => 'datetime',
'deleted_at' => 'datetime',
];
}
/**
* @return BelongsTo<User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* @return BelongsTo<UserPayment, $this>
*/
public function userPayment(): BelongsTo
{
return $this->belongsTo(UserPayment::class, 'user_payment_id');
}
/**
* Abgerechnete Zahlungsvereinbarung (MAN-Kreis). Für STR-Rechnungen NULL.
*
* @return BelongsTo<UserPaymentOption, $this>
*/
public function userPaymentOption(): BelongsTo
{
return $this->belongsTo(UserPaymentOption::class);
}
/**
* @return BelongsTo<InvoiceBillingAddress, $this>
*/
public function invoiceBillingAddress(): BelongsTo
{
return $this->belongsTo(InvoiceBillingAddress::class);
}
/**
* MAN-Kreis: lokal ausgestellte Rechnung, per Überweisung zu zahlen.
*/
public function isManual(): bool
{
return $this->stripe_invoice_id === null;
}
public function isOverdue(): bool
{
return $this->status === InvoiceStatus::Open
&& $this->due_date !== null
&& $this->due_date->isBefore(today());
}
/**
* Rechnungen des manuellen Kreises (ohne Stripe-Beleg).
*/
public function scopeManual(Builder $query): Builder
{
return $query->whereNull('stripe_invoice_id');
}
public function scopeOpen(Builder $query): Builder
{
return $query->where('status', InvoiceStatus::Open->value);
}
public function scopeOverdue(Builder $query): Builder
{
return $this->scopeOpen($query)->whereDate('due_date', '<', today());
}
}