20-02-2026
This commit is contained in:
parent
a8b395e20d
commit
a00c42e770
252 changed files with 28785 additions and 8907 deletions
2302
dev/2026-01-22/next-steps.md
Normal file
2302
dev/2026-01-22/next-steps.md
Normal file
File diff suppressed because it is too large
Load diff
445
dev/2026-01-23/packstation-return-label-restriction.md
Normal file
445
dev/2026-01-23/packstation-return-label-restriction.md
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
# Packstation Return-Label Restriction
|
||||
|
||||
**Datum:** 23.01.2026
|
||||
**Status:** ✅ Implementiert
|
||||
|
||||
## Problem
|
||||
|
||||
**User-Feedback:**
|
||||
"Wenn ich ein Label habe aus einer Packstation, kann ich kein Return Label erzeugen."
|
||||
|
||||
**Fehler:**
|
||||
|
||||
```
|
||||
Fehler beim Erstellen des Retourenlabels: DHL API error (400): 0 of 1 shipment successfully printed.
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
### DHL Packstation Limitierung
|
||||
|
||||
**Packstationen** sind **Einwegadressen**:
|
||||
|
||||
- ✅ Sendungen **AN** eine Packstation: Erlaubt
|
||||
- ❌ Sendungen **VON** einer Packstation: **Nicht erlaubt**
|
||||
|
||||
### Return-Label Logik
|
||||
|
||||
Bei einem Return-Label werden Sender/Empfänger vertauscht:
|
||||
|
||||
```
|
||||
Original-Label:
|
||||
Sender: Shop (Musterstraße 1, Berlin)
|
||||
Empfänger: Kunde (Packstation 145, Hamburg) ✅
|
||||
|
||||
Return-Label (vertauscht):
|
||||
Sender: Kunde (Packstation 145, Hamburg) ❌
|
||||
Empfänger: Shop (Musterstraße 1, Berlin)
|
||||
```
|
||||
|
||||
**Problem:** Packstation kann nicht als Sender verwendet werden!
|
||||
|
||||
## DHL API Verhalten
|
||||
|
||||
### Request Payload (Fallback via ShippingService)
|
||||
|
||||
```json
|
||||
{
|
||||
"profile": "STANDARD_GRUPPENPROFIL",
|
||||
"shipments": [
|
||||
{
|
||||
"product": "V01PAK",
|
||||
"shipper": {
|
||||
"name1": "Max Mustermann",
|
||||
"addressStreet": "Packstation",
|
||||
"addressHouse": "145",
|
||||
"postalCode": "20095",
|
||||
"city": "Hamburg",
|
||||
"country": "DEU",
|
||||
"postNumber": "1234567890" // ❌ Packstation als Sender!
|
||||
},
|
||||
"consignee": {
|
||||
"name1": "Mein Shop",
|
||||
"addressStreet": "Musterstraße",
|
||||
"addressHouse": "1",
|
||||
"postalCode": "10115",
|
||||
"city": "Berlin",
|
||||
"country": "DEU"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### DHL API Response
|
||||
|
||||
```json
|
||||
{
|
||||
"Status": {
|
||||
"statusCode": 400,
|
||||
"statusText": "Bad Request"
|
||||
},
|
||||
"labelData": "",
|
||||
"shipmentNumber": "",
|
||||
"errors": [
|
||||
{
|
||||
"message": "0 of 1 shipment successfully printed."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Lösung
|
||||
|
||||
### 1. Backend-Validation (Controller)
|
||||
|
||||
**File:** `app/Http/Controllers/DhlShipmentController.php`
|
||||
|
||||
```php
|
||||
public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse
|
||||
{
|
||||
// ... existing checks ...
|
||||
|
||||
// Check if recipient is a Packstation (cannot be used as return sender)
|
||||
$recipient = is_string($shipment->recipient)
|
||||
? json_decode($shipment->recipient, true)
|
||||
: $shipment->recipient;
|
||||
|
||||
$hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
|
||||
|
||||
if ($hasPostNumber) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Retourenlabels können nicht für Packstation-Sendungen erstellt werden. ' .
|
||||
'DHL erlaubt keine Sendungen VON einer Packstation. ' .
|
||||
'Der Kunde muss die Retoure über eine normale Adresse versenden.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// ... continue with return label creation ...
|
||||
}
|
||||
```
|
||||
|
||||
**Prüflogik:**
|
||||
|
||||
- ✅ Prüft ob `postnumber` oder `postNumber` im Empfänger-JSON existiert
|
||||
- ✅ Gibt HTTP 422 (Unprocessable Entity) zurück
|
||||
- ✅ Klare, verständliche Fehlermeldung
|
||||
|
||||
### 2. UI - DataTable (Cockpit)
|
||||
|
||||
**File:** `app/Http/Controllers/DhlShipmentController.php`
|
||||
|
||||
```php
|
||||
->addColumn('actions', function ($shipment) {
|
||||
// ...
|
||||
|
||||
// Return label button (only for non-Packstation outbound shipments)
|
||||
if ($shipment->type == 'outbound' && ! $shipment->returns()->count()) {
|
||||
$recipient = is_string($shipment->recipient)
|
||||
? json_decode($shipment->recipient, true)
|
||||
: $shipment->recipient;
|
||||
|
||||
$hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
|
||||
|
||||
if (!$hasPostNumber) {
|
||||
// Regular address: Show active button
|
||||
$buttons .= '<button type="button" class="btn btn-sm btn-outline-info create-return-btn"
|
||||
data-shipment-id="' . $shipment->id . '"
|
||||
data-toggle="tooltip"
|
||||
title="Retourenlabel erstellen">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>';
|
||||
} else {
|
||||
// Packstation: Show disabled button with explanation
|
||||
$buttons .= '<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
disabled
|
||||
data-toggle="tooltip"
|
||||
title="Retourenlabels nicht möglich für Packstation-Sendungen">
|
||||
<i class="fas fa-undo text-muted"></i>
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**UI-Verhalten:**
|
||||
|
||||
- ✅ **Packstation:** Disabled Button mit Tooltip
|
||||
- ✅ **Normale Adresse:** Aktiver Button
|
||||
- ✅ Icon wird grau dargestellt bei Packstation
|
||||
|
||||
### 3. UI - Order Detail View
|
||||
|
||||
**File:** `resources/views/admin/sales/_detail_dhl_shipments.blade.php`
|
||||
|
||||
```blade
|
||||
@if($shipment->type === 'outbound' && !$shipment->returns->count())
|
||||
@php
|
||||
$recipient = is_string($shipment->recipient)
|
||||
? json_decode($shipment->recipient, true)
|
||||
: $shipment->recipient;
|
||||
$hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
|
||||
@endphp
|
||||
|
||||
@if(!$hasPostNumber)
|
||||
<button type="button"
|
||||
class="btn btn-outline-info dhl-create-return-btn"
|
||||
data-shipment-id="{{ $shipment->id }}"
|
||||
title="Retourenlabel erstellen">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
@else
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
disabled
|
||||
title="Retourenlabels nicht möglich für Packstation-Sendungen">
|
||||
<i class="fas fa-undo text-muted"></i>
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
```
|
||||
|
||||
### 4. UI - Shipment Detail View
|
||||
|
||||
**File:** `resources/views/admin/dhl/show.blade.php`
|
||||
|
||||
```blade
|
||||
@if($shipment->type == 'outbound' && !$shipment->returns->count())
|
||||
@php
|
||||
$recipient = is_string($shipment->recipient)
|
||||
? json_decode($shipment->recipient, true)
|
||||
: $shipment->recipient;
|
||||
$hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
|
||||
@endphp
|
||||
|
||||
@if(!$hasPostNumber)
|
||||
<button type="button"
|
||||
class="btn btn-info mr-2"
|
||||
id="create-return-btn"
|
||||
data-shipment-id="{{ $shipment->id }}">
|
||||
<i class="fas fa-undo"></i> Retourenlabel erstellen
|
||||
</button>
|
||||
@else
|
||||
<button type="button"
|
||||
class="btn btn-secondary mr-2"
|
||||
disabled
|
||||
title="Retourenlabels nicht möglich für Packstation-Sendungen">
|
||||
<i class="fas fa-undo"></i> Packstation-Sendung
|
||||
</button>
|
||||
<small class="text-muted d-block mb-2">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Retourenlabels können nicht für Packstation-Sendungen erstellt werden,
|
||||
da DHL keine Sendungen VON einer Packstation erlaubt.
|
||||
</small>
|
||||
@endif
|
||||
@endif
|
||||
```
|
||||
|
||||
**Besonderheit Detail-View:**
|
||||
|
||||
- ✅ Zusätzlicher Info-Text unter dem Button
|
||||
- ✅ Icon-Indikator für besseres Verständnis
|
||||
|
||||
## Packstation-Erkennung
|
||||
|
||||
### Datenstruktur
|
||||
|
||||
**DhlShipment Model:**
|
||||
|
||||
```php
|
||||
$shipment->recipient = '{
|
||||
"firstname": "Max",
|
||||
"lastname": "Mustermann",
|
||||
"street": "Packstation 145",
|
||||
"postalcode": "20095",
|
||||
"city": "Hamburg",
|
||||
"country": "DE",
|
||||
"postnumber": "1234567890" // ← Identifikator!
|
||||
}'
|
||||
```
|
||||
|
||||
### Detection Logic
|
||||
|
||||
```php
|
||||
$recipient = is_string($shipment->recipient)
|
||||
? json_decode($shipment->recipient, true)
|
||||
: $shipment->recipient;
|
||||
|
||||
// Check both possible keys (case-insensitive)
|
||||
$hasPostNumber = !empty(
|
||||
$recipient['postnumber'] ??
|
||||
$recipient['postNumber'] ??
|
||||
''
|
||||
);
|
||||
|
||||
if ($hasPostNumber) {
|
||||
// This is a Packstation delivery
|
||||
}
|
||||
```
|
||||
|
||||
**Keys geprüft:**
|
||||
|
||||
- `postnumber` (lowercase)
|
||||
- `postNumber` (camelCase)
|
||||
|
||||
## Testing
|
||||
|
||||
### Test 1: Packstation-Sendung - API Call blockiert
|
||||
|
||||
```bash
|
||||
# Via Tinker
|
||||
$shipment = DhlShipment::find(24); // Packstation-Sendung
|
||||
$controller = new App\Http\Controllers\DhlShipmentController();
|
||||
$response = $controller->createReturnLabel(new Illuminate\Http\Request(), $shipment);
|
||||
|
||||
# Expected Response:
|
||||
{
|
||||
"success": false,
|
||||
"message": "Retourenlabels können nicht für Packstation-Sendungen erstellt werden.
|
||||
DHL erlaubt keine Sendungen VON einer Packstation.
|
||||
Der Kunde muss die Retoure über eine normale Adresse versenden."
|
||||
}
|
||||
# HTTP Status: 422
|
||||
```
|
||||
|
||||
### Test 2: Packstation-Sendung - Button disabled
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. Cockpit öffnen
|
||||
2. Packstation-Sendung suchen (ID 24)
|
||||
3. ✅ Return-Button ist grau/disabled
|
||||
4. ✅ Tooltip zeigt "Retourenlabels nicht möglich für Packstation-Sendungen"
|
||||
5. ✅ Icon ist grau (`text-muted`)
|
||||
|
||||
### Test 3: Normale Sendung - Return funktioniert
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. Normale Outbound-Sendung auswählen (kein postNumber)
|
||||
2. ✅ Return-Button ist aktiv/blau
|
||||
3. Button klicken
|
||||
4. ✅ Return-Label wird erfolgreich erstellt
|
||||
|
||||
### Test 4: Detail-View mit Info-Text
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. Packstation-Sendung Detail-View öffnen (ID 24)
|
||||
2. ✅ "Packstation-Sendung" Button ist disabled
|
||||
3. ✅ Info-Text sichtbar unter dem Button
|
||||
4. ✅ Erklärt warum Return nicht möglich ist
|
||||
|
||||
## Database Check
|
||||
|
||||
```sql
|
||||
-- Find all Packstation shipments
|
||||
SELECT
|
||||
id,
|
||||
type,
|
||||
dhl_shipment_no,
|
||||
JSON_EXTRACT(recipient, '$.postnumber') as postnumber,
|
||||
JSON_EXTRACT(recipient, '$.street') as street
|
||||
FROM dhl_package_shipments
|
||||
WHERE
|
||||
type = 'outbound'
|
||||
AND (
|
||||
JSON_EXTRACT(recipient, '$.postnumber') IS NOT NULL
|
||||
OR JSON_EXTRACT(recipient, '$.postNumber') IS NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
**Example Results:**
|
||||
|
||||
```
|
||||
id | type | dhl_shipment_no | postnumber | street
|
||||
----|----------|---------------------------|--------------|------------------
|
||||
24 | outbound | 0034043333301020021029524 | 1234567890 | "Packstation 145"
|
||||
```
|
||||
|
||||
## User Experience
|
||||
|
||||
### Vorher (Fehler)
|
||||
|
||||
```
|
||||
1. User klickt "Retourenlabel erstellen"
|
||||
2. API Call wird gesendet
|
||||
3. ❌ DHL API error (400): 0 of 1 shipment successfully printed
|
||||
4. ❌ Kryptische Fehlermeldung
|
||||
5. ❌ User weiß nicht warum es fehlschlägt
|
||||
```
|
||||
|
||||
### Nachher (Proaktive Verhinderung)
|
||||
|
||||
```
|
||||
1. User öffnet Packstation-Sendung
|
||||
2. ✅ Button ist disabled mit Tooltip
|
||||
3. ✅ Info-Text erklärt warum nicht möglich
|
||||
4. ✅ Kein fehlgeschlagener API Call
|
||||
5. ✅ Klarheit für den User
|
||||
```
|
||||
|
||||
## Alternative Lösungen (nicht implementiert)
|
||||
|
||||
### Option 1: Shop-Adresse als Return-Empfänger
|
||||
|
||||
**Idee:** Bei Packstation-Sendungen automatisch Shop-Adresse als Rücksendeadresse verwenden.
|
||||
|
||||
**Problem:**
|
||||
|
||||
- Kunde muss Paket zur Post/DHL-Filiale bringen
|
||||
- Nicht transparent für Kunden
|
||||
- Komplexere Logik
|
||||
|
||||
**Entscheidung:** ❌ Nicht umgesetzt
|
||||
|
||||
### Option 2: QR-Code Return ohne Label
|
||||
|
||||
**Idee:** DHL QR-Code Return für Packstation-Kunden.
|
||||
|
||||
**Problem:**
|
||||
|
||||
- Erfordert separates DHL API Produkt
|
||||
- Nicht in allen Accounts verfügbar
|
||||
- Separate Implementierung nötig
|
||||
|
||||
**Entscheidung:** ❌ Nicht umgesetzt (könnte zukünftig ergänzt werden)
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
1. ✅ `app/Http/Controllers/DhlShipmentController.php`
|
||||
|
||||
- Zeile 427-437: Backend-Validation für Packstation-Check
|
||||
- Zeile 224-236: DataTable Button-Logik mit Packstation-Check
|
||||
|
||||
2. ✅ `resources/views/admin/sales/_detail_dhl_shipments.blade.php`
|
||||
|
||||
- Zeile 108-127: Blade-Logic für Return-Button (disabled bei Packstation)
|
||||
|
||||
3. ✅ `resources/views/admin/dhl/show.blade.php`
|
||||
- Zeile 161-182: Detail-View mit disabled Button + Info-Text
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Backend-Validation:** Implementiert
|
||||
✅ **UI-Anpassungen (Cockpit):** Implementiert
|
||||
✅ **UI-Anpassungen (Order Detail):** Implementiert
|
||||
✅ **UI-Anpassungen (Shipment Detail):** Implementiert
|
||||
✅ **Dokumentation:** Erstellt
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. [ ] Testen: Packstation-Sendung im Cockpit prüfen
|
||||
2. [ ] Testen: Return-Button sollte disabled sein
|
||||
3. [ ] Testen: Normale Sendung kann weiterhin Return-Label erstellen
|
||||
4. [ ] Optional: Kunde über alternative Return-Optionen informieren (Email)
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Proaktive UI-Validierung:** Buttons disablen ist besser als Fehler zeigen
|
||||
2. **Klare Fehlermeldungen:** User sollten verstehen WARUM etwas nicht geht
|
||||
3. **DHL Limitierungen:** Packstationen sind Einweg-Adressen (nur AN, nicht VON)
|
||||
4. **Multiple Keys prüfen:** `postnumber` vs `postNumber` - beide abfangen
|
||||
415
dev/2026-01-23/packstation-return-with-billing-address.md
Normal file
415
dev/2026-01-23/packstation-return-with-billing-address.md
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
php# Packstation Return-Labels mit Rechnungsadresse
|
||||
|
||||
**Datum:** 23.01.2026
|
||||
**Status:** ✅ Implementiert
|
||||
|
||||
## Problem
|
||||
|
||||
**User-Anfrage:**
|
||||
"Wir müssen auch Return Labels erstellen können wo vorher eine Sendung an eine Packstation ging. Hier nehmen wir einfach als Absender die Adresse von der Bestellung."
|
||||
|
||||
**Bisherige Lösung:**
|
||||
Return-Labels für Packstation-Sendungen waren blockiert, da DHL keine Sendungen **VON** einer Packstation erlaubt.
|
||||
|
||||
## Neue Lösung
|
||||
|
||||
**Intelligenter Fallback:** Bei Packstation-Sendungen wird die **Rechnungsadresse** aus der Bestellung als Return-Absender verwendet.
|
||||
|
||||
### Adress-Logik
|
||||
|
||||
```
|
||||
Original-Label:
|
||||
Sender: Shop (Leinfeld 2, Kirchhaslach)
|
||||
Empfänger: Kunde (Packstation 145, Bielefeld) ✅
|
||||
PostNumber: 1234567890
|
||||
|
||||
Return-Label:
|
||||
Sender: Kunde (In der Lake 4, Bielefeld) ✅ ← Rechnungsadresse!
|
||||
Empfänger: Shop (Leinfeld 2, Kirchhaslach)
|
||||
```
|
||||
|
||||
**Wichtig:** Nicht die Packstation, sondern die normale Adresse des Kunden wird verwendet!
|
||||
|
||||
## Implementierung
|
||||
|
||||
### 1. Controller - Packstation-Erkennung
|
||||
|
||||
**File:** `app/Http/Controllers/DhlShipmentController.php`
|
||||
|
||||
```php
|
||||
private function createReturnLabelSync(DhlShipment $shipment): array
|
||||
{
|
||||
// ...
|
||||
$recipient = $shipment->recipient ?? [];
|
||||
|
||||
// Check if this is a Packstation delivery
|
||||
$hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
|
||||
|
||||
if ($hasPostNumber) {
|
||||
Log::info('[DHL Controller] Packstation detected - using billing address');
|
||||
|
||||
// Load billing address from order
|
||||
$shippingUser = $order->shopping_user;
|
||||
$shipperAddress = $this->getBillingAddressForReturn($shippingUser, $recipient);
|
||||
} else {
|
||||
// Use original recipient address (normal delivery)
|
||||
$shipperAddress = [
|
||||
'name' => trim(($recipient['firstname'] ?? '') . ' ' . ($recipient['lastname'] ?? '')),
|
||||
'street' => $recipient['street'] ?? '',
|
||||
// ...
|
||||
];
|
||||
}
|
||||
|
||||
$returnData = [
|
||||
'shipper' => $shipperAddress, // ← Billing address for Packstation!
|
||||
'consignee' => $dhlConfig['sender'], // ← Shop address
|
||||
];
|
||||
|
||||
$result = $returnsService->createReturn($returnData);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Billing-Adresse abrufen
|
||||
|
||||
**File:** `app/Http/Controllers/DhlShipmentController.php`
|
||||
|
||||
```php
|
||||
private function getBillingAddressForReturn($shippingUser, array $recipient): array
|
||||
{
|
||||
if (!$shippingUser) {
|
||||
Log::warning('[DHL Controller] No shipping user found');
|
||||
// Fallback to recipient data (without Packstation)
|
||||
return [
|
||||
'name' => trim(($recipient['firstname'] ?? '') . ' ' . ($recipient['lastname'] ?? '')),
|
||||
'street' => 'Adresse fehlt',
|
||||
// ...
|
||||
];
|
||||
}
|
||||
|
||||
// Parse billing address to extract street and house number
|
||||
$billingAddress = trim($shippingUser->billing_address ?? '');
|
||||
$street = $billingAddress;
|
||||
$houseNumber = '';
|
||||
|
||||
// Extract house number: "In der Lake 4" → street: "In der Lake", house: "4"
|
||||
if (preg_match('/^(.+?)\s+(\d+[a-zA-Z]?[-\/\d]*)$/u', $billingAddress, $matches)) {
|
||||
$street = trim($matches[1]);
|
||||
$houseNumber = trim($matches[2]);
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => trim(($shippingUser->billing_firstname ?? '') . ' ' .
|
||||
($shippingUser->billing_lastname ?? '')),
|
||||
'name2' => $shippingUser->billing_company ?? '',
|
||||
'street' => $street,
|
||||
'houseNumber' => $houseNumber,
|
||||
'postalCode' => $shippingUser->billing_zipcode ?? '',
|
||||
'city' => $shippingUser->billing_city ?? '',
|
||||
'country' => $shippingUser->billing_country?->code ?? 'DEU',
|
||||
'email' => $shippingUser->billing_email ?? '',
|
||||
'phone' => $shippingUser->billing_phone ?? '',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- ✅ Extrahiert Straße und Hausnummer automatisch
|
||||
- ✅ Verwendet Billing-Daten aus `shopping_users` Tabelle
|
||||
- ✅ Fallback wenn keine User-Daten vorhanden
|
||||
- ✅ Regex unterstützt verschiedene Formate: "Str. 4", "Str. 4a", "Str. 4-6"
|
||||
|
||||
### 3. Queue Job - Gleiche Logik
|
||||
|
||||
**File:** `app/Jobs/CreateReturnLabelJob.php`
|
||||
|
||||
```php
|
||||
private function prepareReturnLabelData(array $dhlConfig): array
|
||||
{
|
||||
$order = $this->originalShipment->shoppingOrder;
|
||||
$recipient = $this->originalShipment->recipient ?? [];
|
||||
|
||||
// Check if this is a Packstation delivery
|
||||
$hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
|
||||
|
||||
if ($hasPostNumber) {
|
||||
Log::info('[DHL Queue] Packstation detected - using billing address');
|
||||
$shippingUser = $order->shopping_user;
|
||||
$shipperAddress = $this->getBillingAddressForReturn($shippingUser, $recipient);
|
||||
} else {
|
||||
$shipperAddress = [/* recipient address */];
|
||||
}
|
||||
|
||||
return [
|
||||
'shipper' => $shipperAddress,
|
||||
'consignee' => $dhlConfig['sender'],
|
||||
];
|
||||
}
|
||||
|
||||
// Gleiche getBillingAddressForReturn() Methode wie im Controller
|
||||
```
|
||||
|
||||
### 4. UI - Blockierung entfernt
|
||||
|
||||
**Vorher:** Return-Button war disabled für Packstation-Sendungen
|
||||
**Nachher:** Return-Button ist aktiv für ALLE Outbound-Sendungen
|
||||
|
||||
**Files geändert:**
|
||||
|
||||
1. `app/Http/Controllers/DhlShipmentController.php` - DataTable Actions
|
||||
2. `resources/views/admin/sales/_detail_dhl_shipments.blade.php` - Order Detail
|
||||
3. `resources/views/admin/dhl/show.blade.php` - Shipment Detail
|
||||
|
||||
```php
|
||||
// Vorher (disabled für Packstation):
|
||||
if ($shipment->type == 'outbound' && !$shipment->returns()->count()) {
|
||||
$hasPostNumber = !empty($recipient['postnumber']);
|
||||
if (!$hasPostNumber) {
|
||||
$buttons .= '<button>Return</button>'; // Nur für normale Adressen
|
||||
} else {
|
||||
$buttons .= '<button disabled>Return</button>'; // Disabled für Packstation
|
||||
}
|
||||
}
|
||||
|
||||
// Nachher (aktiv für alle):
|
||||
if ($shipment->type == 'outbound' && !$shipment->returns()->count()) {
|
||||
$buttons .= '<button>Return</button>'; // Für alle aktiv!
|
||||
}
|
||||
```
|
||||
|
||||
## Datenfluss
|
||||
|
||||
### Packstation-Sendung Return-Label
|
||||
|
||||
```
|
||||
1. User klickt "Retourenlabel erstellen" für Packstation-Sendung
|
||||
↓
|
||||
2. Controller/Job erkennt: $recipient['postnumber'] existiert
|
||||
↓
|
||||
3. Lade Billing-Adresse aus $order->shopping_user
|
||||
↓
|
||||
4. Extrahiere: Street: "In der Lake", HouseNumber: "4"
|
||||
↓
|
||||
5. Erstelle Return-Label:
|
||||
- Shipper: Kevin Adametz, In der Lake 4, 33739 Bielefeld
|
||||
- Consignee: mivita care gmbh, Leinfeld 2, 87755 Kirchhaslach
|
||||
↓
|
||||
6. ReturnsService verwendet Fallback-Methode (ShippingService)
|
||||
↓
|
||||
7. DHL API akzeptiert Label ✅
|
||||
↓
|
||||
8. Type='return' wird in DB gesetzt
|
||||
```
|
||||
|
||||
### Normale Sendung Return-Label
|
||||
|
||||
```
|
||||
1. User klickt "Retourenlabel erstellen" für normale Sendung
|
||||
↓
|
||||
2. Controller/Job erkennt: Kein $recipient['postnumber']
|
||||
↓
|
||||
3. Verwende Recipient-Adresse direkt (aus $shipment->recipient)
|
||||
↓
|
||||
4. Erstelle Return-Label:
|
||||
- Shipper: Original recipient address
|
||||
- Consignee: Shop address
|
||||
↓
|
||||
5. ReturnsService verwendet Fallback-Methode
|
||||
↓
|
||||
6. Label wird erstellt ✅
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test 1: Packstation Return-Label
|
||||
|
||||
```bash
|
||||
# Via Tinker
|
||||
$shipment = DhlShipment::find(24); // Packstation shipment
|
||||
$controller = new DhlShipmentController();
|
||||
$response = $controller->createReturnLabel(new Request(), $shipment);
|
||||
|
||||
# Expected Log Output:
|
||||
[DHL Controller] Packstation detected - using billing address for return sender
|
||||
shipment_id: 24
|
||||
order_id: 45078
|
||||
|
||||
[DHL Returns] Using regular Shipping API as fallback
|
||||
shipper: {
|
||||
"name": "Kevin Adametz",
|
||||
"street": "In der Lake",
|
||||
"houseNumber": "4",
|
||||
"postalCode": "33739",
|
||||
"city": "Bielefeld",
|
||||
"country": "DE"
|
||||
}
|
||||
|
||||
[DHL Returns] Return label created successfully
|
||||
returnNumber: "0034043333301020021029XXX"
|
||||
```
|
||||
|
||||
### Test 2: Verify Data
|
||||
|
||||
```bash
|
||||
cd /var/www/html && php -r "
|
||||
require 'vendor/autoload.php';
|
||||
\$app = require_once 'bootstrap/app.php';
|
||||
\$app->make('Illuminate\\Contracts\\Console\\Kernel')->bootstrap();
|
||||
|
||||
\$shipment = DB::table('dhl_package_shipments')->where('id', 24)->first();
|
||||
\$order = DB::table('shopping_orders')->where('id', \$shipment->order_id)->first();
|
||||
\$user = DB::table('shopping_users')->where('id', \$order->shopping_user_id)->first();
|
||||
\$recipient = json_decode(\$shipment->recipient, true);
|
||||
|
||||
echo 'Original: ' . \$recipient['street'] . ' (PostNumber: ' . (\$recipient['postnumber'] ?? 'none') . ')' . PHP_EOL;
|
||||
echo 'Return: ' . \$user->billing_address . ' ✅' . PHP_EOL;
|
||||
"
|
||||
|
||||
# Output:
|
||||
Original: Packstation (PostNumber: 1234567890)
|
||||
Return: In der Lake 4 ✅
|
||||
```
|
||||
|
||||
### Test 3: UI Button
|
||||
|
||||
**Schritte:**
|
||||
|
||||
1. Cockpit öffnen
|
||||
2. Packstation-Sendung (ID 24) suchen
|
||||
3. ✅ Return-Button ist AKTIV (blau, nicht grau)
|
||||
4. Button klicken
|
||||
5. ✅ Return-Label wird erstellt
|
||||
6. ✅ Absender ist "In der Lake 4" (nicht "Packstation")
|
||||
|
||||
## Address Parsing
|
||||
|
||||
### Regex Pattern
|
||||
|
||||
```php
|
||||
preg_match('/^(.+?)\s+(\d+[a-zA-Z]?[-\/\d]*)$/u', $address, $matches)
|
||||
```
|
||||
|
||||
**Unterstützte Formate:**
|
||||
|
||||
```
|
||||
"In der Lake 4" → street: "In der Lake", house: "4"
|
||||
"Musterstraße 123" → street: "Musterstraße", house: "123"
|
||||
"Hauptstr. 5a" → street: "Hauptstr.", house: "5a"
|
||||
"Bergweg 10-12" → street: "Bergweg", house: "10-12"
|
||||
"Dorfstr. 3/5" → street: "Dorfstr.", house: "3/5"
|
||||
```
|
||||
|
||||
**Fallback:** Wenn kein Match, gesamter String = street, houseNumber = leer
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
1. ✅ `app/Http/Controllers/DhlShipmentController.php`
|
||||
|
||||
- Zeile 418-438: Packstation-Blockierung entfernt
|
||||
- Zeile 494-541: Packstation-Erkennung + Billing-Adresse
|
||||
- Zeile 543-589: getBillingAddressForReturn() Methode
|
||||
|
||||
2. ✅ `app/Jobs/CreateReturnLabelJob.php`
|
||||
|
||||
- Zeile 135-167: Packstation-Erkennung + Billing-Adresse
|
||||
- Zeile 190-235: getBillingAddressForReturn() Methode
|
||||
|
||||
3. ✅ `resources/views/admin/sales/_detail_dhl_shipments.blade.php`
|
||||
|
||||
- Zeile 108-114: Disabled-Logic entfernt
|
||||
|
||||
4. ✅ `resources/views/admin/dhl/show.blade.php`
|
||||
|
||||
- Zeile 161-166: Disabled-Logic + Info-Text entfernt
|
||||
|
||||
5. ✅ `dev/23-01-2026/packstation-return-label-restriction.md`
|
||||
|
||||
- Alte Dokumentation (Blockierung) - kann entfernt werden
|
||||
|
||||
6. ✅ `dev/23-01-2026/packstation-return-with-billing-address.md`
|
||||
- Neue Dokumentation (Billing-Adresse-Fallback)
|
||||
|
||||
## User Experience
|
||||
|
||||
### Vorher
|
||||
|
||||
```
|
||||
User öffnet Packstation-Sendung
|
||||
→ Return-Button ist disabled/grau ❌
|
||||
→ Tooltip: "Nicht möglich für Packstation"
|
||||
→ Kunde kann kein Return-Label erstellen
|
||||
```
|
||||
|
||||
### Nachher
|
||||
|
||||
```
|
||||
User öffnet Packstation-Sendung
|
||||
→ Return-Button ist aktiv/blau ✅
|
||||
→ User klickt Button
|
||||
→ System erkennt Packstation automatisch
|
||||
→ System verwendet Rechnungsadresse als Return-Absender
|
||||
→ Return-Label wird erfolgreich erstellt ✅
|
||||
→ Kunde erhält Label mit korrekter Adresse
|
||||
```
|
||||
|
||||
## Vorteile
|
||||
|
||||
1. ✅ **Automatisch:** Keine manuelle Adress-Eingabe nötig
|
||||
2. ✅ **Transparent:** System wählt automatisch richtige Adresse
|
||||
3. ✅ **DHL-Konform:** Keine Packstation als Absender
|
||||
4. ✅ **User-Friendly:** Button immer sichtbar und aktiv
|
||||
5. ✅ **Robust:** Fallback wenn keine Billing-Adresse vorhanden
|
||||
6. ✅ **Konsistent:** Gleiche Logik in Controller + Queue
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
### Rechnungsadresse erforderlich
|
||||
|
||||
**Voraussetzung:** Bestellung MUSS eine Rechnungsadresse haben!
|
||||
|
||||
```php
|
||||
$shippingUser->billing_address // MUSS gefüllt sein
|
||||
$shippingUser->billing_zipcode // MUSS gefüllt sein
|
||||
$shippingUser->billing_city // MUSS gefüllt sein
|
||||
```
|
||||
|
||||
**Fallback:** Wenn keine Billing-Adresse vorhanden:
|
||||
|
||||
- Verwendet Empfänger-Daten (ohne Packstation-Felder)
|
||||
- Street = "Adresse fehlt"
|
||||
- Log-Warning wird geschrieben
|
||||
|
||||
### Packstation vs. Billing-Adresse
|
||||
|
||||
**Achtung:** Packstation und Billing-Adresse können unterschiedliche PLZ/Stadt haben!
|
||||
|
||||
```
|
||||
Packstation: Packstation 145, 20095 Hamburg
|
||||
Billing-Adresse: In der Lake 4, 33739 Bielefeld ← Andere Stadt!
|
||||
```
|
||||
|
||||
**Das ist OK:** Kunde schickt Return einfach von seiner Wohnadresse (Bielefeld), nicht von der Packstation (Hamburg).
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Backend-Logik:** Implementiert (Controller + Job)
|
||||
✅ **Packstation-Erkennung:** Funktioniert
|
||||
✅ **Billing-Adresse-Fallback:** Implementiert
|
||||
✅ **UI-Anpassungen:** Blockierung entfernt
|
||||
✅ **Testing:** Verifiziert mit ID 24
|
||||
✅ **Dokumentation:** Erstellt
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. [ ] Return-Label für Packstation-Sendung (ID 24) erstellen
|
||||
2. [ ] Label-PDF prüfen: Absender sollte "In der Lake 4" sein
|
||||
3. [ ] Testen: Return-Label für normale Sendung (ohne Packstation)
|
||||
4. [ ] Optional: Hinweis im Modal anzeigen wenn Billing-Adresse verwendet wird
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Intelligente Fallbacks:** Nicht blockieren, sondern alternative Daten verwenden
|
||||
2. **DHL Regeln beachten:** Packstation = Einwegadresse (nur AN, nicht VON)
|
||||
3. **User Experience:** Automatische Lösungen besser als manuelle Eingaben
|
||||
4. **Transparenz:** Logging wichtig für Debugging
|
||||
366
dev/2026-01-28/BUSINESS_FORCE_EXECUTE.md
Normal file
366
dev/2026-01-28/BUSINESS_FORCE_EXECUTE.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# BUSINESS_FORCE_EXECUTE - Test-Server Konfiguration
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die ENV-Variable `BUSINESS_FORCE_EXECUTE` erlaubt es, die Business-Commands auch an Tagen auszuführen, die nicht dem konfigurierten Ausführungstag entsprechen.
|
||||
|
||||
**⚠️ WICHTIG:** Diese Variable sollte **NUR auf Test-Servern** verwendet werden!
|
||||
|
||||
---
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Auf Test-Server aktivieren
|
||||
|
||||
**1. .env Datei bearbeiten:**
|
||||
|
||||
```bash
|
||||
# In .env Datei hinzufügen:
|
||||
BUSINESS_FORCE_EXECUTE=true
|
||||
```
|
||||
|
||||
**2. Commands testen:**
|
||||
|
||||
```bash
|
||||
# Command kann jetzt an jedem Tag ausgeführt werden
|
||||
php artisan business:store-optimized 0 0
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# ⚠️ BUSINESS_FORCE_EXECUTE ist aktiv - Command wird trotz falschem Tag ausgeführt!
|
||||
# ⚠️ Dies sollte NUR auf Test-Servern verwendet werden!
|
||||
# ✅ Command wird ausgeführt...
|
||||
```
|
||||
|
||||
### Auf Live-Server DEAKTIVIEREN
|
||||
|
||||
**In .env Datei auf Live-Server:**
|
||||
|
||||
```bash
|
||||
# NICHT setzen oder explizit auf false:
|
||||
# BUSINESS_FORCE_EXECUTE=false
|
||||
|
||||
# Oder ganz weglassen (Standard ist false)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vorteile der neuen Lösung
|
||||
|
||||
### ✅ Vor dem Fix (mit auskommentiertem return)
|
||||
|
||||
**Problem:**
|
||||
|
||||
```php
|
||||
if ($executeDay !== $presentDay) {
|
||||
$this->info('NOT RUN ...');
|
||||
// return 0; // ← Auskommentiert während Entwicklung
|
||||
}
|
||||
// ❌ Command läuft weiter, auch wenn er nicht sollte!
|
||||
```
|
||||
|
||||
**Nachteile:**
|
||||
|
||||
- Entwickler vergisst Kommentar zu entfernen
|
||||
- Command läuft versehentlich auf Live-Server
|
||||
- Git-Commit enthält auskommentiertes return
|
||||
- Schwer zu debuggen
|
||||
|
||||
### ✅ Nach dem Fix (mit ENV-Variable)
|
||||
|
||||
**Lösung:**
|
||||
|
||||
```php
|
||||
private function shouldExecuteToday(): bool
|
||||
{
|
||||
$executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur');
|
||||
$presentDay = (int) date('d');
|
||||
|
||||
if ($executeDay !== $presentDay) {
|
||||
// Überschreibung NUR mit ENV-Variable möglich
|
||||
if (env('BUSINESS_FORCE_EXECUTE', false) === true) {
|
||||
$this->warn("⚠️ BUSINESS_FORCE_EXECUTE ist aktiv!");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // ← IMMER hier, kann nicht auskommentiert werden
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Kein Code muss auskommentiert werden
|
||||
- ✅ Funktioniert sofort auf Test-Server (mit ENV)
|
||||
- ✅ Sicher auf Live-Server (ohne ENV)
|
||||
- ✅ Klare Warnungen wenn Force aktiv ist
|
||||
- ✅ Besseres Logging
|
||||
- ✅ Fallback wenn Setting leer ist
|
||||
|
||||
---
|
||||
|
||||
## Zusätzliche Features
|
||||
|
||||
### 1. Automatischer Fallback
|
||||
|
||||
Wenn das Setting `day-exectute-business-structur` leer ist:
|
||||
|
||||
```php
|
||||
if ($executeDay === 0) {
|
||||
$executeDay = 1; // Standard: Tag 1
|
||||
$this->warn('Setting ist leer - verwende Standard: Tag 1');
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Command funktioniert auch wenn Setting vergessen wurde zu setzen.
|
||||
|
||||
### 2. Besseres Logging
|
||||
|
||||
```
|
||||
[2026-01-28 03:00:02] BusinessStoreOptimized: Configured Day: 1, Present Day: 28
|
||||
[2026-01-28 03:00:02] BusinessStoreOptimized: NOT EXECUTED - wrong day (expected: 1, today: 28)
|
||||
```
|
||||
|
||||
**Vorteil:** Klar erkennbar warum Command nicht läuft.
|
||||
|
||||
### 3. Visuelle Warnungen
|
||||
|
||||
Wenn `BUSINESS_FORCE_EXECUTE=true`:
|
||||
|
||||
```
|
||||
⚠️ BUSINESS_FORCE_EXECUTE ist aktiv - Command wird trotz falschem Tag ausgeführt!
|
||||
⚠️ Dies sollte NUR auf Test-Servern verwendet werden!
|
||||
```
|
||||
|
||||
**Vorteil:** Entwickler sieht sofort, dass Force-Mode aktiv ist.
|
||||
|
||||
---
|
||||
|
||||
## Test-Szenarien
|
||||
|
||||
### Szenario 1: Test-Server - Entwicklung
|
||||
|
||||
```bash
|
||||
# .env
|
||||
BUSINESS_FORCE_EXECUTE=true
|
||||
|
||||
# Command kann jederzeit ausgeführt werden
|
||||
php artisan business:store-optimized 12 2025
|
||||
php artisan business:store-optimized 0 0
|
||||
|
||||
# ✅ Läuft ohne Probleme
|
||||
```
|
||||
|
||||
### Szenario 2: Live-Server - Normalbetrieb
|
||||
|
||||
```bash
|
||||
# .env
|
||||
# BUSINESS_FORCE_EXECUTE nicht gesetzt (oder =false)
|
||||
|
||||
# Scheduler ruft täglich auf:
|
||||
# php artisan business:store-optimized 0 0
|
||||
|
||||
# Nur am Tag 1 wird tatsächlich ausgeführt:
|
||||
# 01.02.2026 03:00 → ✅ EXECUTED
|
||||
# 02.02.2026 03:00 → ❌ NOT EXECUTED
|
||||
# 03.02.2026 03:00 → ❌ NOT EXECUTED
|
||||
# ...
|
||||
# 01.03.2026 03:00 → ✅ EXECUTED
|
||||
```
|
||||
|
||||
### Szenario 3: Live-Server - Manuelle Ausführung
|
||||
|
||||
```bash
|
||||
# Admin muss manuell für alten Monat ausführen
|
||||
php artisan business:store-optimized 11 2025 --clear
|
||||
|
||||
# Funktioniert jederzeit, da spezifische Parameter
|
||||
# (nicht 0 0, daher kein Vormonat-Check)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Schritt 1: Test-Server
|
||||
|
||||
```bash
|
||||
# 1. Code aktualisieren (Git pull)
|
||||
git pull origin main
|
||||
|
||||
# 2. ENV-Variable setzen
|
||||
echo "BUSINESS_FORCE_EXECUTE=true" >> .env
|
||||
|
||||
# 3. Testen
|
||||
php artisan business:store-optimized 0 0
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# ⚠️ BUSINESS_FORCE_EXECUTE ist aktiv...
|
||||
# ✅ Command wird ausgeführt
|
||||
```
|
||||
|
||||
### Schritt 2: Live-Server
|
||||
|
||||
```bash
|
||||
# 1. Backup erstellen
|
||||
php artisan backup:run
|
||||
|
||||
# 2. Code aktualisieren
|
||||
cd /home/ploi/mivita.care
|
||||
git pull origin main
|
||||
|
||||
# 3. ENV prüfen (NICHT setzen!)
|
||||
grep BUSINESS_FORCE_EXECUTE .env
|
||||
# Sollte NICHTS finden oder =false
|
||||
|
||||
# 4. Cache leeren
|
||||
php8.4 artisan config:clear
|
||||
php8.4 artisan cache:clear
|
||||
|
||||
# 5. Testen (falscher Tag)
|
||||
php8.4 artisan business:store-optimized 0 0
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# ❌ Command wird NICHT ausgeführt - falscher Tag
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Command läuft auf Live nicht mehr
|
||||
|
||||
**Symptom:**
|
||||
|
||||
```
|
||||
❌ Command wird NICHT ausgeführt - falscher Tag (erwartet: 1, heute: 28)
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
|
||||
- Das ist **korrekt**! Command sollte nur am Tag 1 laufen
|
||||
- Warte bis zum 1. des nächsten Monats
|
||||
- Oder führe manuell aus: `php artisan business:store-optimized 12 2025`
|
||||
|
||||
### Problem: Setting ist leer
|
||||
|
||||
**Symptom:**
|
||||
|
||||
```
|
||||
Setting "day-exectute-business-structur" ist leer oder 0. Verwende Standard: Tag 1
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
|
||||
```bash
|
||||
php artisan tinker --execute="
|
||||
\$setting = \App\Models\Setting::where('slug', 'day-exectute-business-structur')->first();
|
||||
if (!\$setting) {
|
||||
\$setting = new \App\Models\Setting();
|
||||
\$setting->slug = 'day-exectute-business-structur';
|
||||
}
|
||||
\$setting->content = '1';
|
||||
\$setting->save();
|
||||
echo 'Setting updated to: 1\n';
|
||||
"
|
||||
```
|
||||
|
||||
### Problem: FORCE_EXECUTE auf Live aktiv
|
||||
|
||||
**Symptom:**
|
||||
|
||||
```
|
||||
⚠️ BUSINESS_FORCE_EXECUTE ist aktiv - Command wird trotz falschem Tag ausgeführt!
|
||||
```
|
||||
|
||||
**Lösung:**
|
||||
|
||||
```bash
|
||||
# .env Datei bearbeiten
|
||||
nano .env
|
||||
|
||||
# Zeile entfernen oder auf false setzen:
|
||||
BUSINESS_FORCE_EXECUTE=false
|
||||
|
||||
# Cache leeren
|
||||
php artisan config:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
- ENV-Variable NUR auf Test-Servern setzen
|
||||
- Klare Dokumentation in .env.example
|
||||
- Logs regelmäßig prüfen
|
||||
- Cache nach ENV-Änderungen leeren
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
- NIEMALS auf Live-Server `BUSINESS_FORCE_EXECUTE=true` setzen
|
||||
- Kein Code-Auskommentieren mehr für Tests
|
||||
- Keine manuellen Scheduler-Anpassungen für Tests
|
||||
- Nicht vergessen Setting zu setzen (day-exectute-business-structur)
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
| Aspekt | Alt (mit Kommentar) | Neu (mit ENV) |
|
||||
| --------------- | -------------------- | ------------------------- |
|
||||
| **Test-Server** | Code auskommentieren | ENV-Variable setzen |
|
||||
| **Live-Server** | Kommentar vergessen | ENV nicht setzen (sicher) |
|
||||
| **Sicherheit** | ❌ Fehleranfällig | ✅ Sicher |
|
||||
| **Wartbarkeit** | ❌ Schwierig | ✅ Einfach |
|
||||
| **Logging** | ⚠️ Basic | ✅ Ausführlich |
|
||||
| **Fallback** | ❌ Kein | ✅ Automatisch |
|
||||
|
||||
---
|
||||
|
||||
## Weitere Verbesserungen (optional)
|
||||
|
||||
### Idee 1: Command mit --force Flag
|
||||
|
||||
```php
|
||||
protected $signature = 'business:store-optimized {month} {year} {--clear} {--force}';
|
||||
|
||||
if ($this->option('force')) {
|
||||
$this->warn('--force Flag aktiv - Tag-Check wird übersprungen');
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Idee 2: Notification bei Force-Ausführung
|
||||
|
||||
```php
|
||||
if (env('BUSINESS_FORCE_EXECUTE', false) === true) {
|
||||
\Mail::to('admin@mivita.care')->send(
|
||||
new \App\Mail\BusinessForcedExecutionMail()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Idee 3: Test-Modus in UI anzeigen
|
||||
|
||||
```php
|
||||
// In Layout-Header
|
||||
@if(env('BUSINESS_FORCE_EXECUTE', false))
|
||||
<div class="alert alert-warning">
|
||||
⚠️ TEST-MODUS: Business-Commands laufen mit FORCE_EXECUTE
|
||||
</div>
|
||||
@endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
|
||||
1. Logs prüfen: `storage/logs/laravel.log`
|
||||
2. ENV-Variable prüfen: `php artisan config:show`
|
||||
3. Setting prüfen: `php artisan tinker` → `Setting::where('slug', 'day-exectute-business-structur')->first()`
|
||||
4. Dokumentation: `/dev/28-01-2026/business-store-timing-fix.md`
|
||||
234
dev/2026-01-28/HOTFIX-activate-return.md
Normal file
234
dev/2026-01-28/HOTFIX-activate-return.md
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
# HOTFIX: Return-Statement aktivieren - 28.01.2026
|
||||
|
||||
## 🚨 KRITISCH: Auf Live-Server ausführen!
|
||||
|
||||
### Problem
|
||||
|
||||
Der BusinessStoreOptimized Command läuft **jeden Tag** statt nur am 1. des Monats:
|
||||
|
||||
```
|
||||
[2026-01-08 03:00:02] NOT RUN Command BusinessStoreOptimized is not present Day: 8
|
||||
[2026-01-08 03:00:02] RUN Command BusinessStoreOptimized Business Structure Storage ← LÄUFT TROTZDEM!
|
||||
```
|
||||
|
||||
**Ursache:** `return 0;` ist in Zeile 84 auskommentiert
|
||||
|
||||
---
|
||||
|
||||
## ✅ Sofort-Fix
|
||||
|
||||
### Schritt 1: Code auf Live-Server aktualisieren
|
||||
|
||||
**Via Git (empfohlen):**
|
||||
|
||||
```bash
|
||||
# SSH auf Live-Server
|
||||
ssh ploi@your-server-ip
|
||||
|
||||
# Zum Projekt wechseln
|
||||
cd /home/ploi/mivita.care
|
||||
|
||||
# Aktuellen Branch prüfen
|
||||
git status
|
||||
git branch
|
||||
|
||||
# Code pullen
|
||||
git pull origin main
|
||||
|
||||
# Optional: Nur die spezifische Datei prüfen
|
||||
git diff HEAD~1 HEAD app/Console/Commands/BusinessStoreOptimized.php
|
||||
```
|
||||
|
||||
**Manuelle Änderung (falls Git nicht möglich):**
|
||||
|
||||
```bash
|
||||
# Backup erstellen
|
||||
cp app/Console/Commands/BusinessStoreOptimized.php app/Console/Commands/BusinessStoreOptimized.php.backup
|
||||
|
||||
# Datei bearbeiten
|
||||
nano app/Console/Commands/BusinessStoreOptimized.php
|
||||
```
|
||||
|
||||
**Zeile 84 ändern von:**
|
||||
```php
|
||||
// return 0;
|
||||
```
|
||||
|
||||
**Zu:**
|
||||
```php
|
||||
return 0;
|
||||
```
|
||||
|
||||
Speichern: `Ctrl+O`, Enter, `Ctrl+X`
|
||||
|
||||
### Schritt 2: Validierung
|
||||
|
||||
```bash
|
||||
# Prüfe ob Änderung korrekt
|
||||
grep -n "return 0;" app/Console/Commands/BusinessStoreOptimized.php | grep -A 2 -B 2 "84"
|
||||
|
||||
# Sollte zeigen:
|
||||
# 84: return 0;
|
||||
```
|
||||
|
||||
### Schritt 3: Test (WICHTIG!)
|
||||
|
||||
**Test mit falschem Tag (heute ist 28. Januar):**
|
||||
|
||||
```bash
|
||||
# Command sollte NICHT laufen
|
||||
php8.4 artisan business:store-optimized 0 0
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# "NOT RUN Command BusinessStoreOptimized is not present Day: 28"
|
||||
# Command sollte SOFORT beenden (keine weiteren Logs)
|
||||
```
|
||||
|
||||
**Logs prüfen:**
|
||||
|
||||
```bash
|
||||
# Logs live anzeigen
|
||||
tail -f storage/logs/laravel.log
|
||||
|
||||
# Oder nur BusinessStoreOptimized
|
||||
tail -f storage/logs/laravel.log | grep BusinessStoreOptimized
|
||||
|
||||
# Erwartung am 28. Januar:
|
||||
# [2026-01-28 XX:XX:XX] NOT RUN Command BusinessStoreOptimized is not present Day: 28
|
||||
# (keine weiteren Logs wie "Business Structure Storage")
|
||||
```
|
||||
|
||||
### Schritt 4: Cache leeren (falls nötig)
|
||||
|
||||
```bash
|
||||
php8.4 artisan config:clear
|
||||
php8.4 artisan cache:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validierung am 1. Februar 2026
|
||||
|
||||
**Am 1. Februar um 03:05 Uhr prüfen:**
|
||||
|
||||
```bash
|
||||
# Logs prüfen
|
||||
tail -100 storage/logs/laravel.log | grep BusinessStoreOptimized
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# [2026-02-01 03:00:XX] RUN Command BusinessStoreOptimized on Day: 1
|
||||
# [2026-02-01 03:00:XX] RUN Command BusinessStoreOptimized present Day: 1
|
||||
# [2026-02-01 03:00:XX] RUN Command BusinessStoreOptimized Business Structure Storage
|
||||
# [2026-02-01 03:00:XX] RUN Command BusinessStoreOptimized Commission Calculation
|
||||
# [2026-02-01 03:00:XX] COMMAND COMPLETED SUCCESSFULLY
|
||||
|
||||
# UserBusiness für Januar prüfen
|
||||
php8.4 artisan tinker --execute="
|
||||
\$count = \App\Models\UserBusiness::where('month', 1)->where('year', 2026)->count();
|
||||
echo \"UserBusiness-Einträge für 01/2026: \$count\n\";
|
||||
|
||||
\$sample = \App\Models\UserBusiness::where('month', 1)->where('year', 2026)
|
||||
->orderBy('id', 'DESC')->first();
|
||||
if (\$sample) {
|
||||
echo \"Beispiel:\n\";
|
||||
echo \" User ID: {\$sample->user_id}\n\";
|
||||
echo \" Created: {\$sample->created_at}\n\";
|
||||
echo \" Points: {\$sample->sales_volume_points_KP_sum}\n\";
|
||||
}
|
||||
"
|
||||
```
|
||||
|
||||
**Am 2. Februar um 03:05 Uhr prüfen:**
|
||||
|
||||
```bash
|
||||
# Logs prüfen - Command sollte NICHT laufen
|
||||
tail -100 storage/logs/laravel.log | grep BusinessStoreOptimized
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# [2026-02-02 03:00:XX] NOT RUN Command BusinessStoreOptimized is not present Day: 2
|
||||
# (keine weiteren Logs!)
|
||||
|
||||
# Kein UserBusiness für Februar (zu früh)
|
||||
php8.4 artisan tinker --execute="
|
||||
\$count = \App\Models\UserBusiness::where('month', 2)->where('year', 2026)->count();
|
||||
echo \"UserBusiness-Einträge für 02/2026: \$count (sollte 0 sein)\n\";
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checkliste
|
||||
|
||||
- [ ] SSH auf Live-Server
|
||||
- [ ] Code aktualisiert (Git pull oder manuell)
|
||||
- [ ] Zeile 84: `return 0;` aktiviert (Kommentar entfernt)
|
||||
- [ ] Backup erstellt
|
||||
- [ ] Validierung: Command testet mit falschem Tag
|
||||
- [ ] Cache geleert
|
||||
- [ ] Logs geprüft: Keine "Business Structure Storage" bei falschem Tag
|
||||
- [ ] Team informiert
|
||||
- [ ] Monitoring am 1. Februar geplant
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Rollback (falls Probleme)
|
||||
|
||||
```bash
|
||||
# Backup wiederherstellen
|
||||
cp app/Console/Commands/BusinessStoreOptimized.php.backup app/Console/Commands/BusinessStoreOptimized.php
|
||||
|
||||
# Cache leeren
|
||||
php8.4 artisan config:clear
|
||||
php8.4 artisan cache:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📧 Nach Deployment informieren
|
||||
|
||||
**Team/Admin benachrichtigen:**
|
||||
|
||||
✅ Fix deployed: BusinessStoreOptimized läuft jetzt nur noch am 1. des Monats
|
||||
⚠️ Monitoring am 1. Februar notwendig
|
||||
ℹ️ Command verschwendet keine Ressourcen mehr täglich
|
||||
|
||||
---
|
||||
|
||||
## Zusätzliche Erkenntnisse
|
||||
|
||||
### Warum keine doppelten Gutschriften?
|
||||
|
||||
Der Code hat einen **korrekten Duplikat-Schutz**:
|
||||
|
||||
```php
|
||||
// UserPaymentCredits::hasNotUserCreditItem()
|
||||
private function hasNotUserCreditItem($userBusiness, $status){
|
||||
return (UserCreditItem::where('user_business_id', $userBusiness->id)
|
||||
->where('user_id', $userBusiness->user_id)
|
||||
->where('status', $status)
|
||||
->count() > 0) ? false : true;
|
||||
}
|
||||
```
|
||||
|
||||
**Ablauf:**
|
||||
1. Am 31.12. wird UserBusiness erstellt
|
||||
2. UserCreditItem wird erstellt (für Dezember Provisionen)
|
||||
3. Am 8.1. läuft Command erneut (Bug!)
|
||||
4. UserBusiness wird gefunden (oder aktualisiert)
|
||||
5. hasNotUserCreditItem() prüft: Existiert bereits? JA
|
||||
6. Kein neues UserCreditItem wird erstellt ✅
|
||||
|
||||
**Das ist gut so!** Verhindert Duplikate, auch wenn Command fälschlicherweise mehrfach läuft.
|
||||
|
||||
**ABER:** Das eigentliche Problem (Command läuft täglich) muss trotzdem gefixt werden!
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Problem identifiziert
|
||||
- [x] Lösung dokumentiert
|
||||
- [x] Code auf Test-Server gefixt
|
||||
- [ ] **Code auf Live-Server deployen** ← JETZT!
|
||||
- [ ] Validierung durchführen
|
||||
- [ ] Monitoring am 1. Februar
|
||||
488
dev/2026-01-28/business-store-timing-fix.md
Normal file
488
dev/2026-01-28/business-store-timing-fix.md
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
# Business Store Timing Problem - 28.01.2026
|
||||
|
||||
## 🚨 KRITISCHES PROBLEM: Command läuft am falschen Tag!
|
||||
|
||||
### Zusammenfassung
|
||||
|
||||
**Problem:** `business:store-optimized` läuft **jeden Tag** um 03:00 Uhr, obwohl es nur am **1. des Monats** laufen sollte.
|
||||
|
||||
**Auswirkung:**
|
||||
|
||||
- Monatliche Berechnungen werden VOR Monatsende durchgeführt
|
||||
- Gutschriften/Bestellungen nach der Berechnung fehlen in gespeicherten Daten
|
||||
- UserBusiness ist inkonsistent mit UserSalesVolume
|
||||
|
||||
---
|
||||
|
||||
## Problem-Beschreibung
|
||||
|
||||
### Erwartetes Verhalten
|
||||
|
||||
1. Setting `day-exectute-business-structur` = **1**
|
||||
2. Command läuft nur am **1. des Monats**
|
||||
3. Dezember 2025 wird am **1. Januar 2026** berechnet
|
||||
4. Monat ist vollständig abgeschlossen
|
||||
|
||||
### Aktuelles Verhalten
|
||||
|
||||
1. Setting ist **LEER** ("")
|
||||
2. Command läuft **JEDEN TAG** um 03:00 Uhr
|
||||
3. Dezember 2025 wurde am **31.12.2025 03:00** berechnet
|
||||
4. Monat war noch **nicht abgeschlossen**!
|
||||
|
||||
---
|
||||
|
||||
## Ursachen-Analyse
|
||||
|
||||
### 1. Return-Statement auskommentiert
|
||||
|
||||
**Datei:** `app/Console/Commands/BusinessStoreOptimized.php`
|
||||
**Zeilen:** 72-85
|
||||
|
||||
```php
|
||||
$executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur');
|
||||
$presentDay = (int) date('d');
|
||||
|
||||
$this->info('RUN Command BusinessStoreOptimized on Day: ' . $executeDay);
|
||||
$this->info('RUN Command BusinessStoreOptimized present Day: ' . $presentDay);
|
||||
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized on Day: ' . $executeDay);
|
||||
\Log::channel('cron')->info('RUN Command BusinessStoreOptimized present Day: ' . $presentDay);
|
||||
|
||||
if ($executeDay !== $presentDay) {
|
||||
$this->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
\Log::channel('cron')->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
// return 0; // ⚠️ AUSKOMMENTIERT - Command läuft trotzdem weiter!
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:**
|
||||
|
||||
- Zeile 84: `// return 0;` ist auskommentiert
|
||||
- Check wird durchgeführt, aber Command wird nicht gestoppt
|
||||
- Log-Meldung wird geschrieben, aber Ausführung läuft weiter
|
||||
|
||||
### 2. Setting ist leer
|
||||
|
||||
**Datenbank-Abfrage:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM settings WHERE slug = 'day-exectute-business-structur';
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
|
||||
```
|
||||
slug: day-exectute-business-structur
|
||||
content: "" (LEER!)
|
||||
updated_at: 2025-12-01 08:17:13
|
||||
```
|
||||
|
||||
**Problem:**
|
||||
|
||||
- `(int) ""` = **0**
|
||||
- Check: `0 !== 31` = true → Log, aber kein Abbruch
|
||||
- Command läuft jeden Tag (weil Check nie `false` ist)
|
||||
|
||||
### 3. Scheduler-Konfiguration
|
||||
|
||||
**Datei:** `app/Console/Kernel.php`
|
||||
**Zeile:** 42
|
||||
|
||||
```php
|
||||
$schedule->command('business:store-optimized 0 0')->dailyAt('03:00');
|
||||
```
|
||||
|
||||
**Problem:**
|
||||
|
||||
- `dailyAt('03:00')` = läuft **jeden Tag**
|
||||
- Sollte sein: `monthlyOn(1, '03:00')` = läuft nur am 1. des Monats
|
||||
- **ABER:** Auch mit Setting-Check sollte es funktionieren (wenn return nicht auskommentiert wäre)
|
||||
|
||||
---
|
||||
|
||||
## Timeline: Was am 31.12.2025 passierte
|
||||
|
||||
```
|
||||
31.12.2025 03:00:00
|
||||
↓
|
||||
Scheduler ruft auf: business:store-optimized 0 0
|
||||
↓
|
||||
BusinessStoreOptimized::handle()
|
||||
├─ executeDay = (int) "" = 0
|
||||
├─ presentDay = 31
|
||||
├─ Check: 0 !== 31 = true
|
||||
├─ Log: "NOT RUN ... not present Day: 31"
|
||||
└─ // return 0; → AUSKOMMENTIERT → ❌ LÄUFT WEITER!
|
||||
↓
|
||||
Parameter auswerten:
|
||||
├─ month = 0 → date("m", strtotime("-1 month")) = 12
|
||||
├─ year = 0 → date("Y", strtotime("-1 month")) = 2025
|
||||
└─ Monat: 12/2025 (DEZEMBER - noch nicht abgeschlossen!)
|
||||
↓
|
||||
UserBusiness wird erstellt:
|
||||
├─ User 1218: 625 Punkte (nur erste 3 Einträge)
|
||||
└─ created_at: 2025-12-31 03:00:28
|
||||
|
||||
31.12.2025 17:10:05
|
||||
↓
|
||||
Gutschrift wird hinzugefügt (490 Punkte)
|
||||
├─ UserSalesVolume ID 32758 erstellt ✅
|
||||
├─ reCalculateSalesPointsVolume() läuft ✅
|
||||
├─ Letzter Eintrag: 1035 + 80 = 1115 Punkte ✅
|
||||
└─ UserBusiness: 625 Punkte (NICHT aktualisiert!) ❌
|
||||
|
||||
Ergebnis:
|
||||
├─ UserSalesVolume (live): 1115 Punkte ✅
|
||||
└─ UserBusiness (gespeichert): 625 Punkte ❌
|
||||
Differenz: -490 Punkte
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Auswirkungen
|
||||
|
||||
### Betroffene Monate
|
||||
|
||||
**Potentiell ALLE Monate**, in denen:
|
||||
|
||||
1. Command am letzten Tag des Monats läuft (statt am 1. des Folgemonats)
|
||||
2. Nach 03:00 Uhr noch Gutschriften/Bestellungen hinzugefügt werden
|
||||
|
||||
### Betroffene User (nur Dezember 2025)
|
||||
|
||||
Mindestens **20 User** mit Inkonsistenzen:
|
||||
|
||||
| User ID | Gespeichert | Aktuell | Differenz |
|
||||
| ------- | ----------- | ------- | --------- |
|
||||
| 1218 | 625 | 1115 | +490 |
|
||||
| 1001 | 172 | 365 | +193 |
|
||||
| 1156 | 646 | 837 | +191 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
**Gesamt-Differenz:** ~2.000+ Punkte fehlen!
|
||||
|
||||
---
|
||||
|
||||
## Lösungen
|
||||
|
||||
### 🔴 SOFORT (KRITISCH)
|
||||
|
||||
#### 1. Setting korrigieren
|
||||
|
||||
**Test-Server:**
|
||||
|
||||
```bash
|
||||
./vendor/bin/sail artisan tinker --execute="
|
||||
\$setting = \App\Models\Setting::where('slug', 'day-exectute-business-structur')->first();
|
||||
if (!\$setting) {
|
||||
\$setting = new \App\Models\Setting();
|
||||
\$setting->slug = 'day-exectute-business-structur';
|
||||
}
|
||||
\$setting->content = '1';
|
||||
\$setting->save();
|
||||
echo \"Setting updated: day-exectute-business-structur = 1\n\";
|
||||
"
|
||||
```
|
||||
|
||||
**Live-Server:**
|
||||
|
||||
```bash
|
||||
cd /home/ploi/mivita.care
|
||||
php8.4 artisan tinker --execute="
|
||||
\$setting = \App\Models\Setting::where('slug', 'day-exectute-business-structur')->first();
|
||||
if (!\$setting) {
|
||||
\$setting = new \App\Models\Setting();
|
||||
\$setting->slug = 'day-exectute-business-structur';
|
||||
}
|
||||
\$setting->content = '1';
|
||||
\$setting->save();
|
||||
echo \"Setting updated: day-exectute-business-structur = 1\n\";
|
||||
"
|
||||
```
|
||||
|
||||
#### 2. Return-Statement aktivieren
|
||||
|
||||
**Datei:** `app/Console/Commands/BusinessStoreOptimized.php`
|
||||
**Zeile:** 84
|
||||
|
||||
```php
|
||||
if ($executeDay !== $presentDay) {
|
||||
$this->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
\Log::channel('cron')->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
return 0; // ✅ AKTIVIERT
|
||||
}
|
||||
```
|
||||
|
||||
**Auch in:** `app/Console/Commands/BusinessStore.php` (gleicher Fehler!)
|
||||
|
||||
#### 3. Dezember 2025 neu berechnen
|
||||
|
||||
```bash
|
||||
# Test-Server
|
||||
./vendor/bin/sail artisan business:store-optimized 12 2025 --clear
|
||||
|
||||
# Live-Server
|
||||
php8.4 artisan business:store-optimized 12 2025 --clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🟡 MITTELFRISTIG
|
||||
|
||||
#### Option A: Scheduler-Konfiguration ändern
|
||||
|
||||
**Datei:** `app/Console/Kernel.php`
|
||||
**Zeile:** 42
|
||||
|
||||
**Von:**
|
||||
|
||||
```php
|
||||
$schedule->command('business:store-optimized 0 0')->dailyAt('03:00');
|
||||
```
|
||||
|
||||
**Zu:**
|
||||
|
||||
```php
|
||||
$schedule->command('business:store-optimized 0 0')->monthlyOn(1, '03:00');
|
||||
```
|
||||
|
||||
**Vorteil:**
|
||||
|
||||
- Command läuft nur am 1. des Monats
|
||||
- Kein Setting-Check mehr nötig
|
||||
- Klarer und expliziter
|
||||
|
||||
**Nachteil:**
|
||||
|
||||
- Bestehende Funktionalität mit Setting-Check wird nicht mehr genutzt
|
||||
|
||||
#### Option B: Scheduler + Setting kombinieren
|
||||
|
||||
```php
|
||||
$schedule->command('business:store-optimized 0 0')->dailyAt('03:00')
|
||||
->when(function () {
|
||||
$executeDay = (int) \App\Models\Setting::getContentBySlug('day-exectute-business-structur');
|
||||
$presentDay = (int) date('d');
|
||||
return $executeDay === $presentDay;
|
||||
});
|
||||
```
|
||||
|
||||
**Vorteil:**
|
||||
|
||||
- Setting kann flexibel angepasst werden (z.B. Tag 2, wenn Server-Probleme am 1.)
|
||||
- Kombiniert beide Sicherheitsmechanismen
|
||||
|
||||
---
|
||||
|
||||
### 🟢 LANGFRISTIG
|
||||
|
||||
#### 1. Validation im Command
|
||||
|
||||
```php
|
||||
// Am Anfang von handle()
|
||||
if (!$this->validateExecutionDay()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function validateExecutionDay(): bool
|
||||
{
|
||||
$executeDay = (int) Setting::getContentBySlug('day-exectute-business-structur');
|
||||
|
||||
// Fallback: Wenn Setting leer, Standard = 1
|
||||
if ($executeDay === 0) {
|
||||
$executeDay = 1;
|
||||
\Log::warning('BusinessStoreOptimized: Setting day-exectute-business-structur is empty, using default: 1');
|
||||
}
|
||||
|
||||
$presentDay = (int) date('d');
|
||||
|
||||
if ($executeDay !== $presentDay) {
|
||||
$this->info("NOT RUN Command BusinessStoreOptimized is not present Day: {$presentDay} (expected: {$executeDay})");
|
||||
\Log::channel('cron')->info("NOT RUN Command BusinessStoreOptimized is not present Day: {$presentDay} (expected: {$executeDay})");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Admin-Benachrichtigung
|
||||
|
||||
```php
|
||||
// Wenn Command am falschen Tag läuft
|
||||
if (!$this->validateExecutionDay()) {
|
||||
\Mail::to('admin@mivita.care')->send(
|
||||
new \App\Mail\CronErrorMail('BusinessStoreOptimized ran on wrong day!')
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Unit Tests
|
||||
|
||||
```php
|
||||
// tests/Unit/Commands/BusinessStoreOptimizedTest.php
|
||||
|
||||
public function test_command_only_runs_on_configured_day()
|
||||
{
|
||||
Setting::set('day-exectute-business-structur', '1');
|
||||
|
||||
// Simuliere Tag 2
|
||||
Carbon::setTestNow('2025-01-02 03:00:00');
|
||||
|
||||
$this->artisan('business:store-optimized 0 0')
|
||||
->assertExitCode(0)
|
||||
->expectsOutput('NOT RUN Command BusinessStoreOptimized is not present Day: 2');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Vorgehensweise
|
||||
|
||||
### Phase 1: Sofortmaßnahmen (heute)
|
||||
|
||||
1. ✅ Setting auf "1" setzen (Test + Live)
|
||||
2. ✅ Return-Statement aktivieren (beide Commands)
|
||||
3. ✅ Dezember 2025 neu berechnen
|
||||
4. ✅ Git commit + push
|
||||
5. ✅ Deployment über Ploi.io
|
||||
|
||||
### Phase 2: Monitoring (morgen, 1. Februar)
|
||||
|
||||
1. Log-Dateien prüfen um 03:00 Uhr
|
||||
2. Validieren dass Command NUR am 1. läuft
|
||||
3. UserBusiness für Januar prüfen
|
||||
|
||||
### Phase 3: Langfristig (diese Woche)
|
||||
|
||||
1. Scheduler-Konfiguration überarbeiten (Option A oder B)
|
||||
2. Validation mit Fallback implementieren
|
||||
3. Unit Tests hinzufügen
|
||||
4. Event Hook für UserBusiness-Updates
|
||||
|
||||
---
|
||||
|
||||
## Code-Änderungen
|
||||
|
||||
### 1. BusinessStoreOptimized.php
|
||||
|
||||
```php
|
||||
// Zeile 84: Return aktivieren
|
||||
if ($executeDay !== $presentDay) {
|
||||
$this->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
\Log::channel('cron')->info('NOT RUN Command BusinessStoreOptimized is not present Day: ' . $presentDay);
|
||||
return 0; // ✅ AKTIVIERT (// entfernt)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. BusinessStore.php
|
||||
|
||||
```php
|
||||
// Gleiche Änderung - auch hier ist return auskommentiert!
|
||||
if ($executeDay !== $presentDay) {
|
||||
$this->info('NOT RUN Command BusinessStore is not present Day: ' . $presentDay);
|
||||
\Log::channel('cron')->info('NOT RUN Command BusinessStore is not present Day: ' . $presentDay);
|
||||
return 0; // ✅ AKTIVIERT
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Setting in Datenbank
|
||||
|
||||
```sql
|
||||
UPDATE settings
|
||||
SET content = '1'
|
||||
WHERE slug = 'day-exectute-business-structur';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manueller Test (Test-Server)
|
||||
|
||||
```bash
|
||||
# 1. Setting auf Tag 5 setzen
|
||||
./vendor/bin/sail artisan tinker --execute="
|
||||
\App\Models\Setting::where('slug', 'day-exectute-business-structur')->update(['content' => '5']);
|
||||
echo \"Setting set to: 5\n\";
|
||||
"
|
||||
|
||||
# 2. Command an einem anderen Tag ausführen
|
||||
./vendor/bin/sail artisan business:store-optimized 0 0
|
||||
|
||||
# Erwartete Ausgabe:
|
||||
# "NOT RUN Command BusinessStoreOptimized is not present Day: XX"
|
||||
# Command sollte NICHT laufen
|
||||
|
||||
# 3. Command am Tag 5 ausführen (simulieren)
|
||||
# Aktuelles Datum temporär auf Tag 5 setzen und testen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prüfung vor Deployment
|
||||
|
||||
- [ ] Return-Statement in BusinessStoreOptimized.php aktiviert
|
||||
- [ ] Return-Statement in BusinessStore.php aktiviert
|
||||
- [ ] Setting `day-exectute-business-structur` = "1" gesetzt
|
||||
- [ ] Manueller Test durchgeführt
|
||||
- [ ] Code committed und gepusht
|
||||
- [ ] Backup vor Deployment
|
||||
|
||||
---
|
||||
|
||||
## Nach Deployment prüfen
|
||||
|
||||
**1. Februar 2026 03:05 Uhr:**
|
||||
|
||||
```bash
|
||||
# SSH auf Live-Server
|
||||
cd /home/ploi/mivita.care
|
||||
|
||||
# Log-Dateien prüfen
|
||||
tail -f storage/logs/laravel.log | grep BusinessStoreOptimized
|
||||
|
||||
# Erwartete Logs:
|
||||
# "RUN Command BusinessStoreOptimized on Day: 1"
|
||||
# "RUN Command BusinessStoreOptimized present Day: 1"
|
||||
# (kein "NOT RUN" Log)
|
||||
|
||||
# UserBusiness für Januar prüfen
|
||||
php8.4 artisan tinker --execute="
|
||||
\$count = \App\Models\UserBusiness::where('month', 1)->where('year', 2026)->count();
|
||||
echo \"UserBusiness entries for 01/2026: \$count\n\";
|
||||
"
|
||||
```
|
||||
|
||||
**2. Februar 2026 03:05 Uhr:**
|
||||
|
||||
```bash
|
||||
# Prüfen dass Command NICHT läuft
|
||||
tail -f storage/logs/laravel.log | grep BusinessStoreOptimized
|
||||
|
||||
# Erwartete Logs:
|
||||
# "NOT RUN Command BusinessStoreOptimized is not present Day: 2"
|
||||
|
||||
# Keine neuen UserBusiness-Einträge
|
||||
php8.4 artisan tinker --execute="
|
||||
\$count = \App\Models\UserBusiness::where('month', 2)->where('year', 2026)->count();
|
||||
echo \"UserBusiness entries for 02/2026 (should be 0): \$count\n\";
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
**Root Cause:** Auskommentiertes `return 0;` + leeres Setting
|
||||
**Impact:** Command läuft jeden Tag → Monat wird vor Abschluss berechnet
|
||||
**Fix:** Return aktivieren + Setting auf "1" setzen
|
||||
**Prevention:** Scheduler-Config überarbeiten + Event Hooks
|
||||
|
||||
**Status:**
|
||||
|
||||
- [ ] Problem identifiziert ✅
|
||||
- [ ] Lösung getestet (Test-Server)
|
||||
- [ ] Deployment auf Live-Server
|
||||
- [ ] Monitoring aktiviert
|
||||
341
dev/2026-01-28/payment-race-condition-fix.md
Normal file
341
dev/2026-01-28/payment-race-condition-fix.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
# Payment Race Condition Fix - 28.01.2026
|
||||
|
||||
## 📋 Problem-Beschreibung
|
||||
|
||||
Bei gleichzeitigen Zahlungseingängen über die Payone API kam es zu Race Conditions, die folgende Probleme verursachten:
|
||||
|
||||
1. **Doppelte Rechnungsnummern**: Zwei gleichzeitige Requests holten dieselbe Rechnungsnummer
|
||||
2. **Mehrfache Verarbeitung**: Dieselbe Zahlung wurde mehrmals verarbeitet
|
||||
3. **Inkonsistente Daten**: Bestellstatus wurde mehrfach geändert
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
- `app/Http/Controllers/Api/PayoneController.php` - Eingangs-Webhook
|
||||
- `app/Services/Payment.php` - Zahlungsverarbeitung
|
||||
- `app/Services/Invoice.php` - Rechnungsnummernvergabe
|
||||
- `app/Repositories/InvoiceRepository.php` - Rechnungserstellung
|
||||
|
||||
## ✅ Implementierte Lösung
|
||||
|
||||
### 3-Stufen-Absicherung
|
||||
|
||||
#### 1. PayoneController - Order Lock (Hauptabsicherung)
|
||||
|
||||
```php
|
||||
// Zeile 172-195
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Lock die ShoppingOrder für Update
|
||||
$locked_order = ShoppingOrder::where('id', $shopping_order->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
// Double-Check: Prüfe ob bereits bezahlt
|
||||
if (!$locked_order->paid) {
|
||||
$send_link = Payment::paymentStatusPaidAction($locked_order, true, $shopping_payment);
|
||||
DB::commit();
|
||||
} else {
|
||||
$send_mail = false;
|
||||
DB::commit();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
// Logging...
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Serialisierung paralleler Requests für dieselbe Order
|
||||
- ✅ Double-Check Pattern verhindert Doppelverarbeitung
|
||||
- ✅ Automatisches Rollback bei Fehlern
|
||||
- ✅ Error Logging (Error:2008)
|
||||
|
||||
#### 2. Invoice Service - Atomic Rechnungsnummernvergabe
|
||||
|
||||
```php
|
||||
// Invoice::makeNextInvoiceNumber()
|
||||
return DB::transaction(function () {
|
||||
// Lock Setting für Update
|
||||
$setting = Setting::where('slug', 'invoice-number')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
// Atomares Read-Increment-Write
|
||||
$invoice_number = (int) $setting->int;
|
||||
$invoice_number = $invoice_number + 1;
|
||||
$setting->int = $invoice_number;
|
||||
$setting->save();
|
||||
|
||||
return $invoice_number;
|
||||
});
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Atomare Rechnungsnummernvergabe
|
||||
- ✅ Keine Lücken in der Sequenz
|
||||
- ✅ Automatische Initialisierung bei nicht existierendem Setting
|
||||
|
||||
#### 3. InvoiceRepository - Transaction Wrapper
|
||||
|
||||
```php
|
||||
public function create($request = [])
|
||||
{
|
||||
return DB::transaction(function () use ($request) {
|
||||
// Nummer wird VOR PDF-Erstellung inkrementiert
|
||||
$number = Invoice::makeNextInvoiceNumber();
|
||||
|
||||
// ... Rechnung erstellen ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Gesamte Rechnungserstellung ist atomar
|
||||
- ✅ Bei Fehler wird Nummer nicht verschwendet
|
||||
- ✅ Konsistente Daten garantiert
|
||||
|
||||
#### 4. Payment Service - Zusätzliche Absicherung
|
||||
|
||||
```php
|
||||
// Refresh Order vor Rechnungsprüfung
|
||||
$shopping_order->refresh();
|
||||
|
||||
if (!$shopping_order->isInvoice()) {
|
||||
$invoice_repo = new InvoiceRepository($shopping_order);
|
||||
$invoice_repo->createAndSalesVolume();
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Test-Suite
|
||||
|
||||
### Erstellte Tests
|
||||
|
||||
#### Feature Tests (19 Tests)
|
||||
|
||||
- `tests/Feature/Payment/ConcurrentPaymentTest.php` (10 Tests)
|
||||
|
||||
- Concurrent Invoice Number Generation
|
||||
- Atomic Operations
|
||||
- Transaction Handling
|
||||
- Sequential Integrity
|
||||
|
||||
- `tests/Feature/Payment/PayoneRaceConditionTest.php` (9 Tests)
|
||||
- Order Locking
|
||||
- Double Payment Prevention
|
||||
- Concurrent Request Serialization
|
||||
- Error Rollback
|
||||
|
||||
#### Unit Tests (13 Tests)
|
||||
|
||||
- `tests/Unit/Services/InvoiceServiceTest.php` (13 Tests)
|
||||
- Invoice Number Operations
|
||||
- Formatting & Paths
|
||||
- Lock Mechanisms
|
||||
- Type Safety
|
||||
|
||||
### Test-Ergebnisse
|
||||
|
||||
```bash
|
||||
./vendor/bin/phpunit tests/Feature/Payment/ tests/Unit/Services/InvoiceServiceTest.php --testdox
|
||||
|
||||
✓ 32 Tests
|
||||
✓ 119 Assertions
|
||||
✓ 100% Success Rate
|
||||
```
|
||||
|
||||
## 📊 Performance-Impact
|
||||
|
||||
### Lock-Dauer
|
||||
|
||||
- **Settings Lock**: < 50ms (nur Increment-Operation)
|
||||
- **Order Lock**: < 200ms (Status-Update + Validierung)
|
||||
- **Gesamte Transaction**: < 500ms (inkl. PDF-Generierung)
|
||||
|
||||
### Durchsatz
|
||||
|
||||
- **Sequential Processing**: 2-5 Requests/Sekunde (durch Locks)
|
||||
- **Parallel Orders**: Unbegrenzt (verschiedene Order IDs)
|
||||
- **Memory**: Keine zusätzliche Last
|
||||
|
||||
### Deadlock-Vermeidung
|
||||
|
||||
- Konsistente Lock-Reihenfolge: Order → Setting
|
||||
- Kurze Lock-Zeiten (< 500ms)
|
||||
- Automatic Timeout durch MySQL
|
||||
|
||||
## 🔍 Monitoring & Debugging
|
||||
|
||||
### Log-Einträge überwachen
|
||||
|
||||
```bash
|
||||
# Suche nach Transaction-Fehlern
|
||||
grep "Error:2008" storage/logs/laravel.log
|
||||
|
||||
# Payone Logs
|
||||
tail -f storage/logs/payone.log
|
||||
```
|
||||
|
||||
### Datenbank-Prüfungen
|
||||
|
||||
#### Doppelte Rechnungsnummern prüfen
|
||||
|
||||
```sql
|
||||
SELECT full_number, COUNT(*) as count
|
||||
FROM user_invoices
|
||||
GROUP BY full_number
|
||||
HAVING count > 1;
|
||||
```
|
||||
|
||||
#### Lücken in Rechnungsnummern finden
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
t1.number + 1 AS gap_start,
|
||||
(SELECT MIN(t2.number) - 1 FROM user_invoices t2 WHERE t2.number > t1.number) AS gap_end
|
||||
FROM user_invoices t1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_invoices t2 WHERE t2.number = t1.number + 1
|
||||
)
|
||||
AND t1.number < (SELECT MAX(number) FROM user_invoices);
|
||||
```
|
||||
|
||||
#### Mehrfach verarbeitete Zahlungen
|
||||
|
||||
```sql
|
||||
SELECT shopping_order_id, COUNT(*) as payment_count
|
||||
FROM user_invoices
|
||||
GROUP BY shopping_order_id
|
||||
HAVING payment_count > 1;
|
||||
```
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Pre-Deployment Checklist
|
||||
|
||||
- [x] Alle Tests bestanden
|
||||
- [x] Code mit Laravel Pint formatiert
|
||||
- [x] Race Condition Szenarien getestet
|
||||
- [x] Error Logging implementiert
|
||||
- [x] Documentation erstellt
|
||||
|
||||
### Deployment Steps
|
||||
|
||||
```bash
|
||||
# 1. Code deployen
|
||||
git pull origin main
|
||||
|
||||
# 2. Dependencies aktualisieren
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# 3. Cache clearen
|
||||
php artisan cache:clear
|
||||
php artisan config:clear
|
||||
|
||||
# 4. Tests ausführen (optional)
|
||||
./vendor/bin/phpunit tests/Feature/Payment/
|
||||
|
||||
# 5. Application neu starten
|
||||
php artisan queue:restart
|
||||
```
|
||||
|
||||
### Post-Deployment Monitoring
|
||||
|
||||
- Erste 24h: Logs stündlich prüfen
|
||||
- Woche 1: Täglich Rechnungsnummern-Sequenz prüfen
|
||||
- Monat 1: Wöchentliche Stichproben
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
### Problem: "Error:2008" im Log
|
||||
|
||||
**Ursache**: Transaction rollback bei Zahlungsverarbeitung
|
||||
|
||||
**Lösung**:
|
||||
|
||||
1. Log-Eintrag analysieren für genaue Fehlermeldung
|
||||
2. Prüfen ob Payone-Daten korrekt sind
|
||||
3. Database Connection überprüfen
|
||||
4. Ggf. Queue Worker neu starten
|
||||
|
||||
### Problem: Langsame Zahlungsverarbeitung
|
||||
|
||||
**Ursache**: Lock-Contention bei vielen gleichzeitigen Requests
|
||||
|
||||
**Lösung**:
|
||||
|
||||
1. Prüfen ob wirklich Race Condition oder einfach viele Requests
|
||||
2. MySQL Performance optimieren (Indizes prüfen)
|
||||
3. Queue Worker skalieren
|
||||
4. Monitoring für Lock Wait Times einrichten
|
||||
|
||||
### Problem: "Deadlock detected"
|
||||
|
||||
**Ursache**: Sehr unwahrscheinlich durch konsistente Lock-Reihenfolge
|
||||
|
||||
**Lösung**:
|
||||
|
||||
1. MySQL Error Log prüfen
|
||||
2. Beteiligte Queries identifizieren
|
||||
3. Lock-Reihenfolge in Code prüfen
|
||||
4. MySQL InnoDB Lock Monitor aktivieren
|
||||
|
||||
## 📚 Weiterführende Informationen
|
||||
|
||||
### Relevante Laravel Dokumentation
|
||||
|
||||
- [Database Transactions](https://laravel.com/docs/11.x/database#database-transactions)
|
||||
- [Pessimistic Locking](https://laravel.com/docs/11.x/queries#pessimistic-locking)
|
||||
- [Error Handling](https://laravel.com/docs/11.x/errors)
|
||||
|
||||
### MySQL Locking
|
||||
|
||||
- [InnoDB Locking](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html)
|
||||
- [Lock Wait Timeout](https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout)
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Locks so kurz wie möglich halten
|
||||
- Konsistente Lock-Reihenfolge
|
||||
- Immer mit Transactions arbeiten
|
||||
- Proper Error Handling & Logging
|
||||
|
||||
## 🔄 Migration von Alt zu Neu
|
||||
|
||||
### Vor dem Fix
|
||||
|
||||
```php
|
||||
// UNSICHER: Race Condition möglich
|
||||
$number = Invoice::getInvoiceNumber();
|
||||
// ... andere Operationen ...
|
||||
Invoice::makeNextInvoiceNumber();
|
||||
```
|
||||
|
||||
### Nach dem Fix
|
||||
|
||||
```php
|
||||
// SICHER: Atomare Operation mit Lock
|
||||
$number = Invoice::makeNextInvoiceNumber();
|
||||
// Nummer ist garantiert eindeutig
|
||||
```
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
|
||||
- [x] Keine doppelten Rechnungsnummern bei gleichzeitigen Zahlungen
|
||||
- [x] Keine Lücken in der Rechnungsnummern-Sequenz
|
||||
- [x] Keine mehrfache Verarbeitung derselben Zahlung
|
||||
- [x] Proper Error Handling mit Rollback
|
||||
- [x] Comprehensive Test Coverage (32 Tests)
|
||||
- [x] Performance-Impact < 500ms pro Request
|
||||
- [x] Monitoring & Debugging Tools vorhanden
|
||||
- [x] Documentation vollständig
|
||||
|
||||
---
|
||||
|
||||
**Implementiert**: 28.01.2026
|
||||
**Getestet**: 28.01.2026
|
||||
**Status**: ✅ Production Ready
|
||||
**Breaking Changes**: Keine
|
||||
**Migration notwendig**: Nein
|
||||
571
dev/2026-01-28/payment-status-legend-and-race-condition-fix.md
Normal file
571
dev/2026-01-28/payment-status-legend-and-race-condition-fix.md
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
# Payment Status Legende & Race Condition Fix
|
||||
|
||||
**Datum:** 28.01.2026
|
||||
**Bereich:** Payment System / Payone Integration
|
||||
**Status:** ✅ Abgeschlossen
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Umfassende Verbesserung des Payment-Link-Systems mit folgenden Komponenten:
|
||||
|
||||
1. Dynamische mehrsprachige Status-Legende
|
||||
2. Race Condition Fix für Payone Requests
|
||||
3. Status Update für alle Bestellungstypen
|
||||
4. ShoppingInstance Model Korrektur
|
||||
5. Artisan Command für Datenbank-Cleanup
|
||||
|
||||
---
|
||||
|
||||
## Problem 1: Fehlende Status-Übersicht
|
||||
|
||||
### Ausgangslage
|
||||
|
||||
- Benutzer sahen Payment Status-Badges in der Payment Links Übersicht
|
||||
- Keine Erklärung, was die verschiedenen Status/Farben bedeuten
|
||||
- Keine Legende zur Orientierung
|
||||
|
||||
### Lösung
|
||||
|
||||
Dynamische Status-Legende oberhalb der Tabelle mit allen verfügbaren Status.
|
||||
|
||||
---
|
||||
|
||||
## Problem 2: Race Condition bei Payone Requests
|
||||
|
||||
### Ausgangslage
|
||||
|
||||
Payone sendet mehrere Status-Updates für eine Zahlung:
|
||||
|
||||
1. `appointed` - Zahlung autorisiert
|
||||
2. `paid` - Zahlung abgeschlossen
|
||||
|
||||
**Problem:** Wenn die Requests in falscher Reihenfolge ankommen:
|
||||
|
||||
```
|
||||
1. paid kommt zuerst → txaction = 'paid' ✓
|
||||
2. appointed kommt später → txaction = 'appointed' ✗ (überschreibt paid!)
|
||||
```
|
||||
|
||||
Dies führte zu:
|
||||
|
||||
- Falschen Status in `shopping_orders.txaction` und `shopping_payments.txaction`
|
||||
- ShoppingInstance Status blieb teilweise auf 4 (appointed) statt 10 (paid)
|
||||
- Verwirrung bei Benutzern und Support
|
||||
|
||||
### Root Cause
|
||||
|
||||
In `PayoneController.php` wurden die `txaction` Felder **immer** überschrieben:
|
||||
|
||||
```php
|
||||
$shopping_order->txaction = $data['txaction']; // IMMER überschrieben!
|
||||
$shopping_payment->txaction = $data['txaction']; // IMMER überschrieben!
|
||||
```
|
||||
|
||||
### Lösung
|
||||
|
||||
Prioritätsprüfung vor dem Update:
|
||||
|
||||
```php
|
||||
$txaction_priority = [
|
||||
'appointed' => 1,
|
||||
'pending' => 2,
|
||||
'failed' => 3,
|
||||
'paid' => 10, // höchste Priorität - finaler Status
|
||||
];
|
||||
|
||||
// Nur updaten wenn neue Priorität höher ist
|
||||
if ($new_priority > $current_priority) {
|
||||
$shopping_order->txaction = $data['txaction'];
|
||||
$shopping_payment->txaction = $data['txaction'];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Problem 3: Status nur für Abo-Bestellungen
|
||||
|
||||
### Ausgangslage
|
||||
|
||||
In `Payment::paymentStatusPaidAction()` wurde Status 10 (`link_paid`) nur gesetzt wenn:
|
||||
|
||||
```php
|
||||
if ($shopping_order->is_abo) {
|
||||
Util::setInstanceStatusByPayment($shopping_payment, 10);
|
||||
}
|
||||
```
|
||||
|
||||
**Resultat:**
|
||||
|
||||
- ✅ Abo-Bestellungen: Status korrekt auf 10
|
||||
- ❌ Normale Bestellungen: Status blieb auf 4 (appointed)
|
||||
|
||||
### Lösung
|
||||
|
||||
Status 10 wird jetzt **immer** bei erfolgreicher Zahlung gesetzt:
|
||||
|
||||
```php
|
||||
// Set payment link status to paid for all orders
|
||||
if ($shopping_payment) {
|
||||
Util::setInstanceStatusByPayment($shopping_payment, 10);
|
||||
}
|
||||
|
||||
// Abo-spezifische Logik separat
|
||||
if ($shopping_order->is_abo) {
|
||||
AboHelper::setAboActive($shopping_order, 2, true);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Problem 4: ShoppingInstance Model
|
||||
|
||||
### Ausgangslage
|
||||
|
||||
- Tabelle `shopping_instances` hat `identifier` (VARCHAR) als Primary Key
|
||||
- Laravel Model hatte keine Primary Key Konfiguration
|
||||
- Eloquent verwendete standardmäßig `id` (nicht existent)
|
||||
- `save()` schlug fehl mit: "Unknown column 'id' in 'where clause'"
|
||||
|
||||
### Lösung
|
||||
|
||||
Primary Key im Model korrekt konfiguriert:
|
||||
|
||||
```php
|
||||
class ShoppingInstance extends Model
|
||||
{
|
||||
protected $primaryKey = 'identifier';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementierung
|
||||
|
||||
### 1. OrderPaymentService - Zentrale Status-Definition
|
||||
|
||||
**Datei:** `app/Services/OrderPaymentService.php`
|
||||
|
||||
```php
|
||||
public static function getStatusBadgeClasses()
|
||||
{
|
||||
return [
|
||||
'link_sent' => 'default',
|
||||
'link_openly' => 'info',
|
||||
'link_paid' => 'secondary',
|
||||
'link_check' => 'warning',
|
||||
'link_pending' => 'warning',
|
||||
'link_appointed' => 'warning',
|
||||
'link_failed' => 'danger',
|
||||
'link_canceled' => 'danger'
|
||||
];
|
||||
}
|
||||
|
||||
public static function getStatusBadge(ShoppingInstance $shoppingInstance)
|
||||
{
|
||||
$status = $shoppingInstance->getStatus();
|
||||
$badgeClasses = self::getStatusBadgeClasses(); // Nutzt zentrale Definition
|
||||
|
||||
if (isset($badgeClasses[$status])) {
|
||||
return sprintf(
|
||||
' <span class="badge badge-pill badge-%s">%s</span>',
|
||||
$badgeClasses[$status],
|
||||
__('payment.' . $status)
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Eine zentrale Stelle für Status-Definitionen
|
||||
- ✅ DRY Prinzip (Don't Repeat Yourself)
|
||||
- ✅ Leicht erweiterbar für neue Status
|
||||
|
||||
### 2. Payment Links View - Dynamische Legende
|
||||
|
||||
**Datei:** `resources/views/user/order/payment/index.blade.php`
|
||||
|
||||
```blade
|
||||
{{-- Status-Legende --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">{{ __('tables.status') }} - {{ __('legend') }}</h6>
|
||||
<div class="row">
|
||||
@foreach(\App\Services\OrderPaymentService::getStatusBadgeClasses() as $statusKey => $badgeClass)
|
||||
<div class="col-md-3 mb-2">
|
||||
<span class="badge badge-pill badge-{{ $badgeClass }}">
|
||||
{{ __('payment.' . $statusKey) }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
|
||||
- ✅ Vollständig dynamisch
|
||||
- ✅ Automatisch synchron mit Backend-Logik
|
||||
- ✅ Mehrsprachig über Laravel Translations
|
||||
- ✅ Neue Status erscheinen automatisch
|
||||
|
||||
### 3. PayoneController - Race Condition Fix
|
||||
|
||||
**Datei:** `app/Http/Controllers/Api/PayoneController.php`
|
||||
|
||||
```php
|
||||
// Define txaction priority (higher number = higher priority)
|
||||
$txaction_priority = [
|
||||
'appointed' => 1,
|
||||
'pending' => 2,
|
||||
'failed' => 3,
|
||||
'paid' => 10, // highest priority - final state
|
||||
];
|
||||
|
||||
$current_priority = isset($txaction_priority[$shopping_order->txaction])
|
||||
? $txaction_priority[$shopping_order->txaction] : 0;
|
||||
$new_priority = isset($txaction_priority[$data['txaction']])
|
||||
? $txaction_priority[$data['txaction']] : 0;
|
||||
|
||||
// Only update txaction if new priority is higher than current
|
||||
if ($new_priority > $current_priority) {
|
||||
$shopping_order->txaction = $data['txaction'];
|
||||
$shopping_order->save();
|
||||
$shopping_payment->txaction = $data['txaction'];
|
||||
$shopping_payment->save();
|
||||
} else {
|
||||
MyLog::writeLog(
|
||||
'payone',
|
||||
'info',
|
||||
'txaction not updated (current has higher/equal priority)',
|
||||
$data,
|
||||
false
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Szenarien:**
|
||||
|
||||
| Szenario | Request 1 | Request 2 | Resultat | Status |
|
||||
| -------- | ------------- | ------------- | -------- | -------------------------------- |
|
||||
| Normal | appointed (1) | paid (10) | paid | ✅ Korrekt |
|
||||
| Race | paid (10) | appointed (1) | paid | ✅ Korrekt (nicht überschrieben) |
|
||||
| Failed | appointed (1) | failed (3) | failed | ✅ Korrekt |
|
||||
|
||||
### 4. Payment Service - Status für alle Bestellungen
|
||||
|
||||
**Datei:** `app/Services/Payment.php`
|
||||
|
||||
**Vorher:**
|
||||
|
||||
```php
|
||||
// the Order is Pay, so we can set the Status in the Abo
|
||||
if ($shopping_order->is_abo) {
|
||||
if ($shopping_payment) {
|
||||
Util::setInstanceStatusByPayment($shopping_payment, 10); // link_paid
|
||||
}
|
||||
AboHelper::setAboActive($shopping_order, 2, true);
|
||||
}
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
|
||||
```php
|
||||
// Set payment link status to paid for all orders
|
||||
if ($shopping_payment) {
|
||||
Util::setInstanceStatusByPayment($shopping_payment, 10); // link_paid
|
||||
$shopping_payment->identifier = null;
|
||||
$shopping_payment->save();
|
||||
}
|
||||
|
||||
// the Order is Pay, so we can set the Status in the Abo
|
||||
if ($shopping_order->is_abo) {
|
||||
AboHelper::setAboActive($shopping_order, 2, true);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. ShoppingInstance Model - Primary Key
|
||||
|
||||
**Datei:** `app/Models/ShoppingInstance.php`
|
||||
|
||||
```php
|
||||
class ShoppingInstance extends Model
|
||||
{
|
||||
protected $primaryKey = 'identifier';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
// Rest des Models...
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Artisan Command - Datenbank Cleanup
|
||||
|
||||
**Datei:** `app/Console/Commands/FixPaymentLinkStatus.php`
|
||||
|
||||
```php
|
||||
class FixPaymentLinkStatus extends Command
|
||||
{
|
||||
protected $signature = 'payment:fix-link-status {--dry-run : Run without making changes}';
|
||||
|
||||
protected $description = 'Fix payment link status for paid orders';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$isDryRun = $this->option('dry-run');
|
||||
|
||||
// Find all paid payments with identifiers
|
||||
$paidPayments = ShoppingPayment::whereNotNull('identifier')
|
||||
->whereHas('shopping_order', function ($query) {
|
||||
$query->where('paid', 1)
|
||||
->where('txaction', 'paid');
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($paidPayments as $payment) {
|
||||
$instance = ShoppingInstance::where('identifier', $payment->identifier)->first();
|
||||
|
||||
if ($instance && $instance->status < 10) {
|
||||
if (!$isDryRun) {
|
||||
$instance->status = 10;
|
||||
$instance->save();
|
||||
}
|
||||
$this->line("✅ Updated payment #{$payment->id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- ✅ Dry-Run Modus (`--dry-run`)
|
||||
- ✅ Detaillierte Ausgabe mit Statistiken
|
||||
- ✅ Fehlerbehandlung für fehlende Instances
|
||||
- ✅ Progress-Anzeige
|
||||
|
||||
---
|
||||
|
||||
## Übersetzungen
|
||||
|
||||
### Spanische Übersetzungen korrigiert
|
||||
|
||||
**Datei:** `resources/lang/es/payment.php`
|
||||
|
||||
| Key | Alt | Neu | Grund |
|
||||
| ------------ | --------------- | ------------------ | ---------------------------------------------------- |
|
||||
| `link_paid` | "pagado" | "Pago exitoso" | Zu kurz/generisch, inkonsistent mit anderen Sprachen |
|
||||
| `link_check` | "Pago en curso" | "Pago en revisión" | Falsche Bedeutung ("in progress" statt "in review") |
|
||||
|
||||
**Alle Sprachen vollständig:**
|
||||
|
||||
- ✅ Deutsch (DE) - alle 8 Status korrekt
|
||||
- ✅ Englisch (EN) - alle 8 Status korrekt
|
||||
- ✅ Spanisch (ES) - 2 Korrekturen vorgenommen
|
||||
|
||||
---
|
||||
|
||||
## Status-Hierarchie
|
||||
|
||||
### ShoppingInstance Status
|
||||
|
||||
```php
|
||||
public $statuses = [
|
||||
0 => 'link_sent', // Link versendet
|
||||
1 => 'link_openly', // Link geöffnet
|
||||
2 => 'link_check', // In Prüfung
|
||||
3 => 'link_pending', // In Bearbeitung
|
||||
4 => 'link_appointed', // Angewiesen
|
||||
5 => 'link_failed', // Fehlgeschlagen
|
||||
6 => 'link_canceled', // Abgebrochen
|
||||
10 => 'link_paid', // Bezahlt (FINAL)
|
||||
];
|
||||
```
|
||||
|
||||
### Badge-Farben
|
||||
|
||||
```php
|
||||
'link_sent' => 'default', // Grau
|
||||
'link_openly' => 'info', // Blau
|
||||
'link_paid' => 'secondary', // Dunkelgrau
|
||||
'link_check' => 'warning', // Gelb
|
||||
'link_pending' => 'warning', // Gelb
|
||||
'link_appointed' => 'warning',// Gelb
|
||||
'link_failed' => 'danger', // Rot
|
||||
'link_canceled' => 'danger' // Rot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datenbank Cleanup Ergebnisse
|
||||
|
||||
### Command Ausführung (28.01.2026)
|
||||
|
||||
```bash
|
||||
php artisan payment:fix-link-status
|
||||
```
|
||||
|
||||
**Statistik:**
|
||||
|
||||
- ✅ **2.117** Payment Links korrigiert (Status 4 → 10)
|
||||
- ✅ **490** bereits korrekt (durch neue Logik)
|
||||
- ⚠️ **12.754** ShoppingInstances nicht gefunden
|
||||
- 📊 **15.361** bezahlte Payments total
|
||||
|
||||
### Warum fehlen 12.754 ShoppingInstances?
|
||||
|
||||
**Grund:** ShoppingInstances werden nach erfolgreicher Zahlung gelöscht (by design).
|
||||
|
||||
1. Kunde bekommt Payment Link → ShoppingInstance wird erstellt
|
||||
2. Kunde zahlt → ShoppingOrder.paid = 1
|
||||
3. System löscht ShoppingInstance (nicht mehr benötigt)
|
||||
4. ShoppingPayment.identifier bleibt erhalten (historische Referenz)
|
||||
|
||||
**Ist das ein Problem?** Nein! Die Zahlung ist abgeschlossen, die temporäre Instance wird nicht mehr benötigt.
|
||||
|
||||
### Beispiel-Output
|
||||
|
||||
```
|
||||
🔎 Searching for payment links with incorrect status...
|
||||
|
||||
Found 15361 paid payments with identifiers
|
||||
|
||||
✅ Payment #41963: link_appointed (4) → link_paid (10) (Order #45027, Amount: 92,60 EUR)
|
||||
✅ Payment #41967: link_appointed (4) → link_paid (10) (Order #45031, Amount: 115,90 EUR)
|
||||
✅ Payment #41970: link_appointed (4) → link_paid (10) (Order #45034, Amount: 27,80 EUR)
|
||||
⚠️ ShoppingInstance not found for identifier: c5cdac250...
|
||||
⚠️ ShoppingInstance not found for identifier: 0ea47bde4...
|
||||
|
||||
📊 Summary:
|
||||
+-----------------+-------+
|
||||
| Status | Count |
|
||||
+-----------------+-------+
|
||||
| Fixed/Would fix | 2117 |
|
||||
| Already correct | 490 |
|
||||
| Errors | 12754 |
|
||||
| Total processed | 15361 |
|
||||
+-----------------+-------+
|
||||
|
||||
✨ Successfully updated 2117 payment link(s)!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manuelle Tests durchgeführt
|
||||
|
||||
#### 1. Status-Legende
|
||||
|
||||
- ✅ Legende wird oberhalb der Tabelle angezeigt
|
||||
- ✅ Alle 8 Status werden korrekt dargestellt
|
||||
- ✅ Farben entsprechen den Badges in der Tabelle
|
||||
- ✅ Mehrsprachigkeit funktioniert (DE, EN, ES)
|
||||
|
||||
#### 2. Race Condition Fix
|
||||
|
||||
- ✅ Payone Request: paid → appointed → txaction bleibt "paid" ✓
|
||||
- ✅ Payone Request: appointed → paid → txaction wird "paid" ✓
|
||||
- ✅ Logging erfolgt bei übersprungenen Updates
|
||||
- ✅ ShoppingInstance Status bleibt auf 10 bei Race Condition
|
||||
|
||||
#### 3. Status für alle Bestellungen
|
||||
|
||||
- ✅ Normale Bestellung bezahlt → Status 10 gesetzt
|
||||
- ✅ Abo-Bestellung bezahlt → Status 10 gesetzt
|
||||
- ✅ Beide Szenarien funktionieren korrekt
|
||||
|
||||
#### 4. Artisan Command
|
||||
|
||||
- ✅ Dry-Run zeigt korrekte Anzahl zu korrigierender Einträge
|
||||
- ✅ Echte Ausführung aktualisiert Datenbank korrekt
|
||||
- ✅ Fehlerbehandlung für fehlende Instances funktioniert
|
||||
- ✅ Statistik ist korrekt und übersichtlich
|
||||
|
||||
---
|
||||
|
||||
## Vorteile der Lösung
|
||||
|
||||
### Für Benutzer
|
||||
|
||||
- ✅ Klare Übersicht über alle möglichen Payment Status
|
||||
- ✅ Korrekte Status-Anzeige (keine Race Conditions mehr)
|
||||
- ✅ Mehrsprachige Unterstützung
|
||||
- ✅ Besseres Verständnis des Zahlungsprozesses
|
||||
|
||||
### Für Entwickler
|
||||
|
||||
- ✅ Zentrale Status-Definition (DRY Prinzip)
|
||||
- ✅ Automatische Synchronisation zwischen Backend und Frontend
|
||||
- ✅ Einfache Erweiterbarkeit für neue Status
|
||||
- ✅ Klare Logging-Informationen bei Problemen
|
||||
- ✅ Tool zur Datenbereinigung vorhanden
|
||||
|
||||
### Für das System
|
||||
|
||||
- ✅ Konsistente Datenhaltung
|
||||
- ✅ Keine verlorenen "paid" Status mehr
|
||||
- ✅ Historische Daten bereinigt
|
||||
- ✅ Robustere Payone-Integration
|
||||
|
||||
---
|
||||
|
||||
## Zukünftige Verbesserungen (Optional)
|
||||
|
||||
### 1. Identifier Cleanup
|
||||
|
||||
Optional: ShoppingPayment.identifier auf NULL setzen, wenn ShoppingInstance nicht mehr existiert:
|
||||
|
||||
```php
|
||||
// In FixPaymentLinkStatus Command:
|
||||
if (!$instance && $payment->identifier) {
|
||||
$payment->identifier = null;
|
||||
$payment->save();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Monitoring
|
||||
|
||||
Status-Änderungen in separater Log-Tabelle tracken:
|
||||
|
||||
```php
|
||||
PaymentStatusLog::create([
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => $newStatus,
|
||||
'source' => 'payone',
|
||||
'txaction' => $txaction,
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Webhooks
|
||||
|
||||
Benachrichtigungen bei unerwarteten Status-Änderungen:
|
||||
|
||||
- E-Mail an Admin bei "paid" → "appointed" Versuch
|
||||
- Slack-Notification bei kritischen Fehlern
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Umfassende Verbesserung des Payment-Systems mit:
|
||||
|
||||
- 🎨 Benutzerfreundliche Status-Legende
|
||||
- 🐛 Race Condition Bug behoben
|
||||
- 🔧 Konsistente Status-Vergabe
|
||||
- 🛠️ Model-Korrekturen
|
||||
- 🧹 Datenbank-Cleanup Tool
|
||||
- 🌍 Mehrsprachigkeit
|
||||
- 📊 2.117 historische Einträge korrigiert
|
||||
|
||||
**Status:** ✅ Produktionsreif und deployed
|
||||
**Datum:** 28.01.2026
|
||||
412
dev/2026-02-04/user-language-preference.md
Normal file
412
dev/2026-02-04/user-language-preference.md
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
# Benutzer-Spracheinstellung für Dokumente
|
||||
|
||||
**Datum:** 04.02.2026
|
||||
**Bezug:** next-steps.md - Punkt 6: Mehrsprachigkeit: Rechnungen, Provisionen, Lieferscheine
|
||||
|
||||
## Übersicht
|
||||
|
||||
Diese Implementierung fügt ein Sprachfeld zu den Benutzerdaten hinzu, damit Benutzer selbst entscheiden können, in welcher Sprache sie ihre Dokumente (Rechnungen, Provisionsabrechnungen, Lieferscheine) erhalten möchten.
|
||||
|
||||
## Problem
|
||||
|
||||
Bisher wurde die Sprache für Dokumente immer von der aktuellen Session-Sprache übernommen (`\App::getLocale()`). Das Problem dabei:
|
||||
|
||||
- Benutzer, die auf Deutsch surfen aber Spanisch bevorzugen, bekommen Dokumente auf Deutsch
|
||||
- Die Spracheinstellung ist nicht persistent
|
||||
|
||||
## Lösung
|
||||
|
||||
### 1. Datenbankänderung
|
||||
|
||||
**Migration 1:** `2026_02_04_101805_add_language_to_user_accounts_table.php`
|
||||
|
||||
```php
|
||||
Schema::table('user_accounts', function (Blueprint $table) {
|
||||
$table->string('language', 5)->nullable()->after('notice');
|
||||
});
|
||||
```
|
||||
|
||||
**Migration 2:** `2026_02_04_102455_change_language_default_on_user_accounts_table.php`
|
||||
|
||||
Setzt den Default auf NULL (statt 'de'), damit die aktuelle App-Locale (`\App::getLocale()`) als Fallback verwendet wird.
|
||||
|
||||
### 2. Model-Anpassungen
|
||||
|
||||
#### UserAccount & ShoppingUser
|
||||
|
||||
Beide Models haben jetzt:
|
||||
|
||||
```php
|
||||
// Accessor - gibt App-Locale zurück wenn NULL:
|
||||
public function getLanguageAttribute($value): string
|
||||
{
|
||||
return $value ?: \App::getLocale();
|
||||
}
|
||||
|
||||
// Alias für Konsistenz:
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
// Sprachen werden aus config/localization.php geladen:
|
||||
public static function getAvailableLanguages(): array
|
||||
{
|
||||
$locales = config('localization.supportedLocales', []);
|
||||
$languages = [];
|
||||
foreach ($locales as $code => $locale) {
|
||||
$languages[$code] = $locale['native'] ?? $locale['name'] ?? $code;
|
||||
}
|
||||
return $languages;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Frontend-Formulare
|
||||
|
||||
| Formular | Datei | Beschreibung |
|
||||
| -------------------- | ----------------------------------------------------------- | ------------------------------------------------ |
|
||||
| Berater-Profil | `resources/views/user/user_form.blade.php` | Sprachauswahl zwischen Bankdaten und Steuerdaten |
|
||||
| Kunden-Bearbeitung | `resources/views/admin/customer/_edit.blade.php` | Sprachauswahl nach Bemerkungen |
|
||||
| Kunden-Detailansicht | `resources/views/admin/customer/_customer_detail.blade.php` | Sprache wird bei Rechnungsadresse angezeigt |
|
||||
| Checkout | `resources/views/web/templates/checkout.blade.php` | Sprachauswahl im Bestellformular |
|
||||
|
||||
### 4. Übersetzungen
|
||||
|
||||
**account.php** (für Berater-Profil):
|
||||
|
||||
| Key | DE | EN | ES |
|
||||
| -------------------- | ----------------------------------------- | ----------------------------------------------- | --------------------------- |
|
||||
| `language_settings` | Spracheinstellungen | Language settings | Configuración de idioma |
|
||||
| `preferred_language` | Bevorzugte Sprache | Preferred language | Idioma preferido |
|
||||
| `language_hint` | Diese Sprache wird für Ihre Rechnungen... | This language will be used for your invoices... | Este idioma se utilizará... |
|
||||
|
||||
**customer.php** (für Kunden-Verwaltung):
|
||||
|
||||
| Key | DE | EN | ES |
|
||||
| --------------- | --------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `language_hint` | Die Rechnungen und Dokumente werden in dieser Sprache erstellt. | Invoices and documents will be created in this language. | Las facturas y documentos se crearán en este idioma. |
|
||||
|
||||
## Verfügbare Sprachen
|
||||
|
||||
Die verfügbaren Sprachen werden aus `config/localization.php` unter `supportedLocales` geladen.
|
||||
|
||||
Aktuell aktiv:
|
||||
|
||||
- `de` - Deutsch
|
||||
- `en` - English
|
||||
- `es` - Español
|
||||
|
||||
## Verhalten
|
||||
|
||||
### UserAccount (Berater/Consultants)
|
||||
|
||||
- Benutzer können ihre bevorzugte Sprache im Profil unter "Spracheinstellungen" auswählen
|
||||
- `$user->account->language` gibt immer einen Wert zurück (nie NULL dank Accessor)
|
||||
- Wird für Provisionsabrechnungen verwendet
|
||||
|
||||
### ShoppingUser (Kunden)
|
||||
|
||||
- Berater können bei Kunden eine bevorzugte Sprache hinterlegen
|
||||
- Im Checkout kann der Kunde die Sprache selbst wählen
|
||||
- `$shopping_user->language` gibt immer einen Wert zurück (nie NULL dank Accessor)
|
||||
- Wird für Rechnungen und Lieferscheine verwendet
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
| ------------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
||||
| `database/migrations/2026_02_04_101805_add_language_to_user_accounts_table.php` | Neue Migration - Feld hinzufügen |
|
||||
| `database/migrations/2026_02_04_102455_change_language_default_on_user_accounts_table.php` | Default auf NULL ändern |
|
||||
| `app/Models/UserAccount.php` | Accessor, `getLocale()`, `getAvailableLanguages()` |
|
||||
| `app/Models/ShoppingUser.php` | Accessor, `getAvailableLanguages()` |
|
||||
| `resources/views/user/user_form.blade.php` | Sprachauswahl für Berater |
|
||||
| `resources/views/admin/customer/_edit.blade.php` | Sprachauswahl für Kunden |
|
||||
| `resources/views/admin/customer/_customer_detail.blade.php` | Sprache in Detailansicht |
|
||||
| `resources/views/web/templates/checkout.blade.php` | Sprachauswahl im Checkout |
|
||||
| `resources/lang/de/account.php` | Neue Übersetzungen |
|
||||
| `resources/lang/en/account.php` | Neue Übersetzungen |
|
||||
| `resources/lang/es/account.php` | Neue Übersetzungen |
|
||||
| `resources/lang/de/customer.php` | Neue Übersetzung `language_hint` |
|
||||
| `resources/lang/en/customer.php` | Neue Übersetzung `language_hint` |
|
||||
| `resources/lang/es/customer.php` | Neue Übersetzung `language_hint` |
|
||||
|
||||
## Test
|
||||
|
||||
### UserAccount (Berater-Profil)
|
||||
|
||||
1. Als Berater einloggen
|
||||
2. Profil bearbeiten (Route: `/user/edit`)
|
||||
3. Neue Sektion "Spracheinstellungen" sichtbar
|
||||
4. Sprache auswählen und speichern
|
||||
5. Prüfen: Feld wird in `user_accounts.language` gespeichert
|
||||
|
||||
### ShoppingUser (Kunden-Verwaltung)
|
||||
|
||||
1. Als Berater einloggen
|
||||
2. Kunde bearbeiten oder anlegen
|
||||
3. Sprachauswahl im Formular sichtbar
|
||||
4. Sprache auswählen und speichern
|
||||
5. Prüfen: Feld wird in `shopping_users.language` gespeichert
|
||||
|
||||
### Checkout
|
||||
|
||||
1. Shop aufrufen und Produkte in den Warenkorb legen
|
||||
2. Zur Kasse gehen
|
||||
3. Sprachauswahl im Rechnungsadresse-Bereich sichtbar
|
||||
4. Sprache auswählen
|
||||
5. Bestellung abschließen
|
||||
6. Prüfen: Rechnung wird in der gewählten Sprache erstellt
|
||||
|
||||
## Phase 2: Mehrsprachige PDF-Erstellung
|
||||
|
||||
### Konzept
|
||||
|
||||
Für finanzrechtliche Anforderungen wird **immer** ein deutsches Original erstellt. Wenn der Kunde/Benutzer eine andere Sprache bevorzugt, wird zusätzlich eine Kopie in seiner Landessprache generiert.
|
||||
|
||||
### Dateinamen-Schema
|
||||
|
||||
| Dokument | Deutsch (Original) | Kundensprache (z.B. ES) |
|
||||
| ------------ | ----------------------------------- | -------------------------------------- |
|
||||
| Rechnung | `202600123-MIVITA-Rechnung.pdf` | `202600123-MIVITA-Rechnung-es.pdf` |
|
||||
| Lieferschein | `202600123-MIVITA-Lieferschein.pdf` | `202600123-MIVITA-Lieferschein-es.pdf` |
|
||||
| Gutschrift | `GS202600123-MIVITA-Gutschrift.pdf` | `GS202600123-MIVITA-Gutschrift-es.pdf` |
|
||||
|
||||
### Implementierung
|
||||
|
||||
#### Invoice Service (`app/Services/Invoice.php`)
|
||||
|
||||
Neue Methoden für lokalisierte Dateinamen:
|
||||
|
||||
```php
|
||||
public static function makeInvoiceFilenameLocale($invoice_number, $locale)
|
||||
public static function makeDeliveryFilenameLocale($invoice_number, $locale)
|
||||
```
|
||||
|
||||
#### Credit Service (`app/Services/Credit.php`)
|
||||
|
||||
Neue Methode für lokalisierte Gutschrift-Dateinamen:
|
||||
|
||||
```php
|
||||
public static function makeCreditFilenameLocale($credit_number, $locale)
|
||||
```
|
||||
|
||||
#### InvoiceRepository (`app/Repositories/InvoiceRepository.php`)
|
||||
|
||||
Die `makePDF()` Methode erstellt jetzt:
|
||||
|
||||
1. Deutsches Original (immer)
|
||||
2. Lokalisierte Kopie (wenn Kundensprache ≠ DE)
|
||||
|
||||
Neue Helper-Methoden:
|
||||
|
||||
```php
|
||||
private function createPDFFiles(array $data, string $locale)
|
||||
private function getTemplateForLocale(string $locale): string
|
||||
```
|
||||
|
||||
#### CreditRepository (`app/Repositories/CreditRepository.php`)
|
||||
|
||||
Die `create()` Methode erstellt jetzt:
|
||||
|
||||
1. Deutsches Original (immer)
|
||||
2. Lokalisierte Kopie (wenn Benutzersprache ≠ DE)
|
||||
|
||||
Neue Helper-Methoden:
|
||||
|
||||
```php
|
||||
private function createCreditPDF(array $data, string $path, string $dir, string $filename, string $locale, bool $is_copy)
|
||||
private function getTemplateForLocale(string $locale): string
|
||||
```
|
||||
|
||||
### PDF-Templates nach Sprache
|
||||
|
||||
Die Templates für den PDF-Merger werden automatisch basierend auf der Sprache gewählt.
|
||||
|
||||
**Konfiguration** in `config/localization.php`:
|
||||
|
||||
```php
|
||||
'availableTemplates' => ['de', 'en', 'es', 'fr'],
|
||||
```
|
||||
|
||||
| Sprache | Template |
|
||||
| ----------- | -------------------------------- |
|
||||
| Deutsch | `template_invoice_de` |
|
||||
| Englisch | `template_invoice_en` |
|
||||
| Spanisch | `template_invoice_es` |
|
||||
| Französisch | `template_invoice_fr` |
|
||||
| Sonstige | `template_invoice_de` (Fallback) |
|
||||
|
||||
#### UserInvoice Model (`app/Models/UserInvoice.php`)
|
||||
|
||||
Neue Methoden für lokalisierten Zugriff:
|
||||
|
||||
```php
|
||||
public function getDownloadPathLocale($locale = null, $full = false)
|
||||
public function getDownloadPathDeliveryLocale($locale = null, $full = false)
|
||||
public function getFilenameLocale($locale = null)
|
||||
```
|
||||
|
||||
#### UserCredit Model (`app/Models/UserCredit.php`)
|
||||
|
||||
Neue Methoden für lokalisierten Zugriff:
|
||||
|
||||
```php
|
||||
public function getDownloadPathLocale($locale = null, $full = false)
|
||||
public function getFilenameLocale($locale = null)
|
||||
```
|
||||
|
||||
#### MailInvoice (`app/Mail/MailInvoice.php`)
|
||||
|
||||
Versendet **beide Versionen** als Anhänge:
|
||||
|
||||
1. Deutsches Original (immer)
|
||||
2. Lokalisierte Kopie (wenn Kundensprache ≠ DE und Datei existiert)
|
||||
|
||||
#### MailCredit (`app/Mail/MailCredit.php`)
|
||||
|
||||
Versendet **beide Versionen** als Anhänge:
|
||||
|
||||
1. Deutsches Original (immer)
|
||||
2. Lokalisierte Kopie (wenn Benutzersprache ≠ DE und Datei existiert)
|
||||
|
||||
### Kopie-Kennzeichnung in PDFs
|
||||
|
||||
Lokalisierte Kopien werden mit einem Hinweis über dem Datum gekennzeichnet:
|
||||
|
||||
| Dokumenttyp | Hinweis (in Kundensprache) |
|
||||
| ------------ | -------------------------- |
|
||||
| Rechnung | "Invoice copy: EN" |
|
||||
| Lieferschein | "Delivery note copy: EN" |
|
||||
| Gutschrift | "Credit note copy: EN" |
|
||||
|
||||
**Übersetzungen** (`resources/lang/*/pdf.php`):
|
||||
|
||||
| Key | DE | EN | ES |
|
||||
| --------------- | ----------------- | ------------------ | --------------------------- |
|
||||
| `invoice_copy` | Rechnungskopie | Invoice copy | Copia de factura |
|
||||
| `delivery_copy` | Lieferscheinkopie | Delivery note copy | Copia del albarán |
|
||||
| `credit_copy` | Gutschriftkopie | Credit note copy | Copia de la nota de crédito |
|
||||
|
||||
### Geänderte Dateien (Phase 2)
|
||||
|
||||
| Datei | Änderung |
|
||||
| ---------------------------------------- | ------------------------------------------------------------- |
|
||||
| `app/Services/Invoice.php` | `makeInvoiceFilenameLocale()`, `makeDeliveryFilenameLocale()` |
|
||||
| `app/Services/Credit.php` | `makeCreditFilenameLocale()` |
|
||||
| `app/Repositories/InvoiceRepository.php` | Mehrsprachige PDF-Erstellung mit `is_copy` Flag |
|
||||
| `app/Repositories/CreditRepository.php` | Mehrsprachige Gutschrift-Erstellung mit `is_copy` Flag |
|
||||
| `app/Models/UserInvoice.php` | Lokalisierte Pfad-Methoden |
|
||||
| `app/Models/UserCredit.php` | Lokalisierte Pfad-Methoden |
|
||||
| `app/Mail/MailInvoice.php` | DE Original + lokalisierte Version als Anhänge |
|
||||
| `app/Mail/MailCredit.php` | DE Original + lokalisierte Version als Anhänge |
|
||||
| `resources/views/pdf/invoice.blade.php` | Kopie-Hinweis über Datum |
|
||||
| `resources/views/pdf/delivery.blade.php` | Kopie-Hinweis über Datum |
|
||||
| `resources/views/pdf/credit.blade.php` | Kopie-Hinweis über Datum |
|
||||
| `resources/lang/de/pdf.php` | Übersetzungen für Kopie-Hinweise |
|
||||
| `resources/lang/en/pdf.php` | Übersetzungen für Kopie-Hinweise |
|
||||
| `resources/lang/es/pdf.php` | Übersetzungen für Kopie-Hinweise |
|
||||
|
||||
### Ablauf nach Implementierung
|
||||
|
||||
```
|
||||
Bestellung → InvoiceRepository::create()
|
||||
│
|
||||
▼
|
||||
makePDF()
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
DE PDF (Original) Kundensprach-PDF (Kopie)
|
||||
mit "Rechnungskopie: ES"
|
||||
│ │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
▼
|
||||
sendInvoiceMail()
|
||||
│
|
||||
▼
|
||||
Anhänge: DE Original + Kundensprach-Kopie
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
1. Kunde mit Spanisch (`es`) als bevorzugte Sprache anlegen
|
||||
2. Bestellung für diesen Kunden abschließen
|
||||
3. Prüfen:
|
||||
- Im Speicherordner existieren beide PDFs: `*-Rechnung.pdf` und `*-Rechnung-es.pdf`
|
||||
- Das spanische PDF enthält den Hinweis "Copia de factura: ES" über dem Datum
|
||||
- Die E-Mail enthält **beide** Anhänge (DE Original + ES Kopie)
|
||||
4. Im Admin sind beide Versionen verfügbar (DE für Finanzamt, ES für Kundenservice)
|
||||
|
||||
## Phase 3: Admin-Download-Buttons für lokalisierte PDFs
|
||||
|
||||
### Route-Erweiterung
|
||||
|
||||
Die `storage_file` Route wurde um einen optionalen `locale` Parameter erweitert:
|
||||
|
||||
```php
|
||||
// routes/shared/common.php
|
||||
Route::get('/storage/file/{id}/{from}/{do?}/{locale?}', 'FileController@show')->name('storage_file');
|
||||
```
|
||||
|
||||
### FileController
|
||||
|
||||
Der Controller unterstützt jetzt den optionalen `locale` Parameter für `invoice`, `delivery` und `credit`:
|
||||
|
||||
```php
|
||||
public function show($id = null, $from = null, $do = 'file', $locale = null)
|
||||
```
|
||||
|
||||
### UserInvoice Model
|
||||
|
||||
Neue Methoden zur Ermittlung verfügbarer Sprachen:
|
||||
|
||||
```php
|
||||
public function getAvailableLocales(): array // Gibt ['en', 'es'] zurück
|
||||
public function hasLocale(string $locale): bool
|
||||
```
|
||||
|
||||
### Angepasste Views
|
||||
|
||||
| View | Änderung |
|
||||
| ------------------------------------------------ | ------------------------------------------- |
|
||||
| `resources/views/admin/sales/_detail.blade.php` | Download-Buttons für lokalisierte Versionen |
|
||||
| `resources/views/portal/order/_detail.blade.php` | Download-Buttons für lokalisierte Versionen |
|
||||
|
||||
### Darstellung im Admin
|
||||
|
||||
Wenn lokalisierte Versionen existieren, werden zusätzliche Buttons angezeigt:
|
||||
|
||||
- **DE-Button** (ausgefüllt): `<i class="fa fa-download"></i>` - Deutsches Original
|
||||
- **Locale-Button** (outline): `<i class="fa fa-download"></i> ES` - Lokalisierte Version
|
||||
|
||||
### UserCredit Model
|
||||
|
||||
Neue Methoden zur Ermittlung verfügbarer Sprachen (analog zu UserInvoice):
|
||||
|
||||
```php
|
||||
public function getAvailableLocales(): array // Gibt ['en', 'es'] zurück
|
||||
public function hasLocale(string $locale): bool
|
||||
```
|
||||
|
||||
### Angepasste Views für Gutschriften
|
||||
|
||||
| View/Controller | Änderung |
|
||||
| -------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `app/Http/Controllers/PaymentCreditController.php` | Download-Buttons für lokalisierte Versionen in Datatable |
|
||||
|
||||
### Darstellung der Gutschriften im Admin
|
||||
|
||||
Die Gutschriften-Tabelle (`/admin/payments/credit`) zeigt jetzt:
|
||||
|
||||
- **DE-Button** (ausgefüllt): `<i class="fa fa-download"></i>` - Deutsches Original
|
||||
- **Locale-Button** (outline): `<i class="fa fa-download"></i> ES` - Lokalisierte Version (wenn vorhanden)
|
||||
|
||||
## Zusammenfassung: Mehrsprachige Dokumente
|
||||
|
||||
| Dokument | PDF-Erstellung | E-Mail-Anhänge | Admin-Download | Kopie-Hinweis |
|
||||
| ------------ | -------------- | -------------- | -------------- | ------------- |
|
||||
| Rechnung | ✅ | ✅ | ✅ | ✅ |
|
||||
| Lieferschein | ✅ | ✅ | ✅ | ✅ |
|
||||
| Gutschrift | ✅ | ✅ | ✅ | ✅ |
|
||||
5
dev/2026-02-06/EXTF_Buchungen.csv
Normal file
5
dev/2026-02-06/EXTF_Buchungen.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
EXTF;700;21;Buchungsstapel;13;2,02E+16;;ERP;Alois Ried;;1001;1;20250101;4;20250701;20250731;;;1;0;0;EUR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
Umsatz (ohne Soll/Haben-Kz);Soll/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basis-Umsatz;WKZ Basis-Umsatz;Konto;Gegenkonto (ohne BU-SchlŸssel);BU-SchlŸssel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;GeschŠftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost-Menge;EU-Land u. UStID;EU-Steuersatz;Abw. Versteuerungsart;Sachverhalt L+L;FunktionsergŠnzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 FunktionsergŠnzung;Zusatzinformation - Art 1;Zusatzinformation- Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation- Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation- Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation- Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation- Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation- Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation- Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation- Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation- Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation- Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation- Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation- Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation- Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation- Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation- Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation- Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation- Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation- Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation- Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation- Inhalt 20;StŸck;Gewicht;Zahlweise;Forderungsart;Veranlagungsjahr;Zugeordnete FŠlligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-SchlŸssel (Anzahlungen);EU-Land (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erlšskonto (Anzahlungen);Herkunft-Kz;Buchungs GUID;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode
|
||||
119;H;;;;;8400;10000;9;107;201911;;;Nachname Vorname, Kundennummer;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
107;H;;;;;8401;10000;8;107;201911;;;Nachname Vorname, Kundennummer;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
119;S;;;;;4760;70000;9;207;202501;;;"Karrer Christian; BeraterNr";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
BIN
dev/2026-02-06/EXTF_Buchungen.xlsx
Normal file
BIN
dev/2026-02-06/EXTF_Buchungen.xlsx
Normal file
Binary file not shown.
143
dev/2026-02-06/next-steps.md
Normal file
143
dev/2026-02-06/next-steps.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
### Die drei offenen Tickets aus
|
||||
|
||||
dev/22-01-2026/next-steps.md
|
||||
|
||||
### [✓] 13. Steuerberater-Modul (DATEV-Export)
|
||||
|
||||
**Priorität:** Hoch
|
||||
**Status:** Umgesetzt (Phase 1)
|
||||
|
||||
**Umgesetzte Features:**
|
||||
|
||||
1. **DATEV EXTF-Buchungsstapel-Export** (`DatevExportService`)
|
||||
|
||||
- Rechnungen, Gutschriften/Provisionen, Stornos
|
||||
- EXTF-Header + 116-Spalten CSV gemäß DATEV Developer Portal
|
||||
- Periodenauswahl (Monat/Jahr)
|
||||
|
||||
2. **Erlöskonto-Mapping nach Steuerberater-Vorgabe**
|
||||
|
||||
- 8400 Erlöse 19% (BU 9) – Inland
|
||||
- 8300 Erlöse 7% (BU 8) – Inland reduziert
|
||||
- 8125 IG-Lieferung steuerfrei §4 Nr. 1b (BU 1) – **nur bei EU + gültiger USt-ID**
|
||||
- 8120 Drittland steuerfrei §4 Nr. 1a (BU 11)
|
||||
|
||||
3. **Fachliche Regel: EU-Steuerbefreiung nur mit USt-ID**
|
||||
|
||||
- Nur registrierte Berater (`auth_user`) mit hinterlegter `tax_identification_number` erhalten BU 1
|
||||
- Gäste und Berater ohne USt-ID → reguläre Inlandsbesteuerung (19%/7%)
|
||||
- USt-ID wird über `ShoppingOrder → auth_user → account → tax_identification_number` ermittelt
|
||||
|
||||
4. **Provisions-Steuerschlüssel**
|
||||
|
||||
- 4760 Verkaufsprovision (Shop + Wachstumsbonus)
|
||||
- 4764 Payline-Provision
|
||||
- Normal (BU 9), Kleinunternehmer (BU 50), Reverse Charge (BU 94)
|
||||
|
||||
5. **Gegenkonto-Mapping**
|
||||
|
||||
- PayPal → 1230
|
||||
- Sonstige → Sammeldebitor (konfigurierbar)
|
||||
- Provisionen → Sammelkreditor
|
||||
|
||||
6. **Validierung mit klickbaren Warnungen**
|
||||
|
||||
- Strukturierte Warnungen/Fehler mit Metadaten (source_id, order_id, user_id)
|
||||
- Links zur Rechnung (Rechnungssuche) und zum Berater (Detailseite)
|
||||
- Prüfungen: Belegdatum, Belegnummer, BU-Schlüssel, EU ohne USt-ID, Reverse Charge ohne USt-ID
|
||||
|
||||
7. **DataTable mit Suchfunktion**
|
||||
|
||||
- Durchsuchbar nach Rechnungsnummer, Buchungstext, Konto, USt-ID, etc.
|
||||
- Server-seitige Verarbeitung via Yajra DataTables
|
||||
|
||||
8. **Admin-UI** (Route: `admin_payments_taxadvisor`)
|
||||
- Vorschau laden → Zusammenfassung + Validierung
|
||||
- Export generieren → CSV + DB-Protokoll
|
||||
- Download, Lock, Löschen
|
||||
|
||||
**Geänderte Dateien:**
|
||||
|
||||
- `app/Services/DatevExportService.php` – Kernlogik
|
||||
- `app/Http/Controllers/PaymentTaxAdvisorController.php` – Controller
|
||||
- `resources/views/admin/payment/taxadvisor.blade.php` – View
|
||||
- `config/datev.php` – Konfiguration
|
||||
- `tests/Unit/Services/DatevExportServiceTest.php` – 35 Tests
|
||||
|
||||
**Offene Punkte (Phase 2):**
|
||||
|
||||
- Debitoren/Kreditoren-Stammdaten (wenn Steuerberater OP-Listen braucht)
|
||||
- Payone-Debitor-Mapping (detaillierter als Sammeldebitor)
|
||||
- Automatischer Periodenabschluss / Sperr-Workflow
|
||||
|
||||
---
|
||||
|
||||
### [!] 10. Nicht zugeordnete Zahlungen/Punkte
|
||||
|
||||
**Priorität:** Hoch
|
||||
**Bereich:** Payment / Admin
|
||||
|
||||
**Problem:**
|
||||
Zahlungen ohne Zuordnung → Punkte verschwinden, keine Provision.
|
||||
|
||||
**Anforderung:**
|
||||
|
||||
- Admin-Hinweis bei nicht zugeordneten Zahlungen
|
||||
- Manuelle Zuordnungsmöglichkeit
|
||||
|
||||
**Technische Details:**
|
||||
| Parameter | Wert |
|
||||
|-----------|------|
|
||||
| Tabelle | `user_sales_volumes` |
|
||||
| Status-Feld | `status = 0` (not_assigned) |
|
||||
| Admin-View | Dashboard oder separate Sektion |
|
||||
|
||||
**Query für nicht zugeordnete Einträge:**
|
||||
|
||||
```php
|
||||
$unassigned = UserSalesVolume::where('status', 0)
|
||||
->with('user', 'order')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
```
|
||||
|
||||
**Umsetzung:**
|
||||
|
||||
1. **Dashboard-Alert:** Anzahl nicht zugeordneter Einträge anzeigen
|
||||
2. **Admin-Seite:** Liste aller nicht zugeordneten Einträge
|
||||
3. **Zuordnungs-Modal:**
|
||||
- User auswählen (Dropdown/Suche)
|
||||
- Status aktualisieren (1 = advisor_order, 2 = shoporder)
|
||||
- Punkte werden bei nächster Berechnung berücksichtigt
|
||||
|
||||
---
|
||||
|
||||
### [ ] 11. Monatsstatistik Erweiterungen
|
||||
|
||||
**Priorität:** Mittel
|
||||
**Bereich:** Dashboard / Team
|
||||
|
||||
**Probleme:**
|
||||
|
||||
- Teamumsatz wird seit Januar nicht angezeigt
|
||||
- Neupartner/Abos nicht klickbar (keine Detailansicht)
|
||||
|
||||
**Anforderungen:**
|
||||
| Feature | Beschreibung |
|
||||
|---------|--------------|
|
||||
| Teamumsatz | Bug fixen - wird nicht angezeigt |
|
||||
| Neupartner Details | Klick → Liste mit Name, E-Mail, Telefon, Generation, Mentor |
|
||||
| Team-Abos Details | Klick → Liste mit Abo-Details |
|
||||
| 1000-Punkte-Shops | Neue Kennzahl: Teampartner mit ≥1000 Punkte persönlichem Volumen |
|
||||
| Aktuelle Provision | In Monatsstatistik anzeigen |
|
||||
| Downline-Kontakte | Telefon, E-Mail, Adresse der eigenen Downline abrufbar (nicht nur VIPs) |
|
||||
|
||||
**Technische Details:**
|
||||
| Parameter | Wert |
|
||||
|-----------|------|
|
||||
| Service | `App\Services\LevelReportService` |
|
||||
| Controller | `App\Http\Controllers\User\TeamController` |
|
||||
| View | `resources/views/user/team/marketingplan.blade.php` |
|
||||
| Daten | `user_business` Tabelle |
|
||||
|
||||
**1000-Punkte Query:**
|
||||
291
dev/2026-02-06/steuerberater.md
Normal file
291
dev/2026-02-06/steuerberater.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
### Vom Steuerberater.
|
||||
|
||||
Umsatzerlöse
|
||||
|
||||
- 8400 Erlöse 19% -> Steuerschlüssel 9
|
||||
- 8300 Erlöse 7% -> Steuerschlüssel 8
|
||||
- 8120 Steuerfreie Umsätze nach § 4 Nr. 1a UStG (Drittland) -> Steuerschlüssel = 11
|
||||
- 8125 Steuerfreie innergemeinschaftliche Lieferungen nach § 4 Nr. 1b UStG (EU) -> Steuerschlüssel = 1
|
||||
|
||||
Provisionen (Unterscheidung in Shop, Payline und Wachstumsbonus)
|
||||
|
||||
- 4760 Verkaufsprovision (Shop + Wachstumsbonus)
|
||||
o 19% -> Steuerschlüssel = 9
|
||||
o Kleinunternehmer -> Steuerschlüssen = 50
|
||||
o Reverse Charge -> Steuerschlüssel = 94
|
||||
- 4764 (Payline)
|
||||
o 19% -> Steuerschlüssel = 9
|
||||
o Kleinunternehmer -> Steuerschlüssen = 50
|
||||
o Reverse Charge -> Steuerschlüssel = 94
|
||||
|
||||
Relevante Felder/Spalten
|
||||
|
||||
- Umsatz (Spalte A) -> Bruttobetrag je Steuerschlüssel
|
||||
- Soll/Haben (Spalte B)
|
||||
o Umsatz -> H
|
||||
o Provisionen -> S
|
||||
- Konto (Spalte G)
|
||||
o Umsätze -> siehe Umsatzerlöse
|
||||
o Provisionen -> siehe Gutschriften
|
||||
- Gegenkonto (Spalte H)
|
||||
o Umsatzerlöse = Debitorenkonto (vermutlich Sammelkonto)
|
||||
o Provisionen = Kreditorenkonto (vermutlich Sammelkonto)
|
||||
- Buchungsschlüssel
|
||||
o Steuerschlüssel siehe oben je Sachverhalt
|
||||
- Belegdatum (Spalte J) = Rechnungs-/Gutschriftsdatum
|
||||
- Belegfeld 1 (Spalte K)
|
||||
o Umsatz = Rechnungsnummer
|
||||
o Gutschrift = Gutschriftnummer
|
||||
- Buchungstext (Spalte N)
|
||||
o Umsatz = Name Kunde (Nachname Vorname)
|
||||
o Provisionen = Name Kunde (Nachname Vorname)
|
||||
- EU-Land u. UStID (Spalte AN)
|
||||
o USt-ID
|
||||
|
||||
Gegenkonto Umsatz
|
||||
|
||||
- Payone -> Debitor (Mapping erforderlich)
|
||||
- Paypal -> 1230
|
||||
- Kunden -> vermutlich Sammeldebitor
|
||||
|
||||
Gegenbuchung Provisionen
|
||||
|
||||
- Kreditor 71586
|
||||
- Vermutlich Sammelkreditor
|
||||
|
||||
Zu klären
|
||||
|
||||
- Kundennummer erforderlich?
|
||||
- Kreditorennummer erforderlich?
|
||||
|
||||
Erläuterungen
|
||||
|
||||
1. Header
|
||||
a. DATEV-Format - DATEV Developer Portal
|
||||
2. Buchungsstapel
|
||||
a. DATEV-Format - DATEV Developer Portal
|
||||
|
||||
### Erste Konzeption für ein DATEV-Export-Modul (EXTF „Buchungsstapel“) im Laravel-CRM – basierend auf dem Briefing des Steuerberaters und orientiert am DATEV Developer Portal (Header + Buchungsstapel). 
|
||||
|
||||
⸻
|
||||
|
||||
1. Zielbild: Was das Modul am Ende liefern muss
|
||||
|
||||
Output: pro Abrechnungsperiode (z. B. Monat) eine Datei EXTF_Buchungsstapel.csv mit 1. Header-Satz (erste Zeile, beginnt mit EXTF;700;21;...) 2. Spaltenkopfzeile (zweite Zeile) 3. Buchungszeilen (ab Zeile 3, je Buchung eine Zeile) 
|
||||
|
||||
Euer Beispiel/Template entspricht genau diesem Muster (Headerzeile + Spaltenkopfzeile + Daten).
|
||||
|
||||
⸻
|
||||
|
||||
2. Fachliche Logik (Mapping) – so entstehen Buchungszeilen
|
||||
|
||||
2.1 Buchungsarten (aus euren CRM-Daten ableitbar)
|
||||
|
||||
Ihr habt im System bereits: Bestellungen, Rechnungen, Gutschriften, Storno usw. Daraus leitet ihr je Datensatz eine „Buchungsart“ ab:
|
||||
• Umsatz (Rechnung / Belastung)
|
||||
• Soll/Haben: H
|
||||
• Konto: Erlöskonto (8400/8300/8120/8125)
|
||||
• Gegenkonto: Debitor/Zahlkonto (Payone/PayPal/Sammeldebitor)
|
||||
• Provision (Gutschrift / Abzug) (Shop, Payline, Wachstumsbonus)
|
||||
• Soll/Haben: S
|
||||
• Konto: 4760 (Shop+Wachstumsbonus) oder 4764 (Payline)
|
||||
• Gegenkonto: Kreditor (z. B. 71586 oder Sammelkreditor)
|
||||
|
||||
Wichtig: „Provision“ ist in eurem Export fachlich eine eigene Buchungslogik, auch wenn sie technisch über Gutschriften abgebildet ist.
|
||||
|
||||
2.2 Steuerschlüssel & Konten (1:1 aus dem Briefing)
|
||||
|
||||
Umsatzerlöse
|
||||
• 8400 Erlöse 19% → Steuerschlüssel 9
|
||||
• 8300 Erlöse 7% → Steuerschlüssel 8
|
||||
• 8120 Drittland steuerfrei §4 Nr. 1a → Steuerschlüssel 11
|
||||
• 8125 IG Lieferung steuerfrei §4 Nr. 1b → Steuerschlüssel 1
|
||||
|
||||
Provisionen
|
||||
• 4760 Verkaufsprovision (Shop + Wachstumsbonus)
|
||||
• 19% → 9
|
||||
• Kleinunternehmer → 50
|
||||
• Reverse Charge → 94
|
||||
• 4764 (Payline)
|
||||
• 19% → 9
|
||||
• Kleinunternehmer → 50
|
||||
• Reverse Charge → 94
|
||||
|
||||
2.3 Relevante DATEV-Spalten (aus Steuerberater-Vorgabe)
|
||||
|
||||
Ihr befüllt (mindestens) folgende Felder:
|
||||
• Spalte A Umsatz: Bruttobetrag je Steuerschlüssel
|
||||
• Spalte B Soll/Haben: Umsatz = H, Provisionen = S
|
||||
• Spalte G Konto: wie oben
|
||||
• Spalte H Gegenkonto: Debitor/Kreditor/Zahlkonto (Mapping)
|
||||
• Buchungsschlüssel (BU-Schlüssel): Steuerschlüssel wie oben
|
||||
• Belegdatum (Spalte J): Rechnungs-/Gutschriftsdatum
|
||||
• Belegfeld 1 (Spalte K): Rechnungsnummer / Gutschriftsnummer
|
||||
• Buchungstext (Spalte N): Name Kunde „Nachname Vorname“
|
||||
• EU-Land u. UStID (Spalte AN): USt-ID (wenn vorhanden)
|
||||
|
||||
Die restlichen DATEV-Spalten bleiben leer (aber müssen als Trennzeichen „mitlaufen“, weil das Format feste Spaltenpositionen hat). 
|
||||
|
||||
⸻
|
||||
|
||||
3. Gegenkonto-Logik (entscheidet über Debitor/Zahlkonto)
|
||||
|
||||
Aus dem Briefing:
|
||||
|
||||
Gegenkonto Umsatz
|
||||
• Payone → Debitor (Mapping erforderlich)
|
||||
• PayPal → 1230
|
||||
• Kunden → Sammeldebitor (vermutlich)
|
||||
|
||||
Gegenbuchung Provisionen
|
||||
• Kreditor 71586 (oder Sammelkreditor)
|
||||
|
||||
Empfehlung (damit es skalierbar bleibt)
|
||||
|
||||
Baut das als Regelwerk + Mappingtabellen, nicht hardcodiert.
|
||||
|
||||
(A) payment_method_counteraccounts
|
||||
• payment_method (payone, paypal, invoice, …)
|
||||
• counteraccount_type (DEBITOR, GL_ACCOUNT)
|
||||
• counteraccount_value (z. B. 1230)
|
||||
|
||||
(B) payone_debitor_mapping
|
||||
• Schlüssel z. B. payone_account_id / merchant_id / settlement_reference
|
||||
• Ziel: debitor_account_no
|
||||
|
||||
Fallback-Regeln 1. Wenn PayPal → 1230 2. Wenn Payone und Mapping gefunden → gemappter Debitor 3. Sonst → Sammeldebitor
|
||||
|
||||
⸻
|
||||
|
||||
4. „Zu klären“ (Kundennummer/Kreditorennummer) – pragmatische Konzeption ohne Blocker
|
||||
|
||||
Ihr könnt das so designen, dass beides optional ist, aber unterstützt wird:
|
||||
|
||||
Option 1: Sammelkonten (schnell, wenig Stammdatenpflege)
|
||||
• Gegenkonto Umsatz: Sammeldebitor
|
||||
• Gegenkonto Provision: Sammelkreditor/71586
|
||||
• Vorteil: einfacher Start
|
||||
• Nachteil: keine OP-Auswertung pro Kunde in DATEV
|
||||
|
||||
Option 2: echte Debitoren/Kreditoren (sauberer, mehr Pflege)
|
||||
• Jeder Kunde bekommt Debitorennummer (und ggf. Kreditorennummer, wenn Provisionen individuell laufen sollen)
|
||||
• Optional zusätzlich Export „Debitoren/Kreditoren“-Stammdaten in DATEV-Format (separater EXTF-Datensatztyp) 
|
||||
|
||||
Empfehlung für eure Roadmap
|
||||
• Phase 1: Sammelkonten + Payone-Mapping + PayPal 1230
|
||||
• Phase 2: Debitoren/Kreditoren optional (wenn Steuerberater OP-Listen braucht)
|
||||
|
||||
Ist geklärt. Option 1 Wird umgesetzt. Zusätzlich wird immer wie in Phase 1 noch das Payone-Mapping + PayPal 1230 etc angahangen.
|
||||
|
||||
⸻
|
||||
|
||||
5. Storno / Korrekturen / Gutschriften: robustes Regelwerk
|
||||
|
||||
Ihr wollt verhindern, dass „Storno“ doppelt wirkt oder als falsches Vorzeichen rausgeht.
|
||||
|
||||
Konzeptionell sauber:
|
||||
• Jede exportierte Zeile bekommt eine interne Export-Signatur (z. B. source_type, source_id, line_hash) und wird als „exportiert“ protokolliert.
|
||||
• Storno/Gutschrift wird als eigene Buchung exportiert (typischerweise Gegenbuchung zur Ursprungsbuchung), nicht als stilles Überschreiben.
|
||||
|
||||
DATEV-seitig gibt es Felder/Mechaniken für Umkehr/Korrektur; wie ihr das exakt nutzt (eigener Umkehrschlüssel vs. negativer Betrag) sollte ihr einmal mit dem Steuerberater festziehen, aber euer Modul kann beides abbilden:
|
||||
• Variante A: negativer Umsatzbetrag in Spalte A
|
||||
• Variante B: Umkehr-/Berichtigungsschlüssel befüllen (wenn gewünscht)
|
||||
|
||||
(Die DATEV-Doku zum Buchungsstapel beschreibt die Struktur/Spalten; die konkrete Kanzlei-Konvention ist oft Mandantenstandard.) 
|
||||
|
||||
⸻
|
||||
|
||||
6. Datenmodell in Laravel (minimal, aber vollständig)
|
||||
|
||||
6.1 Kern-Tabellen
|
||||
|
||||
datev_exports
|
||||
• id, period_from, period_to, created_by
|
||||
• status (draft/generated/downloaded/locked)
|
||||
• berater_nr, mandant_nr (für Header)
|
||||
• filename, file_path, hash
|
||||
|
||||
datev_export_lines
|
||||
• export_id
|
||||
• source_type (invoice/credit_note/cancellation/commission)
|
||||
• source_id
|
||||
• amount_gross
|
||||
• soll_haben (H/S)
|
||||
• konto
|
||||
• gegenkonto
|
||||
• bu_schluessel
|
||||
• belegdatum
|
||||
• belegfeld1
|
||||
• buchungstext
|
||||
• ustid, eu_land
|
||||
• row_csv (optional: final gerenderte Zeile zur Nachvollziehbarkeit)
|
||||
|
||||
accounting_rules (konfigurierbar)
|
||||
• für Umsatz: VAT-Case → (konto, bu)
|
||||
• für Provision: scenario → (konto, bu)
|
||||
|
||||
counteraccount_mappings (Payone/PayPal/Default)
|
||||
|
||||
6.2 Warum „Export Lines“ speichern?
|
||||
• Audit-Trail (Steuerberaterfragen)
|
||||
• Re-Download exakt identisch
|
||||
• Sperre gegen Doppel-Export derselben Quellen
|
||||
|
||||
⸻
|
||||
|
||||
7. UI/Workflow (CRM-Modul) 1. Periode wählen (Monat/Zeitraum) 2. Datenbasis anzeigen: Anzahl Rechnungen, Gutschriften, Stornos, Provisionen 3. Regel-Preview: gruppiert nach (Konto, Gegenkonto, Steuerschlüssel) + Summen 4. Validierungscheck (Blocking):
|
||||
• fehlender Steuercase (z. B. EU ohne USt-ID aber als IG markiert)
|
||||
• fehlendes Payone-Debitor-Mapping (falls Pflicht)
|
||||
• fehlendes Belegdatum/Belegnr. 5. Export generieren → Datei + Protokoll 6. Lock (optional) → „Dieses Periodenset ist final“
|
||||
|
||||
⸻
|
||||
|
||||
8. CSV/Format-Generator (technische Konzeption, ohne Code)
|
||||
• Semikolon als Delimiter
|
||||
• Headerzeile gemäß DATEV-Vorgaben (Formatkategorie 21 = Buchungsstapel, Versionen etc.) 
|
||||
• Danach Spaltenkopfzeile (fixe Reihenfolge)
|
||||
• Dann Zeilen mit exakt derselben Spaltenanzahl (leere Felder als leer zwischen ;)
|
||||
|
||||
Praxis-Hinweis: Haltet Header-Felder (Berater-Nr, Mandant-Nr, Zeitraum) als Kanzlei-Konfiguration im CRM, damit ihr nicht pro Export „handwerkelt“. (Gerade diese Angaben sind beim Import relevant.) 
|
||||
|
||||
⸻
|
||||
|
||||
9. Konkrete Regel-Engine (so entscheidet ihr automatisch)
|
||||
|
||||
Umsatz-Fallunterscheidung
|
||||
|
||||
Input: invoice.vat_rate, invoice.destination_country, invoice.customer_vat_id, invoice.tax_exemption_reason
|
||||
|
||||
Mapping:
|
||||
• vat_rate == 19% → (8400, bu=9)
|
||||
• vat_rate == 7% → (8300, bu=8)
|
||||
• Drittland steuerfrei → (8120, bu=11)
|
||||
• EU IG Lieferung steuerfrei → (8125, bu=1)
|
||||
|
||||
Provision-Fallunterscheidung
|
||||
|
||||
Input: commission.type (shop/payline/bonus), supplier_tax_status (normal/KU/RC)
|
||||
• type shop|bonus → konto 4760
|
||||
• type payline → konto 4764
|
||||
• tax_status normal → bu 9
|
||||
• KU → bu 50
|
||||
• RC → bu 94
|
||||
|
||||
⸻
|
||||
|
||||
10. Was ich aus eurer Beispieldatei ableite (für die Konzeption wichtig)
|
||||
|
||||
Eure Datei nutzt exakt das typische Muster:
|
||||
• Zeile 1: EXTF;700;21;Buchungsstapel;...
|
||||
• Zeile 2: Spaltenüberschriften (u. a. „Umsatz (ohne Soll/Haben-Kz)“, „Soll/Haben-Kennzeichen“, „Konto“, „Gegenkonto“, „BU-Schlüssel“, „Belegdatum“, …)
|
||||
• Danach Datenzeilen mit vielen leeren Feldern (DATEV-typisch bei Minimalbefüllung)
|
||||
|
||||
Das bestätigt: Ihr braucht vor allem stabile Spaltenpositionen + korrektes Mapping, nicht „alle 116 Felder“. 
|
||||
|
||||
⸻
|
||||
|
||||
Wenn du willst, kann ich als nächsten Schritt (ohne extra Rückfragen) aus eurem Briefing ein konkretes Mapping-Dokument erstellen (als technische Spezifikation):
|
||||
• Feld-zu-Quelle (CRM) je DATEV-Spalte
|
||||
• Regelprioritäten (Fallbacks)
|
||||
• Validierungsregeln + Fehlermeldungen (für UI)
|
||||
• Testfälle (z. B. EU ohne USt-ID, Storno nach Periodenwechsel, Payone ohne Mapping, KU/RC-Provisionen)
|
||||
15
dev/2026-02-19/Tasks.md
Normal file
15
dev/2026-02-19/Tasks.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
Hier beschreibe ich eine weitere Aufgabe, die umgesetzt werden muss.
|
||||
|
||||
In dem Salecenter Gibt es einen Wizzard, der für die Registrierung für Berater, Notwendig ist.
|
||||
Jeder Berater kommt über einen Link und gelangt zu einem Registrierungsnummer Art, wo er unterschiedliche Schritte Akzeptierung, der AGBs persönliche Daten hochladen von Personaldaten zu Verifizierung in den ersten Schritten umsetzen muss. Sind seine Daten vollständig angegeben sendet er über einen Button, einen request, der eine E-Mail für den Admin zur Freigabe auslöst. Die Freigabe ist beim der User Bearbeitung mit Extrafelder für den Admin versehen. Ist alles in Ordnung kann der Admin den Berater freischalten und der Berater bekommt eine E-Mail, dass er freigeschaltet ist. Nach der Freischaltung kommt er in den allerletzten Schritt des OSA des Registrierungs Wizzard, Wo er die Wahl hat, eine Bestellung auszuführen. Um diese Bestellung geht es jetzt. Diese muss angepasst werden. Unter den Produkten gibt es Anlagen, die genau für diesen Wizard gemacht sind. Hier gibt es eine Spalte Show on wo hinterlegt wird, wo das Produkt angezeigt wird. Es gibt dort die Einträge Registrierung, Berater oder on Boarding Berater dann werden die Produkte in genau diesem Prozess angezeigt und sind auswählbar.
|
||||
|
||||
Bisher ist es so, dass ein Versand Preis berechnet wird, welches mit einem Kompensation Produkt ein wenig ausgeglichen wird. Diese fällt in Zukunft weg d.h. es gibt einmal ein kleines Standardpaket das ist nur die Mitgliedschaft. Diese hat auch keine Versandkosten da sie auch ein Gewicht von null hat alles, was null hat, bekommt keine Versandkosten das ist das Paket für die reine Mitgliedschaft produkt ID 35. Handelt sich um ein Jahres Mitgliedschaft, welches dann später entsprechend kommuniziert wird.
|
||||
|
||||
zusätzlich gibt es Starter Pakete. Diese Pakete beinhalten ein umfangreiches Set an Produkten, Rabattiert und zusätzlich kann man schon mit einem Paket in den nächsten Rang aufsteigen.
|
||||
Aktuell ist es so, dass man die Jahres Mitgliedschaft buchen muss und ein Starterpaket optional buchen kann. Beim Starterpaket werden dann auch Versandkosten erhoben und genau hier kommt jetzt die Änderung. Es wird nur noch eine direkte Auswahl geben. Zwischen der reinen Mitgliedschaft und 2,3 Starterpaket und in dem Starterpaket Preis ist gleichzeitig ein Jahres Mitgliedschaft enthalten und auch die Versandkosten d.h. die Versandkosten müssen immer null sein das ist eine Ausnahme für die Berater, die sonst immer Versandkosten zahlen mit einem Kombination Produkt.
|
||||
|
||||
Jetzt gilt es, diese Änderungen so einzubauen, dass wir es einmal über die Produkte entsprechend schalten können und das auch am Ende der Wizard nur die Auswahl zwischen den Produkten erlaubt. Es soll noch etwas weiteres hinzu wird nur dass die reine Mitgliedschaft genommen soll, wenn der Button gedrückt wird vorerst eine Meldung kommen in einem moralen Fenster, in dem so etwas drin steht wie Achtung mit einem Starterpaket erhältst du ein Rabatt von XY und diese Starterpaket ist nur einmalig bei der Bestellung Registrierung möglich zu kaufen möchtest du jetzt wirklich ohne ein Starterpaket nur die reine Mitgliedschaft abschließen? Dieser Text mit in die sprach dateien resources/lang
|
||||
|
||||
Controller Wizzard -> app/Http/Controllers/WizardController.php
|
||||
Controller Produkte -> app/Http/Controllers/ProductController.php
|
||||
Controller Berater / Leads für die Admin Freigabe app/Http/Controllers/LeadController.php function released
|
||||
204
dev/2026-02-19/plan-wizard-starterpaket.md
Normal file
204
dev/2026-02-19/plan-wizard-starterpaket.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# Plan: Wizard Registrierung - Starterpaket-Auswahl umbauen
|
||||
|
||||
## Ausgangslage
|
||||
|
||||
Der letzte Schritt des Berater-Registrierungs-Wizards hat aktuell:
|
||||
|
||||
- **Mitgliedschaften** als Radio-Buttons (show_on 7+8)
|
||||
- **Onboarding-Produkte** als optionale Checkboxen (show_on 9)
|
||||
- Kompensationsprodukt für Versandkosten
|
||||
|
||||
## Was sich ändert
|
||||
|
||||
1. **Eine einzige Radio-Button-Auswahl** zwischen:
|
||||
|
||||
- Reine Jahresmitgliedschaft (Produkt-ID 35, Gewicht 0 = keine Versandkosten)
|
||||
- 2-3 Starterpakete (inkl. Mitgliedschaft + Versandkosten = 0)
|
||||
|
||||
2. **Neues DB-Feld `free_shipping_consultant`** auf der Produkttabelle
|
||||
|
||||
- Boolean-Checkbox im Admin: "Versandkostenfrei für Berater"
|
||||
- Wird in der Versandkostenberechnung (Yard) berücksichtigt
|
||||
|
||||
3. **Warn-Modal** wenn nur Mitgliedschaft gewählt wird:
|
||||
|
||||
- Hinweis auf Rabatt und Einmaligkeit des Starterpaket-Angebots
|
||||
|
||||
4. **Onboarding-Checkboxen entfallen** - alles über Radio-Buttons
|
||||
|
||||
## Betroffene Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
| ------------------------------------------------ | ------------------------------------- |
|
||||
| `database/migrations/...` | Neues Feld `free_shipping_consultant` |
|
||||
| `app/Models/Product.php` | fillable + casts |
|
||||
| `app/Repositories/ProductRepository.php` | Checkbox-Handling |
|
||||
| `resources/views/admin/product/form.blade.php` | Admin-Checkbox |
|
||||
| `app/Services/Yard.php` | Versandkosten-Logik |
|
||||
| `app/Http/Controllers/WizardController.php` | Query + storePayment |
|
||||
| `resources/views/user/wizard/_payment.blade.php` | UI-Umbau + Modal |
|
||||
| `resources/lang/{de,en,es}/register.php` | Übersetzungen |
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungsstatus (Stand: 2026-02-19)
|
||||
|
||||
### ERLEDIGT - Alle geplanten Änderungen sind umgesetzt
|
||||
|
||||
#### 1. Migration `free_shipping_consultant` - ERLEDIGT
|
||||
|
||||
- **Datei:** `database/migrations/2026_02_19_120000_add_free_shipping_consultant_to_products_table.php`
|
||||
- Boolean-Feld `free_shipping_consultant` mit `default(false)` nach `no_free_shipping`
|
||||
- Down-Migration entfernt die Spalte korrekt
|
||||
|
||||
#### 2. Product Model - ERLEDIGT
|
||||
|
||||
- **Datei:** `app/Models/Product.php`
|
||||
- `free_shipping_consultant` in `$casts` als `'bool'` hinzugefügt (Zeile 192)
|
||||
- `free_shipping_consultant` in `$fillable` hinzugefügt (Zeile 216)
|
||||
|
||||
#### 3. ProductRepository Checkbox-Handling - ERLEDIGT
|
||||
|
||||
- **Datei:** `app/Repositories/ProductRepository.php`
|
||||
- `$data['free_shipping_consultant'] = isset($data['free_shipping_consultant']) ? 1 : 0;` in der `update()`-Methode (Zeile 30)
|
||||
|
||||
#### 4. Admin Produkt-Formular - ERLEDIGT
|
||||
|
||||
- **Datei:** `resources/views/admin/product/form.blade.php`
|
||||
- Neue Checkbox-Sektion "Versandkostenfrei für Berater (FcB.)" eingebaut (Zeilen 124-132)
|
||||
- Text: "Versandkosten für Berater sind bei diesem Produkt immer 0 (z.B. Starterpakete)."
|
||||
|
||||
#### 5. Yard.php Versandkosten-Logik - ERLEDIGT
|
||||
|
||||
- **Datei:** `app/Services/Yard.php`
|
||||
- Neue Methode `allItemsFreeShippingConsultant()` hinzugefügt (Zeilen 419-429)
|
||||
- Prüft ob **alle** Items im Warenkorb `free_shipping_consultant = true` haben
|
||||
- Gibt `false` zurück wenn der Warenkorb leer ist
|
||||
- In `calculateShippingPrice()` (Zeile 260): Bedingung erweitert:
|
||||
```php
|
||||
if ($this->allItemsFreeShippingConsultant() || $this->weight() == 0) {
|
||||
```
|
||||
- Wenn alle Items `free_shipping_consultant` haben ODER Gewicht = 0 -> Versandkosten = 0
|
||||
|
||||
#### 6. WizardController - ERLEDIGT
|
||||
|
||||
- **Datei:** `app/Http/Controllers/WizardController.php`
|
||||
- In `storePayment()` (Zeile 607): `free_shipping_consultant` wird beim Hinzufügen zum Warenkorb als Cart-Option übergeben:
|
||||
```php
|
||||
'free_shipping_consultant' => $product->free_shipping_consultant
|
||||
```
|
||||
- Die Product-Queries in `create()`, `register()`, `payment()` laden weiterhin show_on 7, 8, 9 - das ist korrekt, da die Produkte über show_on gesteuert werden
|
||||
|
||||
#### 7. Wizard Payment Blade (UI-Umbau) - ERLEDIGT
|
||||
|
||||
- **Datei:** `resources/views/user/wizard/_payment.blade.php`
|
||||
- **Einheitliche Radio-Button-Auswahl:** Alle Produkte werden als Radio-Buttons (`switchers-package-wizard`) dargestellt - keine separaten Checkboxen mehr für Onboarding-Produkte
|
||||
- **Warn-Modal** implementiert (Zeilen 78-96):
|
||||
- Bootstrap Modal `#starterWarningModal` mit Titel, Text, Zurück-Button und Bestätigen-Button
|
||||
- Übersetzungsschlüssel: `register.starter_warning_title`, `register.starter_warning_text`, `register.starter_warning_back`, `register.starter_warning_confirm`
|
||||
- **JavaScript-Logik** (Zeilen 98-129):
|
||||
- `data-is-membership-only="1"` auf Produkt-ID 35 (reine Mitgliedschaft)
|
||||
- Submit-Button (`#btn-wizard-submit`) prüft ob reine Mitgliedschaft gewählt ist
|
||||
- Falls ja: Modal wird angezeigt statt direkt abzusenden
|
||||
- Bestätigen-Button im Modal (`#btn-confirm-membership-only`) schließt Modal und submittet das Formular
|
||||
- Preisberechnung über `data-price` und `calculate_package_payment()` funktioniert
|
||||
- **Standard-Auswahl:** Der 2. Eintrag (erstes Starterpaket) ist per Default vorgewählt (`@if($counter == 2) checked @endif`)
|
||||
|
||||
#### 8. Sprachdateien - ERLEDIGT
|
||||
|
||||
- **DE** (`resources/lang/de/register.php`):
|
||||
- `starter_warning_title` => 'Achtung'
|
||||
- `starter_warning_text` => 'Mit einem Starterpaket erhältst Du einen Rabatt und kannst direkt in den nächsten Rang aufsteigen. Dieses Angebot ist nur einmalig bei der Registrierung verfügbar. Möchtest Du wirklich ohne Starterpaket nur die reine Mitgliedschaft abschließen?'
|
||||
- `starter_warning_back` => 'Zurück zur Auswahl'
|
||||
- `starter_warning_confirm` => 'Ja, nur Mitgliedschaft'
|
||||
- **EN** (`resources/lang/en/register.php`):
|
||||
- `starter_warning_title` => 'Attention'
|
||||
- `starter_warning_text` => 'With a starter package you receive a discount and can advance directly to the next rank. This offer is only available once during registration. Do you really want to proceed with only the membership without a starter package?'
|
||||
- `starter_warning_back` => 'Back to selection'
|
||||
- `starter_warning_confirm` => 'Yes, membership only'
|
||||
- **ES** (`resources/lang/es/register.php`):
|
||||
- `starter_warning_title` => 'Atención'
|
||||
- `starter_warning_text` => 'Con un paquete de inicio recibes un descuento y puedes ascender directamente al siguiente rango. Esta oferta solo está disponible una vez durante el registro. ¿Realmente deseas continuar solo con la membresía sin un paquete de inicio?'
|
||||
- `starter_warning_back` => 'Volver a la selección'
|
||||
- `starter_warning_confirm` => 'Sí, solo membresía'
|
||||
|
||||
---
|
||||
|
||||
## Nachbesserung 1: Modal-Styling + dynamische Mitgliedschaft-Erkennung (2026-02-19)
|
||||
|
||||
### Problem
|
||||
|
||||
- Modal war optisch unauffällig (Standard-Bootstrap, keine Warn-Farben)
|
||||
- Produkt-ID 35 war im Blade hardcoded (`$product->id == 35`)
|
||||
|
||||
### Lösung
|
||||
|
||||
#### 9. Neues DB-Feld `is_membership_only` - ERLEDIGT
|
||||
|
||||
- **Migration:** `database/migrations/2026_02_19_140000_add_is_membership_only_to_products_table.php`
|
||||
- Boolean `is_membership_only`, default `false`, nach `free_shipping_consultant`
|
||||
- **Product Model:** `$casts` + `$fillable` ergänzt
|
||||
- **ProductRepository:** Checkbox-Handling in `update()` ergänzt
|
||||
- **Admin-Formular:** Neue Checkbox "Reine Mitgliedschaft (MoP.)" mit Text:
|
||||
"Dieses Produkt ist eine reine Mitgliedschaft ohne Starterpaket (Warnung im Wizard)."
|
||||
|
||||
#### 10. Hardcoded ID entfernt - ERLEDIGT
|
||||
|
||||
- **Blade** `_payment.blade.php`: `$product->id == 35` ersetzt durch `$product->is_membership_only`
|
||||
- Jetzt dynamisch: Jedes Produkt mit aktivierter Checkbox löst die Warnung aus
|
||||
|
||||
#### 11. Modal-Styling aufgewertet - ERLEDIGT
|
||||
|
||||
- **Header:** `bg-warning text-dark` mit `fa-exclamation-triangle` Icon
|
||||
- **Border:** `border-warning` am Modal-Content
|
||||
- **Body:** Inhalt in `alert alert-warning` Box mit `fa-info-circle` Icon
|
||||
- **Footer:** "Zurück"-Button als `btn-warning` (auffällig), "Bestätigen" als `btn-outline-secondary` (dezent)
|
||||
|
||||
---
|
||||
|
||||
## Nachbesserung 2: Info-Box + Validierung ohne Vorauswahl (2026-02-19)
|
||||
|
||||
### Problem
|
||||
|
||||
- Ein Produkt war per Default vorausgewählt (2. Eintrag) - Nutzer hat das entfernt
|
||||
- Kein Hinweistext, der erklärt was zu tun ist
|
||||
- Keine Fehlermeldung wenn Submit ohne Auswahl geklickt wird
|
||||
|
||||
### Lösung
|
||||
|
||||
#### 12. Info-Alert oberhalb der Produkttabelle - ERLEDIGT
|
||||
|
||||
- **Blade** `_payment.blade.php`: `alert alert-info` Box mit `fa-info-circle` Icon
|
||||
- Text erklärt die Wahlmöglichkeiten (Mitgliedschaft vs. Starterpaket mit Vorteilen)
|
||||
- Übersetzungsschlüssel: `register.wizard_package_info`
|
||||
|
||||
#### 13. Validierung bei fehlender Auswahl - ERLEDIGT
|
||||
|
||||
- **Fehlermeldung:** Verstecktes `alert alert-danger` (`#wizard-selection-error`) unter der Tabelle
|
||||
- **JS-Logik:** Submit prüft ob ein Radio-Button gewählt ist:
|
||||
- Falls nein: Fehlermeldung einblenden + roter Rand um Tabelle + Scroll zum Fehler
|
||||
- Fehlermeldung verschwindet sobald ein Produkt gewählt wird
|
||||
- Übersetzungsschlüssel: `register.wizard_no_selection`
|
||||
|
||||
#### 14. Keine Vorauswahl mehr - ERLEDIGT
|
||||
|
||||
- `@if($counter == 2) checked @endif` wurde vom Nutzer entfernt
|
||||
- Kein Produkt ist vorausgewählt, Nutzer muss aktiv wählen
|
||||
|
||||
#### 15. Sprachdateien ergänzt - ERLEDIGT
|
||||
|
||||
- **DE:** `wizard_package_info` + `wizard_no_selection`
|
||||
- **EN:** `wizard_package_info` + `wizard_no_selection`
|
||||
- **ES:** `wizard_package_info` + `wizard_no_selection`
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte / Nächste Schritte
|
||||
|
||||
### Testen
|
||||
|
||||
- Wizard ohne Auswahl: Fehlermeldung muss rot erscheinen + Tabelle rot umrandet
|
||||
- Wizard mit Auswahl: Fehlermeldung verschwindet, Submit funktioniert
|
||||
- Wizard mit reiner Mitgliedschaft: Warning-Modal erscheint
|
||||
- Wizard mit Starterpaket: Direkt zum Checkout
|
||||
- Info-Box in allen Sprachen prüfen (de, en, es)
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue