diff --git a/app/Repositories/AboRepository.php b/app/Repositories/AboRepository.php index 2c417c5a..a598e0c5 100644 --- a/app/Repositories/AboRepository.php +++ b/app/Repositories/AboRepository.php @@ -68,6 +68,21 @@ class AboRepository extends BaseRepository return $this->pauseOneMonth($data); } + // Kündigung ist wie die Pause eine eigenständige Aktion: nach dem + // Statuswechsel gibt es keinen nächsten Liefertermin mehr, der + // neu berechnet oder als Hinweis ausgegeben werden müsste. + if (isset($data['abo_cancel']) && $data['abo_cancel'] == 'true') { + $this->updateStatus($data); + + if ((int) $this->model->status === 4) { + \Session()->flash('alert-success', __('abo.cancel_success')); + + return true; + } + + return false; + } + $this->updateStatus($data); $this->model->abo_interval = (int) $data['abo_interval']; $nextDate = $this->isAdminUpdate($data) diff --git a/app/Services/AboHelper.php b/app/Services/AboHelper.php index 8876c2ed..8785a61e 100644 --- a/app/Services/AboHelper.php +++ b/app/Services/AboHelper.php @@ -212,6 +212,25 @@ class AboHelper return $daysUntil >= 0 && $daysUntil < AboRepository::getLockDaysChange(); } + /** + * Prüft, ob Pausieren/Kündigen aktuell durch die Sperrfrist vor der + * Ausführung blockiert ist (Setting `abo-lock-days-pause-cancel`). Admins + * umgehen die Sperre. Spiegelt die serverseitige Prüfung in + * {@see AboRepository::updateStatus()} bzw. `pauseOneMonthLocked()` für die + * UI, damit der Abschnitt "Abo pausieren oder kündigen" den Hinweis statt + * der Buttons zeigt. + */ + public static function isPauseCancelLocked(UserAbo $userAbo, string $view = 'user'): bool + { + if ($view === 'admin' || ! $userAbo->next_date) { + return false; + } + + $daysUntil = (int) Carbon::now()->diffInDays(Carbon::parse($userAbo->next_date), false); + + return $daysUntil >= 0 && $daysUntil < AboRepository::getLockDaysPauseCancel(); + } + public static function canEditAbo($user_abo, $view = 'user') { if ($view === 'portal') { diff --git a/dev/_tickets/entwicklungsplan-protokoll-september-2026.md b/dev/_tickets/entwicklungsplan-protokoll-september-2026.md index 2046d2e6..eb65b02f 100644 --- a/dev/_tickets/entwicklungsplan-protokoll-september-2026.md +++ b/dev/_tickets/entwicklungsplan-protokoll-september-2026.md @@ -1,6 +1,6 @@ # Entwicklungsplan – Protokoll Zoom-Meeting 08.09.2026 -**Stand:** 14.09.2026 · **Status:** C1 (Team-Statistik) umgesetzt, Deploy offen; nächstes Paket: Abo (A4 → A3 → A1+A2) +**Stand:** 16.09.2026 · **Status:** C1/C2 live seit 14.09., A4 umgesetzt (Commit, Deploy offen); als Nächstes A3, dann A1+A2 **Quellen:** `2026 09 Protokoll September-komprimiert.pdf` (Protokoll), `2026-09-14.md` (Ticket-Entwurf Abos #1–#3) **Verwandte Pläne:** `dev/2026-07-22/entwicklungsplan-2026-09-07.md` (K11–K14 behandeln dieselben Themen aus dem Kundenbrief; die dortigen Rückfragen F4–F7 sind weiterhin offen), `dev/2026-09-10-business-plan/` (Ledger-Plan, löst A07/K11 langfristig) @@ -13,7 +13,7 @@ | **A1** | Abo-Ausführungstag frei wählbar (1–31, Monatsende-Regel) | Ticket #1, Protokoll | 3–4 PT | mittel | – | | **A2** | Änderungssperre erst ab 0:00 Uhr am Ausführungstag | Ticket #1 | 1 PT | niedrig | zusammen mit A1 | | **A3** | Abo-Status und Grund sichtbar (Zahlung / Kunde / Admin) | Ticket #2, Protokoll | 2–3 PT | niedrig | – | -| **A4** | Pausieren/Kündigen beschriften, Kündigungs-Hinweis (6 Ausführungen) | Ticket #3, Protokoll | 1 PT | niedrig | – | +| **A4** | Pausieren/Kündigen beschriften, Kündigungs-Hinweis (6 Ausführungen) – **umgesetzt 16.09., Deploy offen** | Ticket #3, Protokoll | 1 PT | niedrig | – | | **A5** | Kundenabo → Partnerabo automatisch umwandeln | Protokoll | 4–6 PT | hoch | Rückfragen R5 | | **B1** | Jahresgebühr automatisch abbuchen (Opt-in-Häkchen) | Protokoll | 6–8 PT | hoch | Rückfragen R6 | | **B2** | Berater fällt aus: Pausieren nach 2 Tagen, Rückgabe bei Zahlung, harter Cut nach 4 Wochen, Sponsor-Mail | Protokoll, Rückmeldung 14.09. | 3–4 PT | mittel (einmaliger Massen-Effekt, akzeptiert) | – | @@ -120,7 +120,7 @@ **Risiko:** niedrig. Reine Zusatzinformation, keine Verhaltensänderung. -### A4 – Pausieren/Kündigen beschriften, Kündigungs-Hinweis +### A4 – Pausieren/Kündigen beschriften, Kündigungs-Hinweis — umgesetzt 16.09., Deploy offen **Ziel (Ticket #3, Protokoll):** Im Partnershop/Portal sofort sichtbar, dass und wo man pausieren/kündigen kann. Beim Kündigen deutlicher Hinweis: neues Abo braucht wieder 6 Ausführungen; Vorschlag, stattdessen zu pausieren. @@ -135,6 +135,15 @@ **Risiko:** keins. 1 PT. +**Umsetzung 16.09.2026 (Commit auf `payment/phase-r-payone-refactoring`):** + +- Neues Partial `resources/views/admin/abo/_pause_cancel.blade.php`, eingebunden unter den Ausführungen auf der CRM-Abo-Seite (`user/abo/detail`, Sicht me und ot) und im Kundenportal (`portal/abo/my_abo`). Zeigt Fortschritt „X von 6 Lieferungen ausgeführt“ (Balken), vor der Mindestlaufzeit nur den Hinweis auf die restlichen Lieferungen, danach die Pause (Formular, bestehende Regeln) und den Kündigen-Button. Innerhalb der Sperrfrist vor der Ausführung (`abo-lock-days-pause-cancel`) erscheint statt der Buttons ein Hinweis mit Lieferdatum — dafür neu `AboHelper::isPauseCancelLocked()`, spiegelt die Prüfung aus `AboRepository`. +- Kündigung über Bootstrap-Modal statt `confirm()`: drei Folgen (Abo endet sofort, Konditionen/Punkte entfallen, neues Abo wieder 6 Pflichtlieferungen), Hinweis „lieber pausieren“, Button „Lieber 1 Monat pausieren“ als primäre Aktion (nur wenn die Pause gerade möglich ist), „Trotzdem kündigen“ erst nach Bestätigungs-Checkbox aktiv. +- Einstellungs-Modal: die Pause bleibt dort, der Kündigen-Button schließt das Modal und springt zum neuen Abschnitt (kein zweiter Kündigungspfad mehr). +- `AboRepository`: Kündigung ist wie die Pause eine eigenständige Aktion — Erfolgsmeldung `abo.cancel_success`, kein Neuberechnen von `next_date` und kein „nächste Ausführung in N Tagen“-Hinweis mehr nach der Kündigung. Server-Regeln (Mindestlaufzeit, Sperrfrist) unverändert. +- Sprachdateien de/en/es/fr (16 neue Keys `pause_cancel_*`, `cancel_*`). Tests: `AboPauseCancelSectionTest` (9 Tests), bestehende `AboPauseTest`, `AboStatusGuardTest`, `AboUpdateAuthorizationTest`, `AboLockDaysSettingTest` grün. +- **Deploy:** Dateien hochladen, `php artisan view:clear`. Keine Migration, kein Setting, keine Daten. `LocalizationSyncTest` schlägt auf dem Branch weiterhin wegen `msg.php` (Berater-Übertragung, nicht committet) und `payment.php` (es/fr seit `2e71173` unvollständig) fehl — nicht Teil von A4. + ### A5 – Kundenabo → Partnerabo automatisch umwandeln **Ziel (Protokoll):** Nach der Partnerregistrierung erkennt das System ein laufendes Kundenabo → Umwandlung anbieten, Zahlung neu bestätigen, Kompensationsprodukte wählen, **Mindestlaufzeit läuft weiter** (3 Lieferungen als Kunde + Monat 4 als Partner). diff --git a/resources/lang/de/abo.php b/resources/lang/de/abo.php index a4f20bba..02b2f83a 100644 --- a/resources/lang/de/abo.php +++ b/resources/lang/de/abo.php @@ -330,4 +330,22 @@ return [ 'payment_method_change_link_mail_subject' => 'Zahlungsart für dein Abo aktualisieren', 'payment_method_change_link_mail_intro' => 'Bitte aktualisiere die Zahlungsart für dein Abo über den folgenden sicheren Link. Deine neue Zahlungsart wählst du direkt auf der sicheren Seite unseres Zahlungsdienstleisters Stripe — es wird dabei nichts abgebucht.', 'payment_method_change_link_mail_button' => 'Zahlungsart aktualisieren', + + // Abschnitt "Abo pausieren oder kündigen" (Ticket A4) + 'pause_cancel_title' => 'Abo pausieren oder kündigen', + 'pause_cancel_intro' => 'Hier kannst du dein Abo für einen Monat aussetzen oder dauerhaft beenden. Beides ist ab der :min. Lieferung möglich.', + 'pause_cancel_progress' => ':count von :min Lieferungen ausgeführt', + 'pause_cancel_locked_hint' => 'Die nächste Lieferung am :date wird bereits vorbereitet (Sperre :lock Tage vorher). Pausieren oder Kündigen ist erst nach dieser Lieferung wieder möglich.', + 'cancel_short_copy' => 'Beendet das Abo dauerhaft. Ein neues Abo beginnt wieder mit :min Pflichtlieferungen.', + 'cancel_in_section_hint' => 'Die Kündigung findest du unten auf der Abo-Seite im Abschnitt „Abo pausieren oder kündigen“.', + 'cancel_modal_title' => 'Abo wirklich kündigen?', + 'cancel_consequences_intro' => 'Bitte beachte, was eine Kündigung bedeutet:', + 'cancel_consequence_end' => 'Das Abo endet sofort – es gibt keine weitere Lieferung.', + 'cancel_consequence_points' => 'Die Abo-Konditionen und die damit verbundenen Punkte entfallen.', + 'cancel_consequence_restart' => 'Ein neues Abo beginnt wieder von vorn: erst nach :min Pflichtlieferungen kann es pausiert oder gekündigt werden.', + 'cancel_alternative_pause' => 'Tipp: Wenn du nur eine Lieferung aussetzen möchtest, pausiere das Abo lieber für einen Monat. Es bleibt dann mit allen Konditionen bestehen.', + 'cancel_prefer_pause_button' => 'Lieber 1 Monat pausieren', + 'cancel_anyway_button' => 'Trotzdem kündigen', + 'cancel_confirm_checkbox' => 'Ich habe verstanden, dass das Abo endet und ein neues Abo wieder :min Pflichtlieferungen hat.', + 'cancel_success' => 'Dein Abo wurde gekündigt.', ]; diff --git a/resources/lang/en/abo.php b/resources/lang/en/abo.php index d45d8d85..70e84cf8 100644 --- a/resources/lang/en/abo.php +++ b/resources/lang/en/abo.php @@ -330,4 +330,21 @@ return [ 'payment_method_change_link_mail_subject' => 'Update the payment method for your subscription', 'payment_method_change_link_mail_intro' => 'Please update the payment method for your subscription via the following secure link. You choose your new payment method directly on the secure page of our payment provider Stripe — nothing will be charged.', 'payment_method_change_link_mail_button' => 'Update payment method', + + 'pause_cancel_title' => 'Pause or cancel subscription', + 'pause_cancel_intro' => 'Here you can pause your subscription for one month or end it permanently. Both are possible from the :min. delivery on.', + 'pause_cancel_progress' => ':count of :min deliveries completed', + 'pause_cancel_locked_hint' => 'The next delivery on :date is already being prepared (locked :lock days before). Pausing or cancelling is possible again after this delivery.', + 'cancel_short_copy' => 'Ends the subscription permanently. A new subscription starts again with :min mandatory deliveries.', + 'cancel_in_section_hint' => 'You will find the cancellation at the bottom of the subscription page in the section "Pause or cancel subscription".', + 'cancel_modal_title' => 'Really cancel the subscription?', + 'cancel_consequences_intro' => 'Please note what a cancellation means:', + 'cancel_consequence_end' => 'The subscription ends immediately – there will be no further delivery.', + 'cancel_consequence_points' => 'The subscription conditions and the associated points are lost.', + 'cancel_consequence_restart' => 'A new subscription starts from scratch: it can only be paused or cancelled after :min mandatory deliveries.', + 'cancel_alternative_pause' => 'Tip: If you only want to skip one delivery, pause the subscription for one month instead. It then continues with all its conditions.', + 'cancel_prefer_pause_button' => 'Rather pause for 1 month', + 'cancel_anyway_button' => 'Cancel anyway', + 'cancel_confirm_checkbox' => 'I understand that the subscription ends and a new subscription has :min mandatory deliveries again.', + 'cancel_success' => 'Your subscription has been cancelled.', ]; diff --git a/resources/lang/es/abo.php b/resources/lang/es/abo.php index 10c658c1..3a023235 100644 --- a/resources/lang/es/abo.php +++ b/resources/lang/es/abo.php @@ -330,4 +330,21 @@ return [ 'payment_method_change_link_mail_subject' => 'Actualice el método de pago de su suscripción', 'payment_method_change_link_mail_intro' => 'Actualice el método de pago de su suscripción a través del siguiente enlace seguro. Elegirá su nuevo método de pago directamente en la página segura de nuestro proveedor de pagos Stripe; no se cobrará nada.', 'payment_method_change_link_mail_button' => 'Actualizar método de pago', + + 'pause_cancel_title' => 'Pausar o cancelar la suscripción', + 'pause_cancel_intro' => 'Aquí puedes pausar tu suscripción durante un mes o finalizarla definitivamente. Ambas opciones están disponibles a partir de la :min.ª entrega.', + 'pause_cancel_progress' => ':count de :min entregas realizadas', + 'pause_cancel_locked_hint' => 'La próxima entrega del :date ya se está preparando (bloqueo :lock días antes). Pausar o cancelar volverá a ser posible después de esta entrega.', + 'cancel_short_copy' => 'Finaliza la suscripción definitivamente. Una nueva suscripción vuelve a empezar con :min entregas obligatorias.', + 'cancel_in_section_hint' => 'Encontrarás la cancelación al final de la página de la suscripción, en la sección «Pausar o cancelar la suscripción».', + 'cancel_modal_title' => '¿Realmente quieres cancelar la suscripción?', + 'cancel_consequences_intro' => 'Ten en cuenta lo que implica una cancelación:', + 'cancel_consequence_end' => 'La suscripción termina de inmediato: no habrá más entregas.', + 'cancel_consequence_points' => 'Se pierden las condiciones de la suscripción y los puntos asociados.', + 'cancel_consequence_restart' => 'Una nueva suscripción empieza desde cero: solo podrá pausarse o cancelarse después de :min entregas obligatorias.', + 'cancel_alternative_pause' => 'Consejo: si solo quieres saltarte una entrega, pausa la suscripción un mes. Así se mantiene con todas sus condiciones.', + 'cancel_prefer_pause_button' => 'Mejor pausar 1 mes', + 'cancel_anyway_button' => 'Cancelar de todos modos', + 'cancel_confirm_checkbox' => 'Entiendo que la suscripción termina y que una nueva suscripción vuelve a tener :min entregas obligatorias.', + 'cancel_success' => 'Tu suscripción ha sido cancelada.', ]; diff --git a/resources/lang/fr/abo.php b/resources/lang/fr/abo.php index 2bb01428..e459e6fa 100644 --- a/resources/lang/fr/abo.php +++ b/resources/lang/fr/abo.php @@ -330,4 +330,21 @@ return [ 'payment_method_change_link_mail_subject' => 'Mettez à jour le moyen de paiement de votre abonnement', 'payment_method_change_link_mail_intro' => 'Veuillez mettre à jour le moyen de paiement de votre abonnement via le lien sécurisé suivant. Vous choisissez votre nouveau moyen de paiement directement sur la page sécurisée de notre prestataire de paiement Stripe — rien ne sera débité.', 'payment_method_change_link_mail_button' => 'Mettre à jour le moyen de paiement', + + 'pause_cancel_title' => 'Suspendre ou résilier l’abonnement', + 'pause_cancel_intro' => 'Ici, tu peux suspendre ton abonnement pendant un mois ou y mettre fin définitivement. Les deux sont possibles à partir de la :min. livraison.', + 'pause_cancel_progress' => ':count livraisons sur :min effectuées', + 'pause_cancel_locked_hint' => 'La prochaine livraison du :date est déjà en préparation (blocage :lock jours avant). Suspendre ou résilier sera de nouveau possible après cette livraison.', + 'cancel_short_copy' => 'Met fin définitivement à l’abonnement. Un nouvel abonnement recommence avec :min livraisons obligatoires.', + 'cancel_in_section_hint' => 'Tu trouveras la résiliation en bas de la page de l’abonnement, dans la section « Suspendre ou résilier l’abonnement ».', + 'cancel_modal_title' => 'Vraiment résilier l’abonnement ?', + 'cancel_consequences_intro' => 'Merci de noter ce qu’implique une résiliation :', + 'cancel_consequence_end' => 'L’abonnement prend fin immédiatement – il n’y aura plus de livraison.', + 'cancel_consequence_points' => 'Les conditions de l’abonnement et les points associés sont perdus.', + 'cancel_consequence_restart' => 'Un nouvel abonnement repart de zéro : il ne pourra être suspendu ou résilié qu’après :min livraisons obligatoires.', + 'cancel_alternative_pause' => 'Conseil : si tu veux seulement sauter une livraison, suspends plutôt l’abonnement pendant un mois. Il continue alors avec toutes ses conditions.', + 'cancel_prefer_pause_button' => 'Plutôt suspendre 1 mois', + 'cancel_anyway_button' => 'Résilier quand même', + 'cancel_confirm_checkbox' => 'J’ai compris que l’abonnement prend fin et qu’un nouvel abonnement comporte de nouveau :min livraisons obligatoires.', + 'cancel_success' => 'Ton abonnement a été résilié.', ]; diff --git a/resources/views/admin/abo/_pause_cancel.blade.php b/resources/views/admin/abo/_pause_cancel.blade.php new file mode 100644 index 00000000..d3c76bc7 --- /dev/null +++ b/resources/views/admin/abo/_pause_cancel.blade.php @@ -0,0 +1,169 @@ +{{-- + Abschnitt "Abo pausieren oder kündigen" (Ticket A4, Entwicklungsplan September 2026). + Wird auf der Abo-Detailseite im CRM (view me/ot) und im Kundenportal (view portal) + eingebunden. Erwartet $user_abo und $view. Beide Aktionen posten wie das + Einstellungs-Modal auf `user_abos_update` (action abo_update_settings); der + aktuelle Liefertag wird unverändert mitgesendet, damit die Liefertag-Validierung + nicht greift. +--}} +@php + $pcStatus = (int) $user_abo->status; + $pcEnded = in_array($pcStatus, App\Services\AboHelper::ENDED_STATUSES, true); + $pcMinDuration = (int) App\Services\AboHelper::getAboMinDuration(); + $pcPaidOrders = (int) $user_abo->getCountPaidOrders(); + $pcCanCancel = App\Services\AboHelper::canCancelAbo($user_abo, $view); + $pcCanPause = App\Services\AboHelper::canPauseAbo($user_abo, $view); + $pcNextPossiblePause = App\Services\AboHelper::nextPossiblePauseDate($user_abo); + $pcLocked = App\Services\AboHelper::isPauseCancelLocked($user_abo, $view); + $pcProgress = $pcMinDuration > 0 ? min(100, (int) round($pcPaidOrders / $pcMinDuration * 100)) : 100; + $pcUpdateRoute = route('user_abos_update', [$view, $user_abo->id]); + $pcNextDate = $user_abo->next_date ? \Carbon\Carbon::parse($user_abo->next_date) : null; +@endphp + +@if ($pcStatus >= 2 && ! $pcEnded) +
{{ __('abo.pause_cancel_intro', ['min' => $pcMinDuration]) }}
+ ++ {{ __('abo.abo_copy_pause', ['months' => App\Services\AboHelper::getPauseIntervalMonths()]) }} +
+ @if ($pcCanPause) + + @elseif ($pcNextPossiblePause) +{{ __('abo.cancel_short_copy', ['min' => $pcMinDuration]) }}
+ +