Neustrukturierung Customer / Lead / Booking Phase 2
This commit is contained in:
parent
313f0dbf4e
commit
6df9c401af
69 changed files with 3809 additions and 374 deletions
207
app/Repositories/OfferVersionRepository.php
Normal file
207
app/Repositories/OfferVersionRepository.php
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue