207 lines
6.9 KiB
PHP
207 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Models\Offer;
|
|
use App\Models\OfferItem;
|
|
use App\Models\OfferVersion;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Repository für Angebots-Versionen.
|
|
*
|
|
* Zentrale Invarianten:
|
|
* - Nur Draft-Versionen sind editierbar (sonst `DomainException`).
|
|
* - `createNewVersion()` dupliziert alle Items + Files, markiert die alte
|
|
* Version als `superseded` und schiebt `offer.current_version_id` um.
|
|
* - `markSent()` friert die Version ein (status=sent, sent_at=now) und hebt
|
|
* den Offer-Status von `draft` auf `sent`.
|
|
*/
|
|
class OfferVersionRepository extends BaseRepository
|
|
{
|
|
public function __construct(OfferVersion $model)
|
|
{
|
|
$this->model = $model;
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert eine Draft-Version und synchronisiert ggf. die Items.
|
|
* Total-Preis wird anschließend aus den Items neu aufsummiert.
|
|
*
|
|
* @param array<string,mixed> $data beliebige OfferVersion-Felder + optional 'items' => [...]
|
|
*/
|
|
public function updateDraft(OfferVersion $v, array $data): OfferVersion
|
|
{
|
|
if (! $v->isEditable()) {
|
|
throw new \DomainException(
|
|
"OfferVersion #{$v->id} ist nicht mehr editierbar (status={$v->status}). "
|
|
. 'Für Änderungen an versendeten Versionen createNewVersion() nutzen.'
|
|
);
|
|
}
|
|
|
|
return DB::transaction(function () use ($v, $data) {
|
|
$items = $data['items'] ?? null;
|
|
unset($data['items']);
|
|
|
|
$v->fill($data);
|
|
$v->save();
|
|
|
|
if (is_array($items)) {
|
|
$this->syncItems($v, $items);
|
|
$v->total_price = (float) OfferItem::where('offer_version_id', $v->id)->sum('total_price');
|
|
$v->save();
|
|
}
|
|
|
|
return $v->fresh(['items']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dupliziert die aktuelle Version eines Offers in eine neue Draft-Version.
|
|
*
|
|
* - `version_no` = MAX(version_no) + 1
|
|
* - Items + Files werden als neue Zeilen kopiert (Datei-Binaries bleiben
|
|
* physisch geteilt — die Datei-Zeile bekommt nur eine neue DB-ID).
|
|
* - Alte Version → `superseded` (eingefroren).
|
|
* - `offer.current_version_id` wird auf die neue Version gesetzt.
|
|
*
|
|
* Alles atomar in einer Transaktion.
|
|
*/
|
|
public function createNewVersion(Offer $offer): OfferVersion
|
|
{
|
|
return DB::transaction(function () use ($offer) {
|
|
/** @var OfferVersion|null $current */
|
|
$current = $offer->currentVersion()->with(['items', 'files'])->first();
|
|
if (! $current) {
|
|
throw new \RuntimeException("Offer #{$offer->id} hat keine currentVersion.");
|
|
}
|
|
|
|
$nextVersionNo = ((int) $offer->versions()->max('version_no')) + 1;
|
|
|
|
$new = $current->replicate([
|
|
'version_no',
|
|
'status',
|
|
'sent_at',
|
|
'accepted_at',
|
|
'accepted_via',
|
|
'pdf_path',
|
|
'pdf_archived',
|
|
'created_at',
|
|
'updated_at',
|
|
]);
|
|
$new->version_no = $nextVersionNo;
|
|
$new->status = OfferVersion::STATUS_DRAFT;
|
|
$new->sent_at = null;
|
|
$new->accepted_at = null;
|
|
$new->accepted_via = null;
|
|
$new->pdf_path = null;
|
|
$new->pdf_archived = false;
|
|
$new->save();
|
|
|
|
foreach ($current->items as $item) {
|
|
$newItem = $item->replicate(['offer_version_id', 'created_at', 'updated_at']);
|
|
$newItem->offer_version_id = $new->id;
|
|
$newItem->save();
|
|
}
|
|
|
|
foreach ($current->files as $file) {
|
|
$newFile = $file->replicate(['offer_version_id', 'created_at', 'updated_at']);
|
|
$newFile->offer_version_id = $new->id;
|
|
$newFile->save();
|
|
}
|
|
|
|
$current->status = OfferVersion::STATUS_SUPERSEDED;
|
|
$current->save();
|
|
|
|
$offer->current_version_id = $new->id;
|
|
$offer->status = Offer::STATUS_DRAFT;
|
|
$offer->save();
|
|
|
|
return $new->fresh(['items', 'files']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Markiert eine Version als versendet und hebt das Offer (falls noch draft)
|
|
* auf Status `sent`. Idempotent: erneutes Aufrufen ändert nichts.
|
|
*/
|
|
public function markSent(OfferVersion $v): OfferVersion
|
|
{
|
|
if ($v->status === OfferVersion::STATUS_SENT) {
|
|
return $v;
|
|
}
|
|
|
|
return DB::transaction(function () use ($v) {
|
|
$v->status = OfferVersion::STATUS_SENT;
|
|
$v->sent_at = $v->sent_at ?? now();
|
|
$v->save();
|
|
|
|
$offer = $v->offer;
|
|
if ($offer && $offer->status === Offer::STATUS_DRAFT) {
|
|
$offer->status = Offer::STATUS_SENT;
|
|
$offer->save();
|
|
}
|
|
|
|
return $v->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Markiert eine Version als akzeptiert (durch Admin oder Kunde-via-Token).
|
|
* Setzt gleichzeitig den Offer-Status auf `accepted`.
|
|
*/
|
|
public function markAccepted(OfferVersion $v, string $via = OfferVersion::ACCEPTED_VIA_ADMIN): OfferVersion
|
|
{
|
|
return DB::transaction(function () use ($v, $via) {
|
|
$v->status = OfferVersion::STATUS_ACCEPTED;
|
|
$v->accepted_at = $v->accepted_at ?? now();
|
|
$v->accepted_via = $via;
|
|
$v->save();
|
|
|
|
$offer = $v->offer;
|
|
if ($offer && $offer->status !== Offer::STATUS_ACCEPTED) {
|
|
$offer->status = Offer::STATUS_ACCEPTED;
|
|
$offer->save();
|
|
}
|
|
|
|
return $v->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Synchronisiert die Items einer Version mit einer Item-Liste:
|
|
* vorhandene `id`s werden aktualisiert, neue angelegt, fehlende gelöscht.
|
|
* Position wird aus Array-Index übernommen (0-basiert).
|
|
*
|
|
* @param array<int, array<string,mixed>> $items
|
|
*/
|
|
protected function syncItems(OfferVersion $v, array $items): void
|
|
{
|
|
$keepIds = [];
|
|
|
|
foreach (array_values($items) as $position => $itemData) {
|
|
$itemData['offer_version_id'] = $v->id;
|
|
$itemData['position'] = $position;
|
|
|
|
if (! empty($itemData['id'])) {
|
|
/** @var OfferItem $model */
|
|
$model = OfferItem::where('offer_version_id', $v->id)
|
|
->where('id', $itemData['id'])
|
|
->firstOrFail();
|
|
unset($itemData['id']);
|
|
$model->fill($itemData);
|
|
} else {
|
|
unset($itemData['id']);
|
|
$model = new OfferItem($itemData);
|
|
}
|
|
|
|
$model->recomputeTotal();
|
|
$model->save();
|
|
$keepIds[] = $model->id;
|
|
}
|
|
|
|
OfferItem::where('offer_version_id', $v->id)
|
|
->when(! empty($keepIds), fn ($q) => $q->whereNotIn('id', $keepIds))
|
|
->delete();
|
|
}
|
|
}
|