diff --git a/app/Livewire/Admin/PaymentProblems.php b/app/Livewire/Admin/PaymentProblems.php index fa33ab3..72118ca 100644 --- a/app/Livewire/Admin/PaymentProblems.php +++ b/app/Livewire/Admin/PaymentProblems.php @@ -8,6 +8,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; use Livewire\Attributes\Layout; +use Livewire\Attributes\On; use Livewire\Component; /** @@ -32,9 +33,6 @@ use Livewire\Component; #[Layout('layouts.admin')] class PaymentProblems extends Component { - /** @var array FailedCheckout-ID => Begründung */ - public array $note = []; - public function mount(): void { abort_unless(Gate::allows('billing.manage'), 403); @@ -47,13 +45,14 @@ class PaymentProblems extends Component * nächsten Nachfragen wertlos — überwiesen? neu bestellt? nicht erreichbar? * Wer es war, steht mit dabei. */ - public function resolve(int $id): void + #[On('failed-checkout-resolved')] + public function onResolved(int $id, string $note = ''): void { $this->authorize('billing.manage'); $problem = FailedCheckout::query()->findOr($id, fn () => abort(404)); - $begruendung = trim($this->note[$id] ?? ''); + $begruendung = trim($note); $wer = Auth::guard('operator')->user()?->email ?? 'console'; $problem->update([ @@ -61,7 +60,6 @@ class PaymentProblems extends Component 'note' => $begruendung !== '' ? $begruendung.' — '.$wer : $wer, ]); - unset($this->note[$id]); $this->dispatch('notify', message: __('payment_problems.resolved')); } diff --git a/app/Livewire/Admin/ResolveFailedCheckout.php b/app/Livewire/Admin/ResolveFailedCheckout.php new file mode 100644 index 0000000..69d3d5a --- /dev/null +++ b/app/Livewire/Admin/ResolveFailedCheckout.php @@ -0,0 +1,51 @@ +` prüft und die Zeilen `
  • ` waren. + * + * Mutiert nichts selbst (R23): der Bestätigen-Knopf löst ein Ereignis aus, das + * Admin\PaymentProblems auffängt — damit bleibt die Berechtigungsprüfung an der + * einen Stelle, an der sie schon stand, statt hier verdoppelt zu werden. Der + * Aufruf hier prüft trotzdem: ein Modal ist ohne die Route-Middleware der Seite + * erreichbar. + */ +class ResolveFailedCheckout extends ModalComponent +{ + public int $id; + + public string $note = ''; + + public string $label = ''; + + public function mount(int $id): void + { + $this->authorize('billing.manage'); + + $problem = FailedCheckout::query()->findOr($id, fn () => abort(404)); + + $this->id = $problem->id; + $this->label = $problem->name ?: $problem->email; + } + + public function confirm(): void + { + $this->authorize('billing.manage'); + + $this->dispatch('failed-checkout-resolved', id: $this->id, note: $this->note); + $this->closeModal(); + } + + public function render() + { + return view('livewire.admin.resolve-failed-checkout'); + } +} diff --git a/lang/de/payment_problems.php b/lang/de/payment_problems.php index c16b2f7..a09dbe1 100644 --- a/lang/de/payment_problems.php +++ b/lang/de/payment_problems.php @@ -21,4 +21,7 @@ return [ 'note' => 'Begründung', 'resolve' => 'Erledigt', 'resolved' => 'Als erledigt vermerkt.', + 'resolve_title' => 'Als erledigt vermerken?', + 'resolve_body' => 'Der Vorgang verschwindet aus dieser Liste. Was Sie hier eintragen, steht später daneben — zusammen mit Ihrer Adresse.', + 'note_hint' => 'Zum Beispiel: hat überwiesen, hat neu bestellt, nicht erreichbar.', ]; diff --git a/lang/en/payment_problems.php b/lang/en/payment_problems.php index 1c5b036..be806e9 100644 --- a/lang/en/payment_problems.php +++ b/lang/en/payment_problems.php @@ -21,4 +21,7 @@ return [ 'note' => 'Reason', 'resolve' => 'Done', 'resolved' => 'Marked as dealt with.', + 'resolve_title' => 'Mark as dealt with?', + 'resolve_body' => 'It leaves this list. What you write here stays with it afterwards, together with your address.', + 'note_hint' => 'For example: paid by transfer, ordered again, unreachable.', ]; diff --git a/resources/views/livewire/admin/payment-problems.blade.php b/resources/views/livewire/admin/payment-problems.blade.php index 0ee445d..dca419f 100644 --- a/resources/views/livewire/admin/payment-problems.blade.php +++ b/resources/views/livewire/admin/payment-problems.blade.php @@ -71,17 +71,14 @@ {{ $problem->failed_at->local()->isoFormat('D. MMM YYYY, HH:mm') }}

    - - {{-- Begründung neben dem Knopf, nicht danach: „erledigt" - allein ist beim nächsten Nachfragen wertlos. --}} -
    -
    - -
    - + {{-- R20: die Begründung wird im Modal eingetippt, nicht + in der Zeile. Ein Feld hier liesse die Zeile + wachsen und die Nachbarn springen — und ein + Knopf, der eine Methode am Seiten-Bauteil ruft, + ist der Inline-Editor unter anderem Namen. --}} + {{ __('payment_problems.resolve') }}
    diff --git a/resources/views/livewire/admin/resolve-failed-checkout.blade.php b/resources/views/livewire/admin/resolve-failed-checkout.blade.php new file mode 100644 index 0000000..bf257f8 --- /dev/null +++ b/resources/views/livewire/admin/resolve-failed-checkout.blade.php @@ -0,0 +1,23 @@ +{{-- R24: ein Modal mit Eingabefeld benutzt — Kopf und Fuss + bleiben stehen, gescrollt wird nur die Mitte. --}} + +

    {{ __('payment_problems.resolve_body') }}

    + +
    + + + + + + {{ __('common.cancel') }} + + {{-- type=submit + form=… ist der Preis für einen Fuss, der nicht + wegscrollt (R24) — ein HTML-Attribut, kein Trick. --}} + + {{ __('payment_problems.resolve') }} + + +
    diff --git a/tests/Feature/Billing/PaymentProblemsTest.php b/tests/Feature/Billing/PaymentProblemsTest.php index f3daede..b1b4187 100644 --- a/tests/Feature/Billing/PaymentProblemsTest.php +++ b/tests/Feature/Billing/PaymentProblemsTest.php @@ -93,10 +93,12 @@ it('lets somebody mark a bounced payment as dealt with', function () { 'failed_at' => Carbon::now(), ]); + // Seit R20 geht die Begründung durch das Modal (siehe ResolveInModalTest): + // die Seite fängt nur noch das Ereignis auf. Ein Feld in der Zeile wäre + // genau der Inline-Editor, den die Regel verbietet. Livewire::actingAs(operator('Owner'), 'operator') ->test(PaymentProblems::class) - ->set("note.{$problem->id}", 'Kunde hat überwiesen') - ->call('resolve', $problem->id) + ->call('onResolved', $problem->id, 'Kunde hat überwiesen') ->assertHasNoErrors(); $problem->refresh(); diff --git a/tests/Feature/Billing/ResolveInModalTest.php b/tests/Feature/Billing/ResolveInModalTest.php new file mode 100644 index 0000000..93c2b20 --- /dev/null +++ b/tests/Feature/Billing/ResolveInModalTest.php @@ -0,0 +1,90 @@ +` prüft und meine + * Zeilen `
  • ` waren. Der Buchstabe der Prüfung, nicht ihr Sinn. Dieser Test + * schliesst die Lücke für diese Seite; die allgemeine Fassung wäre ein eigener + * Punkt (siehe unten). + */ +function bouncedPayment(): FailedCheckout +{ + return FailedCheckout::query()->create([ + 'stripe_session_id' => 'cs_geplatzt', 'email' => 'neu@example.test', + 'name' => 'Neu GmbH', 'plan' => 'start', 'amount_cents' => 4900, + 'currency' => 'EUR', 'failed_at' => Carbon::now(), + ]); +} + +it('carries no input field on the page itself', function () { + // Die Zeile bleibt eine Zeile. Ein Feld darin lässt sie wachsen, die + // Nachbarzeilen springen, und eine halb im Bearbeitungsmodus stehende + // Liste liest sich wie ein Darstellungsfehler. + bouncedPayment(); + + $html = Livewire::actingAs(operator('Owner'), 'operator') + ->test(PaymentProblems::class) + ->html(); + + expect($html)->not->toContain('wire:model="note.'); +}); + +it('opens a modal instead', function () { + $problem = bouncedPayment(); + + $html = Livewire::actingAs(operator('Owner'), 'operator') + ->test(PaymentProblems::class) + ->html(); + + expect($html)->toContain('admin.resolve-failed-checkout') + ->and($html)->toContain((string) $problem->id); +}); + +it('lets the modal hand the reason back to the page', function () { + // R23: das Modal mutiert nichts selbst. Es löst ein Ereignis aus, und die + // Seiten-Komponente behält ihre eigene Berechtigungsprüfung. + $problem = bouncedPayment(); + + Livewire::actingAs(operator('Owner'), 'operator') + ->test(ResolveFailedCheckout::class, ['id' => $problem->id]) + ->set('note', 'Kunde hat überwiesen') + ->call('confirm') + ->assertDispatched('failed-checkout-resolved'); +}); + +it('records reason and person when the page catches that event', function () { + $problem = bouncedPayment(); + + Livewire::actingAs(operator('Owner'), 'operator') + ->test(PaymentProblems::class) + ->call('onResolved', $problem->id, 'Kunde hat überwiesen') + ->assertHasNoErrors(); + + $problem->refresh(); + + expect($problem->resolved_at)->not->toBeNull() + ->and($problem->note)->toContain('überwiesen'); +}); + +it('keeps the capability check on the page, not only on the modal', function () { + // Ein Modal ist OHNE die Route-Middleware der Seite erreichbar — und das + // Ereignis, das es auslöst, könnte jemand auch direkt schicken. + $problem = bouncedPayment(); + + Livewire::actingAs(operator('Support'), 'operator') + ->test(ResolveFailedCheckout::class, ['id' => $problem->id]) + ->assertForbidden(); + + expect($problem->fresh()->resolved_at)->toBeNull(); +}); diff --git a/tests/Feature/EditInModalTest.php b/tests/Feature/EditInModalTest.php index 708db70..2232fbe 100644 --- a/tests/Feature/EditInModalTest.php +++ b/tests/Feature/EditInModalTest.php @@ -55,6 +55,31 @@ it('never grows input fields inside a table row', function () { $offenders[$path][] = trim(strtok(ltrim($cell), "\n") ?: $m[0]); } } + + // Und dasselbe in
  • . + // + // Nachgetragen am 31.7.2026, weil genau hier ein Verstoß durchrutschte: + // die Liste der geplatzten Zahlungen trug je Zeile ein Textfeld für die + // Begründung und einen Knopf, der eine Methode am Seiten-Bauteil rief. + // Beides steht wörtlich in R20 unter „Verboten" — die Prüfung sah nur + // und damit den Buchstaben statt des Sinns. + // + // Checkbox, Radio und