Abo: Bestellung extern als erledigt markieren (ohne Rechnung/Inkasso)

Neue Admin-Funktion fuer angehaltene Abo-Bestellungen, die der Kunde
ausserhalb des Systems geliefert und verrechnet hat.

Aktion (AboExternalCompletionService):
- Punkte gutschreiben wie bei eingegangener Zahlung (SalesPointsVolume,
  idempotent gegen Doppelbuchung)
- UserAboOrder auf neuen gruenen Status 8 "extern erledigt"; paid bleibt false
- Bestellung: shipped=2 (Lieferung bestaetigt), keine Rechnung/kein Lieferschein
- Blockade aufheben: Abo zurueck auf status=2 (okay), aktiv, next_date neu,
  damit der naechste Termin regulaer laeuft
- wie bei regulaerer Ausfuehrung: IncentiveTracker + AboOneTimeService-Purge

Einstiegspunkte: Button + Bestaetigungs-Modal auf der Kunden-Bestelldetailseite
(SalesController) und auf der Abo-Detailseite (Admin\AboController@externDone,
Route admin_abos_extern_done). Status 8 (gruen) in UserAbo/UserAboOrder, Labels
de/en.

Tests: tests/Feature/AboExternalCompletionTest (Happy Path, Idempotenz,
bezahlt-/Nicht-Abo-Guards, Abo-Detail-Einstieg) - 5 gruen.

Zusaetzlich: docs/2026-06-11-test-suite-vorbestehende-fehler.md - Diagnose der
93 vorbestehenden (von master geerbten) Test-Suite-Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-06-11 15:40:16 +00:00
parent 8593dbe65d
commit 090debf1b4
13 changed files with 609 additions and 59 deletions

View file

@ -8,6 +8,7 @@ use App\Models\ShoppingPayment;
use App\Models\ShoppingUser;
use App\Models\UserShop;
use App\Repositories\InvoiceRepository;
use App\Services\AboExternalCompletionService;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\CustomerPriority;
use App\Services\Payment;
@ -186,10 +187,17 @@ class SalesController extends Controller
return view('admin.sales.customer_detail', $data);
}
public function customersStore($id)
public function customersStore($id, AboExternalCompletionService $aboExternalCompletion)
{
$data = Request::all();
$change_member_error = false;
if (($data['action'] ?? null) === 'shopping-order-abo-extern-done') {
$shopping_order = ShoppingOrder::findOrFail($data['id']);
$result = $aboExternalCompletion->complete($shopping_order);
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
return redirect(route('admin_sales_customers_detail', [$shopping_order->id]));
}
if ($data['action'] === 'shopping-order-change-member') {
if (! isset($data['change_member_key']) || $data['change_member_key'] !== config('mivita.edit_data_pass')) {
$change_member_error = 'Das Passwort ist falsch.';