mein-sterntours/app/Models/OfferVersion.php

174 lines
5.6 KiB
PHP

<?php
namespace App\Models;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Eine konkrete Version (Snapshot) eines Angebots.
*
* Alle inhaltlichen Felder (Texte, Preise, Gültigkeit, PDF-Pfad, Status,
* Anhänge) hängen hier — nicht am Offer-Kopf. So bleiben bereits versendete
* Versionen unveränderlich (`isEditable() === false`), während eine neue
* Version ihre eigenen Texte/Positionen/Anhänge führt.
*
* @property int $id
* @property int $offer_id
* @property int $version_no
* @property string $status draft|sent|accepted|declined|expired|superseded
* @property Carbon|null $valid_until
* @property float $total_price
* @property string|null $headline
* @property string|null $intro_text
* @property string|null $itinerary_text
* @property string|null $closing_text
* @property int|null $template_id
* @property string|null $pdf_path
* @property bool $pdf_archived
* @property Carbon|null $sent_at
* @property Carbon|null $accepted_at
* @property string|null $accepted_via customer_link|admin|email
* @property array|null $template_document_ids JSON-Array von zentral hinterlegten Dokument-IDs
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read Offer $offer
* @property-read Collection|OfferItem[] $items
* @property-read Collection|OfferFile[] $files
* @property-read Collection|OfferAccessToken[] $tokens
* @property-read OfferTemplate|null $template
* @property-read User $createdBy
*/
class OfferVersion extends Model
{
use HasFactory;
public const STATUS_DRAFT = 'draft';
public const STATUS_SENT = 'sent';
public const STATUS_ACCEPTED = 'accepted';
public const STATUS_DECLINED = 'declined';
public const STATUS_EXPIRED = 'expired';
public const STATUS_SUPERSEDED = 'superseded';
public const ACCEPTED_VIA_LINK = 'customer_link';
public const ACCEPTED_VIA_ADMIN = 'admin';
public const ACCEPTED_VIA_EMAIL = 'email';
protected $connection = 'mysql';
protected $table = 'offer_versions';
protected $fillable = [
'offer_id',
'version_no',
'status',
'valid_until',
'total_price',
'headline',
'intro_text',
'itinerary_text',
'closing_text',
'template_id',
'pdf_path',
'pdf_archived',
'sent_at',
'accepted_at',
'accepted_via',
'template_document_ids',
'created_by',
];
protected $casts = [
'offer_id' => 'int',
'version_no' => 'int',
'template_id' => 'int',
'created_by' => 'int',
'total_price' => 'decimal:2',
'valid_until' => 'date',
'sent_at' => 'datetime',
'accepted_at' => 'datetime',
'pdf_archived' => 'bool',
'template_document_ids' => 'array',
];
// ── Beziehungen ──────────────────────────────────────────────────────────
public function offer(): BelongsTo
{
return $this->belongsTo(Offer::class, 'offer_id');
}
public function items(): HasMany
{
return $this->hasMany(OfferItem::class, 'offer_version_id')->orderBy('position');
}
public function files(): HasMany
{
return $this->hasMany(OfferFile::class, 'offer_version_id');
}
public function tokens(): HasMany
{
return $this->hasMany(OfferAccessToken::class, 'offer_version_id');
}
public function template(): BelongsTo
{
return $this->belongsTo(OfferTemplate::class, 'template_id');
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
/**
* Darf diese Version noch bearbeitet werden?
* Nur Draft-Versionen sind editierbar; alle anderen sind eingefroren.
*/
public function isEditable(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* Liefert den zuletzt erzeugten, noch nicht widerrufenen Token
* (falls vorhanden) — wird im Freigabe-Link verwendet.
*/
public function latestToken(): ?OfferAccessToken
{
return $this->tokens()
->whereNull('revoked_at')
->orderByDesc('id')
->first();
}
/**
* Formatierter Preis („1.234,56 €"), konsistent mit der Darstellung
* im Buchungsdetail.
*/
public function totalPriceFormatted(): string
{
return number_format((float) $this->total_price, 2, ',', '.') . ' €';
}
/**
* Gibt die URL auf das zuletzt erzeugte PDF dieser Version zurück,
* oder null, wenn noch kein PDF existiert.
*/
public function getPdfUrl(bool $download = false): ?string
{
if (! $this->pdf_path) {
return null;
}
return route('offer_pdf', ['versionId' => $this->id, 'do' => $download ? 'download' : null]);
}
}