WIP: Sicherheitsnetz vor Phase-1-R\u00fcckbau
Enth\u00e4lt gemischt: Laravel-10-Upgrade + Phase 1 (Contacts-Modul, Duplicats-Commands, Soft-Delete+Merge-Fields) + Phase 2 Code-Umstellungen (inquiry_id, $table='contacts'/'inquiries') + Offers-Modul (Migrationen, Models, offer_id in Booking, offer-Disk in filesystems.php). Phase 2 + Offers werden im folgenden Commit nach dev/backups/phase2-offers-2026-04-17/ verschoben, damit der Workspace auf Phase-1-only (= Test-System-Stand) reduziert ist und direkt auf Live deploybar wird. Tarball-Backup zus\u00e4tzlich unter: ../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz Made-with: Cursor
This commit is contained in:
parent
389d5d1820
commit
e3dc1afd8e
165 changed files with 21914 additions and 3516 deletions
441
dev/customer-bookings/konzept.md
Normal file
441
dev/customer-bookings/konzept.md
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
# Konzept: Neustrukturierung Customer / Lead / Booking
|
||||
|
||||
**Status:** Entwurf
|
||||
**Erstellt:** April 2025
|
||||
**Ziel:** Beseitigung der Daten-Redundanz und Vereinfachung der Kernstruktur
|
||||
|
||||
---
|
||||
|
||||
## 1. Ist-Zustand — Analyse der Probleme
|
||||
|
||||
### 1.1 Aktuelle Datenfluss-Kette
|
||||
|
||||
```
|
||||
Anfrage (Lead)
|
||||
└── customer_id → Customer (neu angelegt pro Lead!)
|
||||
└── LeadMail
|
||||
└── LeadFile
|
||||
└── LeadNotice
|
||||
└── LeadParticipant
|
||||
|
|
||||
| createBooking()
|
||||
↓
|
||||
Buchung (Booking)
|
||||
└── customer_id → Customer (derselbe, aber Daten werden separat gepflegt)
|
||||
└── lead_id → Lead (Rückreferenz)
|
||||
└── CustomerMail
|
||||
└── BookingFile
|
||||
└── BookingNotice
|
||||
└── Participant (Kopie von LeadParticipant!)
|
||||
```
|
||||
|
||||
### 1.2 Konkrete Probleme
|
||||
|
||||
| Problem | Auswirkung |
|
||||
|---------|-----------|
|
||||
| **Kunde wird pro Anfrage neu angelegt** | Bucht ein Kunde zweimal: 2 Customer-Datensätze, keine Kundenhistorie |
|
||||
| **Teilnehmer doppelt gespeichert** | `participant_name/firstname/birthdate` in `lead`, `booking` UND `lead_participant` + `participant` |
|
||||
| **Mail-Tabellen aufgesplittet** | `lead_mail` für Anfragen, `customer_mail` für Buchungen — gleiche Struktur, getrennte Tabellen |
|
||||
| **Notizen aufgesplittet** | `lead_notice` und `booking_notice` — identisch, getrennt |
|
||||
| **Datei-Tabellen aufgesplittet** | `lead_file`, `booking_file`, `customer_fewo_file` — gleiche Funktion |
|
||||
| **Kein übergreifendes Kunden-Profil** | Alle Buchungen/Anfragen eines Kunden nur per ID-Lookup findbar, aber nie wirklich verknüpft |
|
||||
| **`createBooking()` kopiert Daten** | `LeadRepository::createBooking()` kopiert Participants 1:1 — spätere Änderungen laufen auseinander |
|
||||
|
||||
---
|
||||
|
||||
## 2. Soll-Zustand — Zielbild
|
||||
|
||||
### 2.1 Kernprinzip
|
||||
|
||||
```
|
||||
Contact (Stammkunde — einmal, für immer)
|
||||
└── hasMany: Inquiry (früher: Lead)
|
||||
└── hasMany: Booking
|
||||
|
||||
Inquiry (Anfrage)
|
||||
└── belongsTo: Contact
|
||||
└── hasMany: Booking (eine Anfrage kann zu einer Buchung werden)
|
||||
└── hasMany: Communication (Mails, Notizen, Dateien)
|
||||
|
||||
Booking (Buchung)
|
||||
└── belongsTo: Contact
|
||||
└── belongsTo: Inquiry
|
||||
└── hasMany: Communication
|
||||
└── hasMany: Participant
|
||||
```
|
||||
|
||||
### 2.2 Neue Tabellen-Struktur
|
||||
|
||||
#### `contacts` (ersetzt `customer`)
|
||||
Identisch zur `customer`-Tabelle — nur umbenannt und mit Deduplizierungslogik ausgestattet.
|
||||
|
||||
```sql
|
||||
CREATE TABLE contacts (
|
||||
-- alle bisherigen Felder aus customer
|
||||
id, salutation_id, title, name, firstname, birthdate,
|
||||
company, street, zip, city, email, phone, phonebusiness,
|
||||
phonemobile, fax, bank, bank_code, bank_account_number,
|
||||
credit_card_type_id, credit_card_number,
|
||||
credit_card_expiration_date, participants_remarks,
|
||||
miscellaneous_remarks, country_id,
|
||||
-- neu:
|
||||
merged_into_id INT NULL, -- zeigt auf Haupt-Datensatz bei Duplikaten
|
||||
created_at, updated_at, deleted_at
|
||||
);
|
||||
```
|
||||
|
||||
#### `inquiries` (umbenennung von `lead`)
|
||||
Weitgehend identisch, `customer_id` → `contact_id`.
|
||||
|
||||
```sql
|
||||
-- Änderungen gegenüber lead:
|
||||
ALTER TABLE inquiries
|
||||
RENAME COLUMN customer_id TO contact_id;
|
||||
-- Teilnehmerfelder bleiben vorerst (Abwärtskompatibilität Phase 1)
|
||||
-- Entfernung in Phase 3
|
||||
```
|
||||
|
||||
#### `bookings` (geringfügige Anpassung)
|
||||
`customer_id` → `contact_id`, `lead_id` → `inquiry_id`.
|
||||
|
||||
```sql
|
||||
-- inquiry_id bleibt nullable:
|
||||
-- Direktbuchungen (ohne vorherige Anfrage) sollen möglich sein → inquiry_id = NULL
|
||||
-- Teilnehmerfelder (participant_name etc.) fallen in Phase 3 weg
|
||||
-- → wandern komplett in die participants-Tabelle
|
||||
```
|
||||
|
||||
#### `participants` (konsolidiert `lead_participant` + `participant`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE participants (
|
||||
id,
|
||||
contact_id INT NULL, -- direkte Zuordnung zum Stammkunden (optional)
|
||||
inquiry_id INT NULL, -- FK auf inquiries (früher lead_participant)
|
||||
booking_id INT NULL, -- FK auf bookings (früher participant)
|
||||
salutation_id INT NULL,
|
||||
name VARCHAR,
|
||||
firstname VARCHAR,
|
||||
birthdate DATE NULL,
|
||||
participant_child BOOL DEFAULT FALSE,
|
||||
nationality_id INT NULL,
|
||||
is_lead_contact BOOL DEFAULT FALSE, -- markiert den Hauptreisenden
|
||||
created_at, updated_at
|
||||
);
|
||||
```
|
||||
|
||||
#### `communications` (konsolidiert `lead_mail` + `customer_mail`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE communications (
|
||||
id,
|
||||
contact_id INT NULL,
|
||||
inquiry_id INT NULL,
|
||||
booking_id INT NULL,
|
||||
-- alle bisherigen Felder aus lead_mail/customer_mail:
|
||||
from_email, to_email, from_name, to_name,
|
||||
subject, body, sent_at,
|
||||
dir, subdir,
|
||||
-- Typ-Unterscheidung:
|
||||
context ENUM('inquiry', 'booking') NOT NULL,
|
||||
created_at, updated_at
|
||||
);
|
||||
```
|
||||
|
||||
#### `notices` (konsolidiert `lead_notice` + `booking_notice`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE notices (
|
||||
id,
|
||||
inquiry_id INT NULL,
|
||||
booking_id INT NULL,
|
||||
from_user_id INT NOT NULL,
|
||||
to_user_id INT NULL,
|
||||
message TEXT,
|
||||
edit_at DATETIME NULL,
|
||||
created_at, updated_at
|
||||
);
|
||||
```
|
||||
|
||||
#### `attachments` (konsolidiert `lead_file` + `booking_file` + `customer_fewo_file`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE attachments (
|
||||
id,
|
||||
contact_id INT NULL,
|
||||
inquiry_id INT NULL,
|
||||
booking_id INT NULL,
|
||||
disk, path, filename, mime_type, filesize,
|
||||
identifier VARCHAR NULL,
|
||||
created_at, updated_at
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Migrationsstrategie — Phasen
|
||||
|
||||
Die Migration ist bewusst **rückwärtskompatibel** geplant: jede Phase kann unabhängig deployed werden, das System bleibt zu jeder Zeit funktionsfähig.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — Contact-Deduplizierung (Kern-Problem lösen)
|
||||
|
||||
**Ziel:** Mehrfach-Kunden zusammenführen. Kein Datenverlust.
|
||||
|
||||
#### 1a) Duplikate identifizieren
|
||||
|
||||
```php
|
||||
// artisan-Befehl: php artisan contacts:find-duplicates
|
||||
// Gruppierungsstrategie (absteigend nach Konfidenz):
|
||||
// 1. Gleiche E-Mail-Adresse → sicher identisch
|
||||
// 2. Gleicher Name + Vorname + Geburtsdatum → sehr wahrscheinlich identisch
|
||||
// 3. Gleicher Name + Vorname + PLZ → wahrscheinlich identisch (manuell prüfen)
|
||||
```
|
||||
|
||||
Ausgabe: CSV-Liste mit Gruppen und vorgeschlagenem Haupt-Datensatz.
|
||||
|
||||
#### 1b) `merged_into_id` Spalte hinzufügen
|
||||
|
||||
```sql
|
||||
ALTER TABLE customer ADD COLUMN merged_into_id INT NULL;
|
||||
ALTER TABLE customer ADD COLUMN merged_at DATETIME NULL;
|
||||
```
|
||||
|
||||
#### 1c) Migration ausführen
|
||||
|
||||
```php
|
||||
// artisan-Befehl: php artisan contacts:merge-duplicates --dry-run
|
||||
// Dann: php artisan contacts:merge-duplicates --confirm
|
||||
//
|
||||
// Für jeden gefundenen Duplikat-Cluster:
|
||||
// 1. Neuesten Datensatz (höchste ID / neuestes updated_at) als Master wählen
|
||||
// → Entscheidung: immer die neueste Adresse gewinnt
|
||||
// 2. Alle leads: customer_id → master_id
|
||||
// 3. Alle bookings: customer_id → master_id
|
||||
// 4. Duplikat: merged_into_id = master_id setzen
|
||||
```
|
||||
|
||||
#### 1d) Customer-Model: Abfragen abfangen
|
||||
|
||||
```php
|
||||
// app/Models/Customer.php — GlobalScope hinzufügen
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope('not_merged', function ($query) {
|
||||
$query->whereNull('merged_into_id');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Aufwand:** ~2 Tage
|
||||
**Risiko:** Gering — nur Lese-/Schreib-Operationen, keine Struktur-Änderung
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Tabellen umbenennen (Customer → Contact, Lead → Inquiry)
|
||||
|
||||
**Ziel:** Sprechende Namen einführen, interne Logik bereinigen.
|
||||
|
||||
```sql
|
||||
-- Migration:
|
||||
RENAME TABLE customer TO contacts;
|
||||
RENAME TABLE lead TO inquiries;
|
||||
|
||||
-- Booking:
|
||||
ALTER TABLE bookings RENAME COLUMN lead_id TO inquiry_id;
|
||||
-- customer_id bleibt vorerst, wird in Phase 2b auf contact_id umgestellt
|
||||
```
|
||||
|
||||
**Code-Änderungen:**
|
||||
- `App\Models\Customer` → bleibt, aber `protected $table = 'contacts'`
|
||||
- `App\Models\Lead` → bleibt, aber `protected $table = 'inquiries'`
|
||||
- Alle Referenzen auf `lead_id` in Booking → `inquiry_id`
|
||||
- Controller-Routen: `/lead/` → `/inquiry/` (alte URLs: Redirect 301)
|
||||
|
||||
**Aufwand:** ~3 Tage
|
||||
**Risiko:** Mittel — viele Stellen im Code müssen angepasst werden. Sorgfältige Suche mit `grep -r "lead_id\|customer_id\|'lead'\|'customer'"`.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — Participants konsolidieren
|
||||
|
||||
**Ziel:** `lead_participant` und `participant` zu einer Tabelle zusammenführen. Teilnehmerfelder in `lead` und `booking` entfernen.
|
||||
|
||||
```sql
|
||||
CREATE TABLE participants (
|
||||
-- wie in 2.2 beschrieben
|
||||
);
|
||||
|
||||
-- Daten migrieren:
|
||||
INSERT INTO participants (inquiry_id, name, firstname, birthdate, ...)
|
||||
SELECT lead_id, participant_name, participant_firstname, participant_birthdate, ...
|
||||
FROM lead_participant;
|
||||
|
||||
INSERT INTO participants (booking_id, name, firstname, birthdate, ...)
|
||||
SELECT booking_id, participant_name, participant_firstname, participant_birthdate, ...
|
||||
FROM participant;
|
||||
|
||||
-- Hauptreisenden aus den denormalisierten Feldern migrieren:
|
||||
-- (lead.participant_name → participant mit is_lead_contact = true)
|
||||
INSERT INTO participants (inquiry_id, name, firstname, birthdate, is_lead_contact, ...)
|
||||
SELECT id, participant_name, participant_firstname, participant_birthdate, true, ...
|
||||
FROM lead
|
||||
WHERE participant_name IS NOT NULL;
|
||||
```
|
||||
|
||||
**Code-Änderungen:**
|
||||
- `LeadRepository::createBooking()` — kein Kopieren von Participants mehr nötig; bestehende `inquiry_id`-Participants werden automatisch per `booking_id` ergänzt
|
||||
- `lead_participant`, `participant` Tabellen nach Abgleich droppen
|
||||
- Denormalisierte Felder (`participant_name` etc.) in `lead` und `booking` mit `NULL` befüllen, dann in Phase 4 entfernen
|
||||
|
||||
**Aufwand:** ~4 Tage
|
||||
**Risiko:** Mittel — betrifft PDF-Generierung, Buchungsbestätigungen; diese müssen angepasst werden
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — Communications / Notices / Attachments konsolidieren
|
||||
|
||||
**Ziel:** Die 6+ Mail/Notiz/Datei-Tabellen auf 3 gemeinsame Tabellen reduzieren.
|
||||
|
||||
```sql
|
||||
-- Mails migrieren:
|
||||
INSERT INTO communications (inquiry_id, context, from_email, ...)
|
||||
SELECT lead_id, 'inquiry', from_email, ... FROM lead_mail;
|
||||
|
||||
INSERT INTO communications (booking_id, context, from_email, ...)
|
||||
SELECT booking_id, 'booking', from_email, ... FROM customer_mail;
|
||||
|
||||
-- Notizen migrieren:
|
||||
INSERT INTO notices (inquiry_id, from_user_id, message, ...)
|
||||
SELECT lead_id, from_user_id, message, ... FROM lead_notice;
|
||||
|
||||
INSERT INTO notices (booking_id, from_user_id, message, ...)
|
||||
SELECT booking_id, from_user_id, message, ... FROM booking_notice;
|
||||
|
||||
-- Dateien migrieren:
|
||||
INSERT INTO attachments (inquiry_id, disk, path, ...)
|
||||
SELECT lead_id, disk, path, ... FROM lead_file;
|
||||
|
||||
INSERT INTO attachments (booking_id, disk, path, ...)
|
||||
SELECT booking_id, disk, path, ... FROM booking_file;
|
||||
```
|
||||
|
||||
**Code-Änderungen:**
|
||||
- `LeadMailRepository`, `CustomerMailRepository` → gemeinsames `CommunicationRepository`
|
||||
- `LeadFileRepository`, `BookingFileRepository` → gemeinsames `AttachmentRepository`
|
||||
- `MailDirService` (bereits vorhanden) erhält einheitlichen Datenzugriff
|
||||
- Views für Mails/Notizen/Dateien in Lead und Booking können shared werden
|
||||
|
||||
**Aufwand:** ~5 Tage
|
||||
**Risiko:** Hoch — betrifft viele Views und Controller. Muss sehr sorgfältig getestet werden.
|
||||
|
||||
---
|
||||
|
||||
## 4. Neues Beziehungsmodell (Zielbild)
|
||||
|
||||
```
|
||||
Contact (1)
|
||||
├── (N) Inquiry
|
||||
│ ├── (N) Participant [inquiry_id]
|
||||
│ ├── (N) Communication [inquiry_id]
|
||||
│ ├── (N) Notice [inquiry_id]
|
||||
│ ├── (N) Attachment [inquiry_id]
|
||||
│ └── (1) Booking
|
||||
│ ├── (N) Participant [booking_id]
|
||||
│ ├── (N) Communication [booking_id]
|
||||
│ ├── (N) Notice [booking_id]
|
||||
│ ├── (N) Attachment [booking_id]
|
||||
│ └── (N) BookingDocument, BookingInvoice, ...
|
||||
└── (N) Booking (direkt, ohne Umweg über Inquiry)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Controller-Vereinfachung
|
||||
|
||||
Nach den Migrationen können Controller und Views schrittweise zusammengelegt werden:
|
||||
|
||||
| Jetzt | Ziel |
|
||||
|-------|------|
|
||||
| `LeadController` | `InquiryController` |
|
||||
| `CustomerController` | `ContactController` |
|
||||
| `BookingController` | bleibt, aber schlanker |
|
||||
| `LeadMailController` + `CustomerMailController` | `CommunicationController` |
|
||||
| `CustomerFewoMailController` | → `CommunicationController` (mit Fewo-Kontext) |
|
||||
|
||||
Die Detail-Seiten für Lead und Booking sind strukturell fast identisch. Langfristig könnte es eine gemeinsame Basis-View geben, die je nach Kontext unterschiedliche Sektionen einblendet.
|
||||
|
||||
---
|
||||
|
||||
## 6. Empfohlene Reihenfolge
|
||||
|
||||
```
|
||||
Phase 1 (Contact-Deduplizierung)
|
||||
→ Unmittelbarer Gewinn: Kundenhistorie ist korrekt
|
||||
→ Unabhängig von allen anderen Phasen
|
||||
→ Sofort umsetzbar
|
||||
|
||||
Phase 2 (Umbenennung)
|
||||
→ Voraussetzung für Phase 3 & 4
|
||||
→ Kann parallel zur normalen Entwicklung erfolgen
|
||||
|
||||
Phase 3 (Participants)
|
||||
→ Abhängig von Phase 2
|
||||
→ Direkte Auswirkung auf PDF-Generierung — sorgfältig testen
|
||||
|
||||
Phase 4 (Communications/Notices/Attachments)
|
||||
→ Größtes Vorhaben, letzter Schritt
|
||||
→ Kann in Teilschritte aufgesplittet werden
|
||||
(erst Notices, dann Attachments, dann Communications)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Was NICHT geändert werden soll
|
||||
|
||||
- **Buchungs-Detailseite** (`booking.detail`): Bleibt strukturell erhalten — zu komplex für gleichzeitige Überarbeitung
|
||||
- **PDF-Generierung**: Erst nach Phase 3 anpassen (Participants-Konsolidierung)
|
||||
- **Fewo-Buchungsstruktur** (`FewoLodging`, `TravelUserBookingFewo`): Separates Thema, nicht Teil dieses Konzepts
|
||||
- **Legacy-Datenbank** (`mysql_stern`): Keine Änderungen — bleibt read-only
|
||||
|
||||
---
|
||||
|
||||
## 8. Entscheidungen (geklärt)
|
||||
|
||||
| # | Frage | Entscheidung |
|
||||
|---|-------|-------------|
|
||||
| 1 | Welcher Datensatz wird Master bei Duplikaten mit verschiedenen Adressen? | **Immer der neueste** (höchste ID / jüngstes `updated_at`) — vollautomatisch, kein manueller Review |
|
||||
| 2 | Direktbuchung ohne vorherige Anfrage? | **Ja** — `inquiry_id` in `bookings` bleibt `NULL`able; Direktbuchungen sind vorgesehen |
|
||||
| 3 | Was bedeutet `is_rebook`? | **Umbuchung** (nicht Wiederbuchung). Checkbox „Umbuchung abgeschlossen" auf der Anfrage. Feld bleibt auf `inquiries`, keine Sonderbehandlung nötig |
|
||||
| 4 | DSGVO — Einwilligungen pro Datensatz? | **Kein Problem.** Einwilligung war immer Pflichtfeld beim Formular-Submit. Jede Anfrage enthält implizit eine gültige Einwilligung — keine separate Migrationsprüfung nötig |
|
||||
|
||||
### Auswirkungen auf die Migration
|
||||
|
||||
**Zu 1 — Automatische Master-Wahl:**
|
||||
```php
|
||||
// In contacts:merge-duplicates:
|
||||
// Master = Customer::where('email', $email)->orderByDesc('updated_at')->first()
|
||||
// Kein manuelles Eingreifen erforderlich
|
||||
```
|
||||
|
||||
**Zu 2 — Direktbuchung:**
|
||||
```sql
|
||||
-- bookings.inquiry_id bleibt NULL erlaubt (bereits so geplant)
|
||||
-- Neuer Einstiegspunkt im UI: "Buchung direkt anlegen" ohne Lead-Voraussetzung
|
||||
-- BookingController::store() darf inquiry_id weglassen
|
||||
```
|
||||
|
||||
**Zu 3 — is_rebook bleibt auf Inquiry:**
|
||||
```php
|
||||
// Bedeutung: diese Anfrage ist eine Umbuchung einer bestehenden Buchung
|
||||
// is_rebook: bool — Checkbox "Umbuchung abgeschlossen"
|
||||
// Keine Änderung an der Logik, nur Umbenennung mit Phase 2
|
||||
```
|
||||
|
||||
**Zu 4 — DSGVO:**
|
||||
```
|
||||
Keine zusätzlichen Schritte nötig.
|
||||
Beim Mergen zweier Kontakte gilt: beide haben gültige Einwilligungen erteilt.
|
||||
Der zusammengeführte Master-Datensatz ist datenschutzrechtlich unbedenklich.
|
||||
```
|
||||
464
dev/customer-bookings/umsetzung.md
Normal file
464
dev/customer-bookings/umsetzung.md
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
# Umsetzung: Neustrukturierung Customer / Lead / Booking
|
||||
|
||||
**Status:** Phase 1 auf Testsystem abgeschlossen — Contacts-Modul live; **Phase-2-App-Code vorbereitet (deploy-bereit)**
|
||||
**Erstellt:** April 2025
|
||||
**Konzept:** [konzept.md](konzept.md)
|
||||
|
||||
---
|
||||
|
||||
## Aktueller Fortschritt
|
||||
|
||||
| Phase | Status | Deployed auf Test? | Deployed auf Live? |
|
||||
|-------|--------|-------------------|-------------------|
|
||||
| Phase 1 — Contact-Deduplizierung | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein |
|
||||
| Phase 1 — Contacts-Modul (neuer Code) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein |
|
||||
| Phase 2 — App-Code (Models/Repos/Controller/Views auf `contacts`/`inquiries`/`inquiry_id`) | ✅ Abgeschlossen | ⬜ Nein | ⬜ Nein |
|
||||
| Phase 2 — Tabellen-Migrationen (3 Migrationsdateien) | ⬜ Ausstehend (Code ist deploy-ready) | ⬜ Nein | ⬜ Nein |
|
||||
| Phase 3 — Participants konsolidieren | ⬜ Ausstehend | ⬜ Nein | ⬜ Nein |
|
||||
| Phase 4 — Communications / Notices / Attachments | ⬜ Ausstehend | ⬜ Nein | ⬜ Nein |
|
||||
|
||||
### Was in Phase 1 umgesetzt wurde
|
||||
|
||||
**Datenbank-Migrationen (alle auf Testsystem eingespielt):**
|
||||
- `merged_into_id` + `merged_at` auf `customer`-Tabelle → Duplikat-Tracking
|
||||
- `deleted_at` auf `customer`-Tabelle → Soft Delete für neue Contacts-UI
|
||||
|
||||
**Artisan Commands:**
|
||||
- `contacts:find-duplicates` — findet Duplikate nach E-Mail / Name+Geburtsdatum / Name+PLZ
|
||||
- `contacts:merge-duplicates` — führt Duplikate zusammen (dry-run-Modus vorhanden)
|
||||
|
||||
**Contacts-Modul (neuer paralleler Code, alter Code bleibt unberührt):**
|
||||
- `app/Models/Contact.php` — Global Scope schließt Duplikate + gelöschte Kontakte aus
|
||||
- `app/Repositories/ContactRepository.php`
|
||||
- `app/Http/Controllers/ContactController.php` — index, detail, store, destroy, getContacts
|
||||
- `resources/views/contact/` — index, detail, partials
|
||||
- Neue Routes unter `/contacts` und `/contact/*`
|
||||
- Neuer Navigationspunkt "Kontakte" im Sidenav
|
||||
|
||||
**Contacts-Übersicht — Features:**
|
||||
- DataTable mit Schnellsuche (Name, Vorname, E-Mail, Telefon gleichzeitig)
|
||||
- PLZ/Ort-Suche mit OR-Logik über beide Felder
|
||||
- Schnellfilter: Alle / Mit Anfragen / Mit Buchungen
|
||||
- Anfragen- und Buchungs-Zähler pro Kontakt als klickbare Badges
|
||||
- History-Modal: Klick auf Anfragen- oder Buchungs-Badge öffnet Modal mit vollständiger Verlaufsübersicht (AJAX, `_detail_history.blade.php` mit `$modal=true`, Links öffnen in neuem Tab)
|
||||
- Löschen mit Bootstrap-Bestätigungs-Modal + Fehler-Toast (blockiert wenn Anfragen/Buchungen vorhanden)
|
||||
|
||||
---
|
||||
|
||||
## Übersicht der Migrations-Dateien
|
||||
|
||||
Alle Migrations-Dateien liegen in `database/migrations/` und können gezielt auf den Live-Server eingespielt werden. Jede Phase ist **unabhängig deploybar** — das System bleibt nach jeder Phase voll funktionsfähig.
|
||||
|
||||
| Datei | Phase | Beschreibung |
|
||||
|-------|-------|-------------|
|
||||
| `2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php` | 1 | `merged_into_id` + `merged_at` zu `customer` hinzufügen |
|
||||
| `2025_04_15_100002_phase1_add_soft_delete_to_customer_table.php` | 1 | `deleted_at` (Soft Delete) zu `customer` hinzufügen |
|
||||
| `2025_04_15_200001_phase2_rename_customer_to_contacts.php` | 2 | `customer` → `contacts` umbenennen |
|
||||
| `2025_04_15_200002_phase2_rename_lead_to_inquiries.php` | 2 | `lead` → `inquiries` umbenennen |
|
||||
| `2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php` | 2 | `booking.lead_id` → `booking.inquiry_id` |
|
||||
| `2025_04_15_300001_phase3_create_participants_unified_table.php` | 3 | `participants_unified` erstellen + Daten migrieren |
|
||||
| `2025_04_15_300002_phase3_drop_old_participant_tables.php` | 3 | `lead_participant` + `participant` droppen (**nach Test!**) |
|
||||
| `2025_04_15_400001_phase4_create_communications_table.php` | 4 | `communications` erstellen + Daten migrieren |
|
||||
| `2025_04_15_400002_phase4_create_notices_table.php` | 4 | `notices` erstellen + Daten migrieren |
|
||||
| `2025_04_15_400003_phase4_create_attachments_table.php` | 4 | `attachments` erstellen + Daten migrieren |
|
||||
| `2025_04_15_400004_phase4_drop_old_communication_tables.php` | 4 | `lead_mails` + `customer_mails` droppen (**nach Test!**) |
|
||||
| `2025_04_15_400005_phase4_drop_old_notice_tables.php` | 4 | `lead_notices` + `booking_notices` droppen (**nach Test!**) |
|
||||
| `2025_04_15_400006_phase4_drop_old_attachment_tables.php` | 4 | `lead_files` + `booking_files` droppen (**nach Test!**) |
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
```bash
|
||||
# Auf dem Live-Server ausführen:
|
||||
# 1. Datenbank-Backup erstellen (vor JEDER Phase!)
|
||||
mysqldump -u root -ppassword stern_crm > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 2. Migration-Status prüfen
|
||||
php artisan migrate:status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Contact-Deduplizierung
|
||||
|
||||
### Schritt 1: Migration einspielen
|
||||
```bash
|
||||
php artisan migrate --path=database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_100002_phase1_add_soft_delete_to_customer_table.php
|
||||
```
|
||||
|
||||
### Schritt 2: Duplikate analysieren (dry-run)
|
||||
```bash
|
||||
php artisan contacts:find-duplicates
|
||||
# Optional: CSV exportieren
|
||||
php artisan contacts:find-duplicates --export=storage/app/duplicates.csv
|
||||
# Nur bestimmte Konfidenz-Stufe
|
||||
php artisan contacts:find-duplicates --confidence=HIGH
|
||||
```
|
||||
|
||||
### Schritt 3: Duplikate zusammenführen
|
||||
```bash
|
||||
# Erst Vorschau ohne Änderungen
|
||||
php artisan contacts:merge-duplicates --dry-run
|
||||
|
||||
# Dann ausführen (HIGH-Konfidenz zuerst, sicherste Duplikate)
|
||||
php artisan contacts:merge-duplicates --confidence=HIGH --force
|
||||
php artisan contacts:merge-duplicates --confidence=MEDIUM --force
|
||||
|
||||
# LOW nur nach manueller Prüfung der CSV
|
||||
php artisan contacts:merge-duplicates --confidence=LOW --force
|
||||
```
|
||||
|
||||
### Ergebnis-Prüfung
|
||||
```sql
|
||||
-- Wie viele Duplikate wurden zusammengeführt?
|
||||
SELECT COUNT(*) FROM customer WHERE merged_into_id IS NOT NULL;
|
||||
|
||||
-- Gibt es noch aktive Duplikate (gleiche E-Mail)?
|
||||
SELECT email, COUNT(*) FROM customer
|
||||
WHERE merged_into_id IS NULL AND email IS NOT NULL AND email != ''
|
||||
GROUP BY email HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
### Rollback Phase 1
|
||||
```bash
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Tabellen umbenennen
|
||||
|
||||
> **Wichtig:** Vor Phase 2 muss der App-Code bereits auf die neuen Tabellennamen vorbereitet sein,
|
||||
> ODER die Migration wird deployed bevor der Code-Release erfolgt (mit sofortigem Rollback-Plan).
|
||||
>
|
||||
> **Der App-Code ist vorbereitet** (siehe Abschnitt "Erledigte Code-Änderungen"). Empfehlung
|
||||
> für den Live-Deploy: **Maintenance-Mode-Window** — Code-Release und Migrationen atomar
|
||||
> in einem Wartungsfenster einspielen, damit Code und DB immer synchron sind.
|
||||
|
||||
### Erledigte Code-Änderungen (App-Code ist Phase-2-ready)
|
||||
|
||||
**Models (`app/Models/`):**
|
||||
- `Customer.php`: `protected $table = 'contacts';` (mit Doku-Kommentar zu Modul 3 Phase 2)
|
||||
- `Lead.php`: `protected $table = 'inquiries';` (mit Doku-Kommentar — Model-Name bleibt aus Kompatibilitätsgründen)
|
||||
- `Contact.php`: `protected $table = 'contacts';` (ohne Fallback-Kommentar zu "pre-Phase-2")
|
||||
- `Booking.php`:
|
||||
- `@property int $inquiry_id` (ersetzt `$lead_id`)
|
||||
- `$casts['inquiry_id']` + `$fillable` enthalten `'inquiry_id'` statt `'lead_id'`
|
||||
- `lead()`-Relation: `return $this->belongsTo(Lead::class, 'inquiry_id');` — Methodenname bleibt für Legacy-Kompatibilität
|
||||
- Zusätzlich `inquiry()`-Alias-Relation für semantische Klarheit im neuen Code
|
||||
|
||||
**Repositories (`app/Repositories/`):**
|
||||
- `BookingPDFRepository.php`: alle 9 Vorkommen von `$this->model->lead_id` → `$this->model->inquiry_id`. In `booking_documents.lead_id` (Shadow-Feld, Spaltenname bleibt) wird jetzt `$this->model->inquiry_id` geschrieben.
|
||||
- `LeadRepository::createBooking()`: Booking-Create nutzt `'inquiry_id' => $this->model->id`.
|
||||
- `CustomerMailRepository`: `customer_mails.lead_id`-Spalte bleibt erhalten, Wert kommt aus `$booking->inquiry_id`. Alle 6 Vorkommen angepasst.
|
||||
|
||||
**Controllers + Services + Commands:**
|
||||
- `RequestController.php`: alle Booking-Queries `where('lead_id', ...)` → `where('inquiry_id', ...)`; Datatables-SQL-Sort auf `inquiry_id`.
|
||||
- `API/BookingController.php`: API-Feldname `lead_id` bleibt (Abwärtskompatibilität), Wert aus `$booking->inquiry_id`.
|
||||
- `Admin/ReportController.php` + `Admin/ReportProviderController.php`: alle 16 Vorkommen von `$v->booking->lead_id` / `$export->booking->lead_id` → `...->inquiry_id`.
|
||||
- `Admin/ReportLeadsController.php` + `LeadController.php`: qualifizierte Subquery `whereColumn('lead_id', 'lead.id')` → `whereColumn('lead_id', 'inquiries.id')`.
|
||||
- `ContactController.php`: `select('customer.*')` → `select('contacts.*')`, `where('customer.id', ...)` → `where('contacts.id', ...)`, `DB::table('customer')` / `DB::table('lead')` → `DB::table('contacts')` / `DB::table('inquiries')`.
|
||||
- `CustomerController.php`: `select('customer.*')` → `select('contacts.*')`.
|
||||
- `Services/BookingImport.php`: Booking-Create-Array nutzt `'inquiry_id'`.
|
||||
- `Console/Commands/ContactsMergeDuplicates.php` + `ContactsFindDuplicates.php`: alle `DB::table('customer')` / `DB::table('lead')` umgestellt.
|
||||
- `Console/Commands/SyncNewsletterKulturreisen.php`: `metadata['lead_id']`-Key bleibt (ist Payload-Konvention), Wert aus `$booking->inquiry_id`.
|
||||
|
||||
**Views (`resources/views/`):**
|
||||
- `pdf/components/booking_header.blade.php`, `pdf/components/booking_head.blade.php`, `customer/mail/modal-show-mail-inner.blade.php`: `{{ $booking->lead_id }}` → `{{ $booking->inquiry_id }}`.
|
||||
- Alle weiteren View-Treffer zu `lead_id` sind Lead-Kontext (`lead_mails.lead_id`, `lead_notices.lead_id`, HTML-Data-Attribute) und bleiben unverändert — die Spalten werden von Phase 2 nicht umbenannt.
|
||||
|
||||
**Was _nicht_ umgestellt wurde (mit Absicht):**
|
||||
- FK-Spalten `lead_mails.lead_id`, `lead_notices.lead_id`, `lead_files.lead_id`, `lead_participant.lead_id`, `inquiry.lead_id`, `status_history.lead_id`, `customer_mails.lead_id`, `booking_documents.lead_id` — Spaltennamen bleiben, FKs zeigen nach `RENAME TABLE` automatisch auf `inquiries.id`.
|
||||
- API-Feld `lead_id` in `API/BookingController::import`-Response — Abwärtskompatibilität für API-Konsumenten.
|
||||
- Metadaten-Keys `lead_id` in Newsletter-Payload.
|
||||
- HTML-Data-Attribute `data-lead_id` + DataTables-Spaltennamen `lead_id` (UI-seitige Konventionen, kein DB-Bezug).
|
||||
- Routen-Pfade `/lead/*` — reine UX-Arbeit, nicht blockierend für DB-Rename. Nachträglich als 301-Redirect-Paket umsetzbar.
|
||||
- Routen-Namen (`lead_detail`, `lead_index`) — bleiben als Aliase, um Links in Views/Mails/Logs nicht zu brechen.
|
||||
|
||||
### Schritt 1: Migrationen einspielen
|
||||
```bash
|
||||
# Backup erstellen!
|
||||
php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php
|
||||
```
|
||||
|
||||
### Ergebnis-Prüfung
|
||||
```sql
|
||||
-- Tabellen vorhanden?
|
||||
SHOW TABLES LIKE 'contacts';
|
||||
SHOW TABLES LIKE 'inquiries';
|
||||
-- Spalte umbenannt?
|
||||
SHOW COLUMNS FROM booking LIKE 'inquiry_id';
|
||||
-- FK vorhanden?
|
||||
SELECT * FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_NAME = 'booking' AND COLUMN_NAME = 'inquiry_id';
|
||||
```
|
||||
|
||||
### Rollback Phase 2
|
||||
```bash
|
||||
# In umgekehrter Reihenfolge
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Participants konsolidieren
|
||||
|
||||
### Schritt 1: Neue Tabelle erstellen + Daten migrieren
|
||||
```bash
|
||||
# Backup erstellen!
|
||||
php artisan migrate --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
|
||||
```
|
||||
|
||||
### Ergebnis-Prüfung (vor Schritt 2!)
|
||||
```sql
|
||||
-- Zeilenzahlen vergleichen
|
||||
SELECT 'lead_participant' AS src, COUNT(*) AS cnt FROM lead_participant
|
||||
UNION ALL
|
||||
SELECT 'participant', COUNT(*) FROM participant
|
||||
UNION ALL
|
||||
SELECT 'participants_unified (inquiry)', COUNT(*) FROM participants_unified WHERE inquiry_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 'participants_unified (booking)', COUNT(*) FROM participants_unified WHERE booking_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 'participants_unified (lead_contact)', COUNT(*) FROM participants_unified WHERE is_lead_contact = 1;
|
||||
|
||||
-- Stichprobe: Vergleich einzelner Datensätze
|
||||
SELECT * FROM lead_participant LIMIT 5;
|
||||
SELECT * FROM participants_unified WHERE inquiry_id IS NOT NULL LIMIT 5;
|
||||
```
|
||||
|
||||
### Schritt 2: Alte Tabellen droppen (erst nach erfolgreicher Prüfung!)
|
||||
```bash
|
||||
# Backup erstellen!
|
||||
php artisan migrate --path=database/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php
|
||||
```
|
||||
|
||||
> **Achtung:** Schritt 2 ist irreversibel. Nur ausführen wenn:
|
||||
> - participants_unified seit mindestens 1 Woche stabil läuft
|
||||
> - Alle PDF-Generierungen, Buchungsbestätigungen etc. korrekt funktionieren
|
||||
> - Kein Rollback auf Schritt 2 geplant
|
||||
|
||||
### Rollback Phase 3
|
||||
```bash
|
||||
# Nur Schritt 1 rollback-fähig:
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
|
||||
# Schritt 2 ist irreversibel → Backup einspielen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Communications / Notices / Attachments konsolidieren
|
||||
|
||||
> **Voraussetzung:** Phase 2 muss abgeschlossen sein (inquiries + contacts Tabellen müssen existieren).
|
||||
|
||||
### Schritt 1: Neue Tabellen erstellen + Daten migrieren
|
||||
|
||||
Die drei Schritt-1-Migrationen sind voneinander **nicht** unabhängig:
|
||||
- `400003_attachments` setzt `400001_communications` voraus (wegen `communication_id` FK)
|
||||
- Reihenfolge daher: communications → notices → attachments
|
||||
|
||||
```bash
|
||||
# Backup erstellen!
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400001_phase4_create_communications_table.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400002_phase4_create_notices_table.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400003_phase4_create_attachments_table.php
|
||||
```
|
||||
|
||||
### Ergebnis-Prüfung Communications
|
||||
```sql
|
||||
SELECT 'lead_mails' AS src, COUNT(*) FROM lead_mails
|
||||
UNION ALL
|
||||
SELECT 'customer_mails', COUNT(*) FROM customer_mails
|
||||
UNION ALL
|
||||
SELECT 'communications (lead)', COUNT(*) FROM communications WHERE legacy_source = 'lead_mail'
|
||||
UNION ALL
|
||||
SELECT 'communications (cust)', COUNT(*) FROM communications WHERE legacy_source = 'customer_mail';
|
||||
|
||||
-- Reply-Chain korrekt?
|
||||
SELECT COUNT(*) FROM communications WHERE reply_id IS NOT NULL;
|
||||
-- Muss gleich sein wie:
|
||||
SELECT COUNT(*) FROM lead_mails WHERE reply_id IS NOT NULL;
|
||||
-- + COUNT(*) FROM customer_mails WHERE reply_id IS NOT NULL;
|
||||
```
|
||||
|
||||
### Ergebnis-Prüfung Notices
|
||||
```sql
|
||||
SELECT 'lead_notices' AS src, COUNT(*) FROM lead_notices
|
||||
UNION ALL
|
||||
SELECT 'booking_notices', COUNT(*) FROM booking_notices
|
||||
UNION ALL
|
||||
SELECT 'notices (inquiry)', COUNT(*) FROM notices WHERE inquiry_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 'notices (booking)', COUNT(*) FROM notices WHERE booking_id IS NOT NULL;
|
||||
```
|
||||
|
||||
### Ergebnis-Prüfung Attachments
|
||||
```sql
|
||||
SELECT 'lead_files' AS src, COUNT(*) FROM lead_files
|
||||
UNION ALL
|
||||
SELECT 'booking_files', COUNT(*) FROM booking_files
|
||||
UNION ALL
|
||||
SELECT 'attachments (lead)', COUNT(*) FROM attachments WHERE legacy_source = 'lead_file'
|
||||
UNION ALL
|
||||
SELECT 'attachments (booking)', COUNT(*) FROM attachments WHERE legacy_source = 'booking_file';
|
||||
|
||||
-- communication_id korrekt verknüpft?
|
||||
SELECT COUNT(*) FROM lead_files WHERE lead_mail_id IS NOT NULL;
|
||||
-- Muss gleich sein wie:
|
||||
SELECT COUNT(*) FROM attachments WHERE legacy_source = 'lead_file' AND communication_id IS NOT NULL;
|
||||
```
|
||||
|
||||
### Schritt 2: Alte Tabellen droppen (erst nach erfolgreicher Prüfung!)
|
||||
|
||||
Kann unabhängig pro Tabellenpaar eingespielt werden:
|
||||
|
||||
```bash
|
||||
# Backup erstellen!
|
||||
# Communications droppen (achtet auf FK von lead_files → lead_mails wird intern behandelt):
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php
|
||||
|
||||
# Notices droppen:
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php
|
||||
|
||||
# Attachments droppen:
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400006_phase4_drop_old_attachment_tables.php
|
||||
```
|
||||
|
||||
> **Achtung:** Reihenfolge bei Schritt 2:
|
||||
> `400004_communications` muss vor `400006_attachments` laufen,
|
||||
> da `lead_files.lead_mail_id` FK auf `lead_mails` zeigt und erst in 400004 entfernt wird.
|
||||
> (Der FK wird in 400004 automatisch entfernt falls noch vorhanden.)
|
||||
|
||||
### Rollback Phase 4
|
||||
```bash
|
||||
# Schritt 1 rollback (umgekehrte Reihenfolge):
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_400003_phase4_create_attachments_table.php
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_400002_phase4_create_notices_table.php
|
||||
php artisan migrate:rollback --path=database/migrations/2025_04_15_400001_phase4_create_communications_table.php
|
||||
# Schritt 2 ist irreversibel → Backup einspielen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vollständige Deployment-Sequenz (alle Phasen auf einmal)
|
||||
|
||||
Nur empfohlen wenn alle Phasen bereits lokal/staging getestet wurden:
|
||||
|
||||
```bash
|
||||
# 1. Backup
|
||||
mysqldump -u [user] -p stern_crm > backup_pre_migration_$(date +%Y%m%d).sql
|
||||
|
||||
# 2. Maintenance Mode aktivieren
|
||||
php artisan down --render="errors::503"
|
||||
|
||||
# 3. Phase 1 — Deduplizierungsfelder
|
||||
php artisan migrate --path=database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php
|
||||
|
||||
# 4. Phase 1 — Duplikate zusammenführen
|
||||
php artisan contacts:merge-duplicates --confidence=HIGH --force
|
||||
php artisan contacts:merge-duplicates --confidence=MEDIUM --force
|
||||
|
||||
# 5. Phase 2 — Umbenennung
|
||||
php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php
|
||||
|
||||
# 6. Phase 3 — Participants
|
||||
php artisan migrate --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
|
||||
|
||||
# 7. Phase 4 — Communications / Notices / Attachments
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400001_phase4_create_communications_table.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400002_phase4_create_notices_table.php
|
||||
php artisan migrate --path=database/migrations/2025_04_15_400003_phase4_create_attachments_table.php
|
||||
|
||||
# 8. Maintenance Mode deaktivieren
|
||||
php artisan up
|
||||
|
||||
# 9. Testen — BEVOR die Cleanup-Migrationen laufen!
|
||||
# → Daten prüfen (SQL-Queries oben), App manuell testen
|
||||
|
||||
# Cleanup-Migrationen (300002, 400004-400006) SEPARAT nach Testphase einspielen!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten zwischen Phasen
|
||||
|
||||
```
|
||||
Phase 1 ──────────────────────────────────────► unabhängig
|
||||
Phase 2 ──────────────────────────────────────► unabhängig (empfohlen nach Phase 1)
|
||||
Phase 3 ──────────► setzt Phase 2 voraus (FK auf inquiries)
|
||||
Phase 4a (comm) ──► setzt Phase 2 voraus (FK auf inquiries + contacts)
|
||||
Phase 4b (not.) ──► setzt Phase 2 voraus (FK auf inquiries)
|
||||
Phase 4c (att.) ──► setzt Phase 2 + Phase 4a voraus (FK auf communications)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback-Strategie: Zusammenfassung
|
||||
|
||||
| Migration | Rollback möglich? | Methode |
|
||||
|-----------|-------------------|---------|
|
||||
| Phase 1 (merge fields) | Ja | `migrate:rollback` |
|
||||
| Phase 2 (rename) | Ja | `migrate:rollback` (umgekehrte Reihenfolge) |
|
||||
| Phase 3 Schritt 1 (create participants_unified) | Ja | `migrate:rollback` |
|
||||
| Phase 3 Schritt 2 (drop participant tables) | **NEIN** | Backup einspielen |
|
||||
| Phase 4 Schritt 1 (create comm/notices/attach) | Ja | `migrate:rollback` (umgekehrte Reihenfolge) |
|
||||
| Phase 4 Schritt 2 (drop old tables) | **NEIN** | Backup einspielen |
|
||||
|
||||
**Faustregel:** Alle `_create_*` Migrationen sind rollback-fähig. Alle `_drop_*` Migrationen sind irreversibel.
|
||||
|
||||
---
|
||||
|
||||
## Abhängiges Modul: Newsletter (/newsletter)
|
||||
|
||||
Das Newsletter-Modul (`app/Http/Controllers/NewsletterController.php`) ist **nicht direkt betroffen** von den bisherigen Änderungen, muss aber bei zukünftigen Phasen im Blick behalten werden:
|
||||
|
||||
- Eigene Tabelle `newsletter_contacts` mit `customer_id` FK → `customer.id`
|
||||
- Verwendet `App\Models\Customer` (altes Model) über die `customer()`-Beziehung
|
||||
- Export (`/newsletter/export`) läuft über `NewsletterExport` → `NewsletterContact`-Model, nicht direkt über `customer`
|
||||
- Sync-Commands: `contacts:sync-newsletter-kulturreisen`, `contacts:sync-newsletter-ferienwohnungen`
|
||||
|
||||
### Was bei Phase 2 angepasst werden muss
|
||||
|
||||
Wenn `customer` → `contacts` umbenannt wird (Phase 2), muss im Newsletter-Modul:
|
||||
- `App\Models\Customer` → weiterhin verwenden ODER auf `App\Models\Contact` umstellen
|
||||
- FK `newsletter_contacts.customer_id` zeigt weiterhin auf dieselbe Tabelle (jetzt `contacts`) — keine Migration nötig, da FK-Name sich nicht ändert, nur der Tabellenname
|
||||
|
||||
### Aktueller Status
|
||||
- ✅ Newsletter-Export funktioniert unverändert (Phase 1 hat nichts daran geändert)
|
||||
- ✅ Phase-2-Check: `NewsletterContact` belongs-to `Customer::class` — `Customer` verwendet jetzt `$table = 'contacts'`, der FK `newsletter_contacts.customer_id` bleibt stabil. Keine Code-Änderung im Newsletter-Modul nötig.
|
||||
|
||||
---
|
||||
|
||||
## Code-Änderungen nach den Migrationen
|
||||
|
||||
Die Migrationen erstellen nur die neuen Tabellen und migrieren die Daten.
|
||||
Der App-Code muss separat angepasst werden (nicht Teil der Migrations selbst):
|
||||
|
||||
### Nach Phase 2
|
||||
- ✅ `app/Models/Customer.php`: `protected $table = 'contacts';`
|
||||
- ✅ `app/Models/Lead.php`: `protected $table = 'inquiries';`
|
||||
- ✅ `app/Models/Contact.php`: `protected $table = 'contacts';`
|
||||
- ✅ `app/Models/Booking.php`: `lead_id` → `inquiry_id` (`$casts`, `$fillable`, `@property`, `lead()`-Relation mit expliziter FK-Spalte `inquiry_id`, neuer `inquiry()`-Alias)
|
||||
- ✅ Alle Repositories, Controllers, Services, Commands und Views im Booking-Kontext auf `inquiry_id` umgestellt
|
||||
- ✅ Raw-SQL (`DB::table('customer')` / `DB::table('lead')`, `select('customer.*')` / `select('lead.*')`, `whereColumn(..., 'lead.id')`) auf `contacts` / `inquiries` umgestellt
|
||||
- ⬜ Route-URLs: `/lead/` → `/inquiry/` (301 Redirects) — nachgelagert, nicht blockierend für DB-Rename
|
||||
|
||||
### Nach Phase 3
|
||||
- `LeadRepository::createBooking()`: Participants nicht mehr kopieren — bestehende `inquiry_id`-Einträge werden per `booking_id` ergänzt
|
||||
- PDF-Generierung: auf `participants_unified` umstellen statt `lead_participant`/`participant`
|
||||
- Nach Cleanup (Schritt 2): alle direkten Queries auf `lead_participant`/`participant` entfernen
|
||||
|
||||
### Nach Phase 4
|
||||
- `LeadMailRepository` + `CustomerMailRepository` → `CommunicationRepository` (neu erstellen)
|
||||
- `LeadFileRepository` + `BookingFileRepository` → `AttachmentRepository` (neu erstellen)
|
||||
- `LeadNoticeRepository` + `BookingNoticeRepository` → `NoticeRepository` (neu erstellen)
|
||||
- `MailDirService`: Datenzugriff auf `communications` umstellen
|
||||
- Views für Mails/Notizen/Dateien in Lead und Booking können nach Umstellung geteilt werden
|
||||
Loading…
Add table
Add a link
Reference in a new issue