Teilnahmerechnungen erstellen
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Models\Tenant;
|
||||
use App\RelationModels\EventEatingHabits;
|
||||
use App\RelationModels\EventLocalGroups;
|
||||
use App\RelationModels\EventPaymentMethods;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateEventCommand {
|
||||
@@ -27,8 +28,13 @@ class CreateEventCommand {
|
||||
}
|
||||
|
||||
|
||||
$event = Event::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
$tenant = app('tenant');
|
||||
|
||||
// Anlage als Ganzes: der Event-Teil der Rechnungsnummer wird unter der Sperre vergeben und muss
|
||||
// im selben Zug geschrieben werden, sonst könnten zwei gleichzeitige Anlagen denselben erhalten.
|
||||
$event = DB::transaction(function () use ($tenant): Event {
|
||||
$event = Event::create([
|
||||
'tenant' => $tenant->slug,
|
||||
'name' => $this->request->name,
|
||||
'identifier' => Str::random(10),
|
||||
'location' => $this->request->location,
|
||||
@@ -49,12 +55,22 @@ class CreateEventCommand {
|
||||
'support_flat' => 0,
|
||||
// Umsatzsteuerliche Grundlage bei Anlage einfrieren (unveränderlich), damit Preisermittlung und
|
||||
// spätere Rechnungen von späteren Tenant-Änderungen unberührt bleiben.
|
||||
'tax_liable' => app('tenant')->tax_liable,
|
||||
'vat_rate' => app('tenant')->vat_rate,
|
||||
'vat_pricing_mode' => app('tenant')->vat_pricing_mode,
|
||||
'tax_exemption_reason' => app('tenant')->tax_exemption_reason,
|
||||
'tax_exemption_note' => app('tenant')->tax_exemption_note,
|
||||
]);
|
||||
'tax_liable' => $tenant->tax_liable,
|
||||
'vat_rate' => $tenant->vat_rate,
|
||||
'vat_pricing_mode' => $tenant->vat_pricing_mode,
|
||||
'tax_exemption_reason' => $tenant->tax_exemption_reason,
|
||||
'tax_exemption_note' => $tenant->tax_exemption_note,
|
||||
// Den Event-Teil der Rechnungsnummer einfrieren: Tenant-Präfix und Startdatum sind später
|
||||
// änderbar, eine herausgegebene Rechnung würde sonst zu einer anderen Nummer gehören.
|
||||
//
|
||||
// Der Rechnungssteller wird bewusst NICHT mitkopiert -- er ist der Mandant und wird beim
|
||||
// Erzeugen der Rechnung von dort gelesen, damit eine Korrektur an Name oder Anschrift auch
|
||||
// auf bestehende Veranstaltungen wirkt.
|
||||
'invoice_key' => $this->nextInvoiceKey($tenant),
|
||||
]);
|
||||
|
||||
return $event;
|
||||
});
|
||||
|
||||
if ($event !== null) {
|
||||
EventEatingHabits::create([
|
||||
@@ -90,4 +106,35 @@ class CreateEventCommand {
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event-Teil der Rechnungsnummer, z.B. `WM-V-20260701`: Tenant-Präfix, Dokumentart „V" für
|
||||
* Veranstaltung, dann ohne Trenner Jahr, Monat des Beginns und die laufende Nummer der
|
||||
* Veranstaltungen dieses Tenants in diesem Monat.
|
||||
*
|
||||
* Gezählt wird über die bereits vergebenen Schlüssel desselben Monats. Die Sperre auf der
|
||||
* Tenant-Zeile serialisiert gleichzeitige Anlagen; der Aufruf erfolgt innerhalb der
|
||||
* Anlage-Transaktion, sodass die Sperre bis zum Schreiben des Events steht.
|
||||
*/
|
||||
private function nextInvoiceKey(Tenant $tenant): string
|
||||
{
|
||||
// Jahr und Monat ohne Trenner; die laufende Nummer haengt unmittelbar daran. Alle drei Teile
|
||||
// haben feste Laenge, der Schluessel bleibt dadurch eindeutig zerlegbar.
|
||||
$month = $this->request->begin->format('Ym');
|
||||
$prefix = sprintf('%s-V-%s', $tenant->invoice_prefix ?? strtoupper($tenant->slug), $month);
|
||||
|
||||
DB::table('tenants')->where('id', $tenant->id)->lockForUpdate()->first();
|
||||
|
||||
$used = DB::table('events')
|
||||
->where('tenant', $tenant->slug)
|
||||
->where('invoice_key', 'like', $prefix . '%')
|
||||
->pluck('invoice_key');
|
||||
|
||||
$highest = 0;
|
||||
foreach ($used as $key) {
|
||||
$highest = max($highest, (int) substr((string) $key, strlen($prefix)));
|
||||
}
|
||||
|
||||
return $prefix . str_pad((string) ($highest + 1), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enumerations\EatingHabit;
|
||||
use App\Enumerations\EfzStatus;
|
||||
use App\EventPaymentModules\EventPaymentModuleRegistry;
|
||||
use App\ValueObjects\Age;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SignUpCommand {
|
||||
@@ -43,15 +44,21 @@ class SignUpCommand {
|
||||
|
||||
$participantAge = new Age($this->request->birthday);
|
||||
|
||||
$response->participant = $this->request->event->participants()->create(
|
||||
// Anmeldung als Ganzes: die laufende Nummer (invoice_sequence) muss unter der Sperre vergeben und
|
||||
// im selben Zug geschrieben werden, sonst könnten zwei gleichzeitige Anmeldungen dieselbe erhalten.
|
||||
$response->participant = DB::transaction(function () use ($participantAge, $eatingHabit, $paymentOptions, $participationOptionRows) {
|
||||
$participant = $this->request->event->participants()->create(
|
||||
[
|
||||
'tenant' => $this->request->event->tenant,
|
||||
'user_id' => $this->request->user_id,
|
||||
'identifier' => Str::random(10),
|
||||
'invoice_sequence' => $this->nextInvoiceSequence(),
|
||||
'firstname' => $this->request->firstname,
|
||||
'lastname' => $this->request->lastname,
|
||||
'nickname' => $this->request->nickname,
|
||||
'participation_type' => $this->request->participationType,
|
||||
'fee_type' => $this->request->feeType,
|
||||
'sibling_reduction' => $this->request->siblingReduction,
|
||||
'local_group' => $this->request->localGroup->slug,
|
||||
'birthday' => $this->request->birthday,
|
||||
'address_1' => $this->request->address_1,
|
||||
@@ -90,15 +97,40 @@ class SignUpCommand {
|
||||
'payment_options' => $paymentOptions,
|
||||
'efz_status' => $participantAge->isfullAged() ? EfzStatus::EFZ_STATUS_NOT_CHECKED : EfzStatus::EFZ_STATUS_NOT_REQUIRED,
|
||||
]
|
||||
);
|
||||
);
|
||||
|
||||
$this->persistParticipationOptions($response->participant, $participationOptionRows);
|
||||
$this->persistAddons($response->participant);
|
||||
$this->persistParticipationOptions($participant, $participationOptionRows);
|
||||
$this->persistAddons($participant);
|
||||
|
||||
return $participant;
|
||||
});
|
||||
|
||||
$response->success = true;
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nächste laufende Nummer des Teilis innerhalb der Veranstaltung -- der letzte Block der
|
||||
* Rechnungsnummer. Die Event-Zeile wird gesperrt, damit gleichzeitige Anmeldungen derselben
|
||||
* Veranstaltung serialisiert werden; der Aufruf erfolgt innerhalb der Anmelde-Transaktion, sodass die
|
||||
* Sperre bis zum Schreiben des Teilis steht.
|
||||
*
|
||||
* Abgemeldete Teilis behalten ihre Nummer, gezählt wird deshalb über MAX und nicht über COUNT.
|
||||
*/
|
||||
private function nextInvoiceSequence(): int
|
||||
{
|
||||
DB::table('events')
|
||||
->where('id', $this->request->event->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
$highest = DB::table('event_participants')
|
||||
->where('event_id', $this->request->event->id)
|
||||
->max('invoice_sequence');
|
||||
|
||||
return (int) $highest + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert die gewählten Zusätze (Event-Addons) als Snapshot-Zeilen (Titel/Preis/Betrag) für die spätere
|
||||
* Rechnung. Die Beträge sind bereits in der Controller-Auflösung (`resolveAddons`) berechnet, inkl. USt.
|
||||
|
||||
@@ -50,6 +50,10 @@ class SignUpRequest {
|
||||
public array $participationOptions = [],
|
||||
/** Aufgelöste Zusätze (Event-Addons): je Eintrag [ key, title, price, flat, days, amount ]. */
|
||||
public array $addonItems = [],
|
||||
/** Gewählte Preisspalte: standard | reduced | solidarity. Wird für die Rechnung mitgeschrieben. */
|
||||
public ?string $feeType = null,
|
||||
/** Ob die 50-%-Geschwisterermäßigung in `amount` eingerechnet ist. */
|
||||
public bool $siblingReduction = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user