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>
This commit is contained in:
parent
ada41cb067
commit
36691a5b13
33 changed files with 3606 additions and 195 deletions
|
|
@ -3,6 +3,7 @@
|
|||
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;
|
||||
|
|
@ -15,6 +16,7 @@ class Invoice extends Model
|
|||
protected $fillable = [
|
||||
'user_id',
|
||||
'user_payment_id',
|
||||
'user_payment_option_id',
|
||||
'invoice_billing_address_id',
|
||||
'number',
|
||||
'status',
|
||||
|
|
@ -27,6 +29,12 @@ class Invoice extends Model
|
|||
'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',
|
||||
];
|
||||
|
|
@ -42,6 +50,10 @@ class Invoice extends Model
|
|||
'invoice_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
'paid_at' => 'datetime',
|
||||
'sent_at' => 'datetime',
|
||||
'reminded_at' => 'datetime',
|
||||
'reminder_count' => 'integer',
|
||||
'voided_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
|
@ -62,6 +74,16 @@ class Invoice extends Model
|
|||
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>
|
||||
*/
|
||||
|
|
@ -69,4 +91,37 @@ class Invoice extends Model
|
|||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,25 @@ 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 = [
|
||||
|
|
@ -65,4 +84,27 @@ class LegacyInvoice extends Model
|
|||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue