6.4 KiB
CLAUDE.md
Laravel 12 + Inertia/Vue, mandantenfähig (custom tenant-Scoping über app('tenant'), gesetzt in
app/Middleware/IdentifyTenant.php). PHP 8.5 — Tests/Artisan laufen im Container:
docker exec mareike-mareike-app-1 php artisan test.
Projektkonventionen (verbindlich)
Die verbindlichen Konventionen (Actions/Request-Command-Response, Controller, Repositories, Models/Resources, Tenant,
Routing, Mails) stehen in .ai/conventions.md und werden hier eingebunden — immer beachten:
@.ai/conventions.md
Bereichs-Dokumentation
- Zahlungsmodule — interface-gesteuerte Zahlungsarten (Optionen, Config, Anmelde-Zusammenfassung, GiroCode/Fähigkeits-Interfaces, neues Modul hinzufügen).
Local Group / Parent Group (Stamm / Bund)
Technisch heißt es Local Group (Mandant/Tenant) und Parent Group (übergeordnete Gruppe); „Stamm"/„Bund" sind nur UI-Bezeichnungen.
- ParentGroup (
App\Models\ParentGroup, Tabelleparent_groups, app-weit):tenants.parent_group_idist Pflicht (n:1).is_fallbackmarkiert „Ohne Zuordnung" —Tenant::booted()setzt sie, wenn nichts angegeben ist; sie ist nie löschbar. Andere nur, solange weder Tenant noch Anmeldung sie nutzt (AdminParentGroupRepository::isInUse()). Verwaltung LV-only unter/admin/parent-groups; Zuordnung am Tenant im Tab „Allgemeines" und bei der Anlage. - Event:
participant_group_type(EnumApp\Enumerations\ParticipantGroupType:local_group|parent_group), gewählt bei der Anlage, in den Einstellungen änderbar nur ohne Anmeldungen. Einzige Quelle für die Auswahl:Event::participantGroupOptions()— teilnehmende Stämme oder deren Bünde (distinct). Gespeichert wirdevent_participants.local_group(Slug) oderevent_participants.parent_group_id. - Anzeige:
EventParticipant::participantGroupName()→ Resource-FeldparticipantGroup;EventResourceliefertparticipantGroupType,participantGroupLabel,participantGroupOptions({id, name, value}). - Bezeichnungen in
global_settings(ModelGlobalSetting, RepositoryGlobalSettingRepository, im Containerscoped— immer überapp(...)beziehen), Singular + Plural je Ebene, gepflegt von Hauptadmins unter/admin/settings. Gelesen überParticipantGroupType::label()/pluralLabel()/labels(). Texte ohne Artikel formulieren („Bitte bei „{label}" eine Auswahl treffen."), weil der Artikel vom Wort abhängt.
eFZ-Prüfung pro Veranstaltung
events.coc_check_required (Default true, Checkbox in CommonSettings.vue, Resource-Feld cocCheckRequired). Ist
sie aus, liefert CertificateOfConductionCheckCommand ohne API-Aufruf not_required, und SignUpCommand belegt
efz_status mit not_required vor. Wirkt nur auf neue Anmeldungen, bestehende Status bleiben.
Umsatzsteuer / Steuerinformationen
Rechtlich korrekter Begriff ist Umsatzsteuer (USt), nicht MwSt. Ziel: tagesbasierte Anmelde-Rechnungen („Teilnahme Veranstaltung X in Gruppe Y" × Anwesenheitstage × Beitrag/Tag). Grundlage ist gebaut, die Rechnungs-PDF/-berechnung folgt noch.
Tenant-Einstellungen (Tab „Steuerinformationen")
Felder auf tenants (Self-Service TenantData.vue + verwaltet TenantEdit.vue, Partial TenantTaxInfo.vue,
Controller TenantTax* / ManagedTenantTax*, Action UpdateTenantTax):
tax_liable(bool) — ob USt-Pflicht besteht.vat_rate(int, %) — USt-Satz, nur relevant wenntax_liable(0–100, geclamped).vat_pricing_mode(EnumApp\Enumerations\VatPricingMode, natives PHP-Enum:inclusive|add_on).tax_exemption_reason(FK →tax_exemption_reasons.slug, nullable) — Grund wenn nicht steuerpflichtig. DB-Default neuer Tenants:ustg_4_25a(Jugendhilfe).tax_exemption_note(text, nullable) — Freitext, nur bei Grundcustom(requires_note).
Preismodus — das Kernverhalten
Es wird immer der finale Brutto-Beitrag pro Anmeldung gespeichert (EventParticipant.amount, keine USt-Spalten am
Teilnehmer). Der Modus wirkt nur bei der Preisermittlung im Anmeldeprozess:
inclusive(Brutto / „zusammenfassen", Supermarkt-Prinzip, Default): der konfigurierte Beitrag ist der Endpreis, USt ist bereits enthalten → Preis bleibt gleich; die USt wird erst auf der Rechnung herausgerechnet:USt = brutto × Satz / (100 + Satz),netto = brutto − USt.add_on(Netto / „aufaddieren"): der konfigurierte Beitrag ist netto → Endpreis =Beitrag × (1 + Satz/100). Die USt wird bei der Anmeldung aufgeschlagen → Teilnehmer zahlt mehr.
Umsetzung in App\Resources\EventResource::calculateAmount(): nach allen Multiplikatoren (Early-Bird, pay_per_day,
Geschwister) gilt
if ($event->tax_liable && $event->vat_pricing_mode === VatPricingMode::AddOn->value) $fee->multiply(1 + $event->vat_rate/100);
danach round(…, 2). Bei inclusive/nicht steuerpflichtig bleibt der Beitrag unverändert.
Event-Snapshot (unveränderlich)
Bei Event-Anlage (CreateEventCommand::execute()) werden tax_liable, vat_rate, vat_pricing_mode,
tax_exemption_reason, tax_exemption_note aus dem Tenant auf das Event kopiert und dort eingefroren (Spalten auf
events). Preisermittlung und spätere Rechnungen lesen vom Event, nicht vom Tenant → spätere Tenant-Änderungen berühren
bestehende Events/Rechnungen nicht.
Befreiungsgründe (DB-gestützt)
Tabelle tax_exemption_reasons, Model App\Enumerations\TaxExemptionReason (Eloquent, slug PK; name,
invoice_text, requires_note, sort_order). options() liefert {value,label,text} sortiert nach sort_order. FK
von tenants und events. Seed in Migration 170010, verfeinert in 170020. Gründe (in Reihenfolge):
ustg_19 (§ 19 Kleinunternehmer), ustg_4_24 (§ 4 Nr. 24 Jugendherbergen),
ustg_4_25a (§ 4 Nr. 25 Buchst. a — Jugendhilfe-Veranstaltungen, Default), ustg_4_22a (§ 4 Nr. 22 Buchst. a —
Vorträge/Kurse), custom (Freitext). Jeder Grund kapselt den Rechnungs-Pflichthinweis nach § 14 Abs. 4 Nr. 8 UStG
(invoice_text; bei custom der Tenant-Freitext via invoiceText(?note)). Gemeinnützigkeit allein befreit nicht —
es braucht immer einen konkreten Rechtsgrund.
Noch offen (nächster Schritt)
Die eigentliche Rechnung: tagesbasierte Positionen, USt aus dem gespeicherten Brutto herausrechnen (Satz vom Event-Snapshot); bei Befreiung kein USt-Ausweis, stattdessen der Pflichthinweis des Grundes.