Mitgliedschaft: Erinnerungen und eigenes Verlaengerungs-Produkt

Die Erinnerungsmails vor Ablauf der Mitgliedschaft beschrieben noch den
abgeschafften SEPA-Einzug und verlinkten auf die Hauptdomain, wo
/user/membership nicht existiert. Das Produkt fuer die Verlaengerung war
zudem dasselbe wie bei der Registrierung und sprach deshalb vom
Neuabschluss.

- Erinnerungen: nur noch Stufen 31/33/34/35, SEPA-Stufen 32/36/37 samt
  :price und :pay_date entfallen; Button verlinkt auf my.mivita.care;
  fehlt ein Text zum Status, wird die Mail geloggt statt roh versendet.
- Alle Erinnerungstexte (de/en/es/fr) erklaeren jetzt, dass ohne Zahlung
  der Account pausiert und offene Provisionen ausgezahlt werden.
- Eigenes Produkt fuer die Verlaengerung (identifier show_renewal) als
  Kopie des Mitgliedschafts-Produkts inkl. Preisen, Kategorien und
  Uebersetzungen fuer en/es/fr; die Mitgliedschaftsseite faellt ohne
  dieses Produkt auf das bisherige zurueck.
- Mitgliedschaftsseite auf ein Paket reduziert: Upgrade-, Wechsel- und
  Abo-Optionen entfernt, Texte auf die manuelle Verlaengerung umgestellt.
- Alte Cron-Weboberflaeche entfernt, der Versand laeuft ueber den
  Scheduler (payments:check-accounts).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-08-26 08:24:32 +00:00
parent 0bdb6eabc6
commit 30fb031d12
30 changed files with 810 additions and 1223 deletions

View file

@ -1,420 +0,0 @@
<?php
namespace App\Http\Controllers;
use Carbon;
use App\User;
use App\Services\Util;
use App\Models\UserHistory;
use App\Models\UserMessage;
use App\Mail\MailCustomMessage;
use App\Mail\MailVerifyAccount;
use App\Services\PaymentHelper;
use App\Repositories\UserRepository;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class CronController extends Controller
{
protected $userRepo;
// Konstanten für bessere Lesbarkeit
private const CRON_KEY = 'CqZHL79FwUCcy9pjvi';
private const RUN_CRON_KEY = 'G8ZvEbnP8fEPfnWX4L';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct(UserRepository $userRepo)
{
$this->userRepo = $userRepo;
Log::channel('cron')->info('CronController initialisiert');
}
/**
* Hauptindex-Methode für Cron-Jobs
*
* @return string
*/
public function index()
{
Log::channel('cron')->info('Cron-Index aufgerufen');
//$this->checkConfirmation();
//TODO
//SEPA Booking
//Mail reminder
return "Cron-Index ausgeführt";
}
/**
* Führt eine bestimmte Cron-Aktion aus
*
* @param string|bool $action Die auszuführende Aktion
* @param string|bool $key Sicherheitsschlüssel
* @return mixed
*/
public function action($action = false, $key = false)
{
Log::channel('cron')->info('Cron-Aktion aufgerufen: ' . $action);
if($key !== self::CRON_KEY){
Log::channel('cron')->warning('Ungültiger Cron-Key verwendet: ' . $key);
abort(404);
}
if($action === 'check_payments_account'){
Log::channel('cron')->info('Starte Überprüfung der Zahlungskonten');
return $this->checkPaymentsAccounts();
}
Log::channel('cron')->warning('Unbekannte Aktion angefordert: ' . $action);
return response('Keine gültige Aktion angegeben', 400);
}
/**
* Überprüft Benutzerbestätigungen und sendet Erinnerungen
*
* @return string
*/
public function checkConfirmation()
{
Log::channel('cron')->info('Starte Überprüfung der Benutzerbestätigungen');
$now = date('Y-m-d H:i:s');
$next = date('Y-m-d H:i:s', strtotime('+3 week'));
$users = User::where('confirmed', '=', 0)->where('confirmation_code_to', '<', $now)->get();
Log::channel('cron')->info('Gefundene unbestätigte Benutzer: ' . $users->count());
foreach ($users as $user) {
//delete user
if ($user->confirmation_code_remider == 1) {
Log::channel('cron')->warning('Lösche unbestätigten Benutzer: ' . $user->email);
$this->userRepo->deleteUser($user);
}
//send new remider
if ($user->confirmation_code_remider == 0) {
if(!Util::isTestSystem()){
Log::channel('cron')->info('Sende Bestätigungserinnerung an: ' . $user->email);
Mail::to($user->email)->locale($user->getLocale())->send(new MailVerifyAccount($user->confirmation_code, $user));
$user->confirmation_code_to = $next;
$user->confirmation_code_remider = 1;
$user->save();
} else {
Log::channel('cron')->info('Testsystem: Bestätigungserinnerung an: ' . $user->email);
}
}
}
return "TOSK";
}
/**
* Überprüft Zahlungskonten und sendet Erinnerungen
*
* @return string
*/
public function checkPaymentsAccounts()
{
Log::channel('cron')->info('Starte Überprüfung der Zahlungskonten');
/*RULES
reminders
> 21 remind_first_days = 31 reminder_first
> 21 remind_first_days + sepa = 32 reminder_first_sepa
> 14 remind_sec_days = 33 reminder_sec
> 2 remind_last_days = 34 reminder_last
> 0 deaktiv = 35 reminder_deaktiv
> 0 deaktiv + sepa = 36 reminder_deaktiv_sepa
== 7 abo_booking_days + sepa + cron = 37 reminder_collect_sepa
*/
//max Date for reminder
$renewalDate = Carbon::now()->modify('+'.(config('mivita.remind_first_days')+1).' days');
Log::channel('cron')->info('Erneuerungsdatum für Zahlungen: ' . $renewalDate->format('Y-m-d H:i:s'));
$users = User::where('payment_account', '!=', NULL)
->where('active', '=', 1)
->where('blocked', '!=', 1)
->where('payment_account', '<', $renewalDate)
->get();
Log::channel('cron')->info('Gefundene Benutzer für Zahlungserinnerungen: ' . $users->count());
foreach ($users as $user){
Log::channel('cron')->info('Prüfe Zahlungserinnerungen für Benutzer: ' . $user->email);
$this->checkReminderPayments($user);
}
return "TOSK";
}
/**
* Initiiert Abo-Zahlungen für einen Benutzer
* hier geht es um die Mitglieschaft Abos - die sind derzeit deaktiviert
*
* @param User $user Benutzer
* @return bool
*/
private function userInitAboPayment(User $user)
{
if(!$user->isAcountAboPayDate()){
Log::channel('cron')->info('Kein Abo-Zahlungsdatum für Benutzer: ' . $user->email);
return false;
}
//user has a open Abo Payment
if($this->checkIsAboPaymentOpen($user)){
Log::channel('cron')->info('Offene Abo-Zahlung für Benutzer: ' . $user->email);
return false;
}
if($user->payment_order_product){
Log::channel('cron')->info('Starte Abo-Zahlung für Benutzer: ' . $user->email);
$this->buyProductAboPayment($user, $user->payment_order_product);
}
return true;
}
/**
* Prüft, ob eine offene Abo-Zahlung existiert
*
* @param User $user Benutzer
* @return bool
*/
private function checkIsAboPaymentOpen(User $user)
{
$isOpen = UserHistory::whereUserId($user->id)
->whereAction('abo_open_payment')
->whereIdentifier($user->payment_account)
->where('status', '>=', 1) //open //error // payment
->get()->last();
if($isOpen){
Log::channel('cron')->info('Offene Abo-Zahlung gefunden für: ' . $user->email);
return true;
}
return false;
}
/**
* Prüft und sendet Zahlungserinnerungen basierend auf Benutzerkontostand
*
* @param User $user Benutzer
* @return bool
*/
private function checkReminderPayments(User $user)
{
//35 reminder_deaktiv, 36 reminder_deaktiv_sepa
if(!$user->isActiveAccount()){
Log::channel('cron')->info('Inaktives Konto für Benutzer: ' . $user->email);
$isSend = $this->checkIsReminderSend($user, 35);
return $isSend;
}
//34 reminder_last
if($user->daysActiveAccount() <= config('mivita.remind_last_days')){
Log::channel('cron')->info('Letzte Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$isSend = $this->checkIsReminderSend($user, 34);
return $isSend;
}
//33 reminder_sec
if($user->daysActiveAccount() <= config('mivita.remind_sec_days')){
Log::channel('cron')->info('Zweite Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$isSend = $this->checkIsReminderSend($user, 33);
return $isSend;
}
//31 reminder_first
if($user->daysActiveAccount() > config('mivita.remind_sec_days')){
Log::channel('cron')->info('Erste Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$isSend = $this->checkIsReminderSend($user, 31);
return $isSend;
}
return false;
}
/**
* Überprüft, ob eine Erinnerung bereits gesendet wurde
*
* @param User $user Benutzer
* @param int $status Status-Code der Erinnerung
* @return bool
*/
private function checkIsReminderSend(User $user, $status)
{
$isSend = UserHistory::whereUserId($user->id)
->whereAction('reminder_payments')
->whereIdentifier($user->payment_account)
->whereStatus($status)
->latest()
->first();
if($isSend){
Log::channel('cron')->info('Erinnerung bereits gesendet für Benutzer: ' . $user->email . ' (Status: ' . $status . ')');
return true;
}
Log::channel('cron')->info('Sende neue Erinnerung für Benutzer: ' . $user->email . ' (Status: ' . $status . ')');
$referenz = $this->sendReminderMail($user, $status);
//is not sent create
UserHistory::create([
'user_id' => $user->id,
'action' => 'reminder_payments',
'referenz' => $referenz,
'identifier' => $user->payment_account,
'status' => $status
]);
return false;
}
/**
* Sendet eine Erinnerungs-E-Mail an den Benutzer
*
* @param User $user Benutzer
* @param int $status Status-Code der Erinnerung
* @return int
*/
private function sendReminderMail(User $user, $status)
{
$days = abs($user->daysActiveAccount());
$pay_date = Carbon::parse($user->payment_account)
->modify('- ' . config('mivita.abo_booking_days') . ' days')
->format('d.m.Y');
$datetime = $user->getPaymentAccountDateFormat();
$price = "";
if($user->payment_order_id && isset($user->payment_order_product->price)){
$price = 'von ' . $user->payment_order_product->getFormattedPrice() . ' EUR';
}
$message = __('reminder.copy_first_' . $status, [
'days' => $days,
'datetime' => $datetime,
'price' => $price,
'pay_date' => $pay_date
]);
$message_last = __('reminder.copy_last_' . $status, [
'days' => $days,
'datetime' => $datetime,
'price' => $price,
'pay_date' => $pay_date
]);
$button = __('reminder.button_' . $status);
$message = preg_replace("/[\n\r]/", "", $message);
$message_last = preg_replace("/[\n\r]/", "", $message_last);
$data = [
'subject' => __('reminder.subject') . " | ID: " . $status,
'message' => $message,
'message_last' => $message_last,
'url' => route('user_membership'),
'button' => $button,
];
$sender = User::find(1);
$customer_mail = UserMessage::create([
'user_id' => $user->id,
'send_user_id' => $sender->id,
'email' => $user->email,
'subject' => $data['subject'],
'message' => $data['message'] . " " . $data['message_last'],
]);
try {
if(!Util::isTestSystem()){
if($status >= 34){
Log::channel('cron')->info('Sende kritische Erinnerung mit BCC an: ' . $user->email);
Mail::to($user->email)
->locale($user->getLocale())
->bcc(config('app.default_mail'))
->send(new MailCustomMessage($user, $data, $sender, false));
} else {
Log::channel('cron')->info('Sende normale Erinnerung an: ' . $user->email);
Mail::to($user->email)
->locale($user->getLocale())
->send(new MailCustomMessage($user, $data, $sender, false));
}
} else {
Log::channel('cron')->info('Testsystem: E-Mail-Versand simuliert für: ' . $user->email);
}
} catch(\Exception $e) {
Log::channel('cron')->error('Mail-Fehler für Benutzer ' . $user->email . ': ' . $e->getMessage());
$customer_mail->fail = true;
$customer_mail->error = $e->getMessage();
$customer_mail->save();
return 0;
}
$customer_mail->send = true;
$customer_mail->sent_at = now();
$customer_mail->save();
Log::channel('cron')->info('Erinnerungsmail erfolgreich gesendet an: ' . $user->email);
return 1;
}
/**
* Kauft ein Produkt mit Abo-Zahlung
*
* @param User $user Benutzer
* @param object $product Produkt
* @return void
*/
private function buyProductAboPayment($user, $product)
{
Log::channel('cron')->info('Starte Abo-Produktkauf für Benutzer: ' . $user->email);
$paymentHelper = new PaymentHelper();
$paymentHelper->setProduct($product);
$paymentHelper->initELVPayment($user);
Log::channel('cron')->info('Abo-Produktkauf abgeschlossen für: ' . $user->email);
}
/**
* Führt das Cron-Script aus
*
* @param string $key Sicherheitsschlüssel
* @return \Illuminate\Http\Response
*/
public function runCron($key)
{
Log::channel('cron')->info('Cron-Script-Ausführung angefordert');
if($key !== self::RUN_CRON_KEY){
Log::channel('cron')->warning('Ungültiger Cron-Script-Key verwendet: ' . $key);
abort(404);
}
$scriptPath = Util::isTestSystem() ? '../cron_script_local.sh' : '../cron_script_server.sh';
Log::channel('cron')->info('Führe Script aus: ' . $scriptPath);
exec("/bin/bash {$scriptPath} 2>&1", $out, $result);
Log::channel('cron')->info('Cron-Script-Ausführung abgeschlossen mit Code: ' . $result);
echo "Returncode: " . $result . "<br>";
echo "Ausgabe des Scripts: " . "<br>";
echo "<pre>"; print_r($out);
exit;
/*return response()->view('cron.result', [
'result' => $result,
'output' => $out
]);*/
}
}

View file

@ -9,7 +9,6 @@ use App\Services\SyS\BuyingsProducts;
use App\Services\SyS\ChangeUserBusinesses;
use App\Services\SyS\CleanHTMLProductDescription;
use App\Services\SyS\Correction;
use App\Services\SyS\Cronjobs;
use App\Services\SyS\Customers;
use App\Services\SyS\Import;
use App\Services\SyS\ImportDbipCountry;
@ -53,9 +52,6 @@ class SysController extends Controller
case 'customers':
return Customers::show();
break;
case 'cronjobs':
return Cronjobs::show();
break;
case 'shopping_orders':
return ShoppingOrders::show();
break;
@ -108,9 +104,6 @@ class SysController extends Controller
case 'customers':
return Customers::store();
break;
case 'cronjobs':
return Cronjobs::store();
break;
case 'shopping_orders':
return ShoppingOrders::store();
break;

View file

@ -9,11 +9,11 @@ use App\Models\ShippingCountry;
use App\Models\ShoppingInstance;
use App\Models\ShoppingOrder;
use App\Models\UserHistory;
use App\Services\Payment;
use App\Services\UserService;
use App\User;
use Auth;
use Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Mail;
use Request;
use Util;
@ -34,11 +34,6 @@ class MembershipController extends Controller
public function index()
{
$user = User::find(Auth::user()->id);
$diff_months = 0;
if ($user->payment_account) {
$diff_months = Carbon::now()->diffInMonths(Carbon::parse($user->payment_account)) + 1;
}
$userShoppingOrders = ShoppingOrder::with('shopping_user', 'shopping_payments')->select('shopping_orders.*')
->where('auth_user_id', '=', $user->id)
@ -48,10 +43,9 @@ class MembershipController extends Controller
->get();
$userHistoryPaymentOrder = null;
$userHistoryUpgradeOrder = null;
/* Bezhalung ist nur 29 Tage vor ablauf möglich */
/* isRenewalAccount payment_account date - config('mivita.renewal_days') Vertragsverlängerung */
/* Die Zahlung ist erst ab config('mivita.renewal_days') Tagen vor Ablauf möglich.
Geht sie ein, verlängert sich die Mitgliedschaft um ein Jahr, sonst läuft sie aus. */
if ($user->isRenewalAccount()) {
// Acount ist noch nicht verlängert / bezahlt
if ($user->payment_account) {
@ -60,10 +54,6 @@ class MembershipController extends Controller
$userHistoryPaymentOrder = UserHistory::whereUserId($user->id)->whereAction('payment_order')->where('created_at', '>=', $payment_greaterThan)->get()->last();
}
}
if ($user->isActiveAccount() && ! $user->isActiveShop()) {
$payment_greaterThan = Carbon::parse($user->payment_account)->modify('-'.(config('mivita.renewal_days') + 1).' days');
$userHistoryUpgradeOrder = UserHistory::whereUserId($user->id)->whereAction('upgrade_order')->where('created_at', '>=', $payment_greaterThan)->get()->last();
}
$userHistoryDeleteMembership = UserHistory::whereUserId($user->id)->whereAction('delete_membership')->whereStatus(50)->get()->last();
$shipping_country_id = $this->checkShoppingCountry($user);
@ -75,11 +65,8 @@ class MembershipController extends Controller
$data = [
'user' => $user,
'products' => Product::where('active', true)->whereJsonContains('show_on', ['7', '8'])->orderBy('pos', 'ASC')->get(),
'upgrade' => Product::where('active', true)->whereJsonContains('show_on', '8')->where('identifier', 'upgrade')->get(),
'diff_months' => $diff_months,
'products' => $this->renewalProducts(),
'userHistoryPaymentOrder' => $userHistoryPaymentOrder,
'userHistoryUpgradeOrder' => $userHistoryUpgradeOrder,
'userHistoryDeleteMembership' => $userHistoryDeleteMembership,
'yard_info' => UserService::getYardInfo(),
'userShoppingOrders' => $userShoppingOrders,
@ -88,6 +75,34 @@ class MembershipController extends Controller
return view('user.membership.index', $data);
}
/**
* Produkte, die bei der Mitgliedschafts-Verlängerung zur Auswahl stehen.
*
* Die Registrierung (show_on 7) nutzt ein eigenes Produkt mit Registrierungs-Text.
* Für die Verlängerung gibt es eine Kopie mit passendem Inhalt (identifier show_renewal).
* Solange keine angelegt ist, greift das bisherige Mitgliedschafts-Produkt.
*
* @return Collection<int, Product>
*/
private function renewalProducts(): Collection
{
$renewalProducts = Product::where('active', true)
->where('identifier', 'show_renewal')
->whereJsonContains('show_on', '8')
->orderBy('pos', 'ASC')
->get();
if ($renewalProducts->isNotEmpty()) {
return $renewalProducts;
}
return Product::where('active', true)
->whereJsonContains('show_on', '7')
->whereJsonContains('show_on', '8')
->orderBy('pos', 'ASC')
->get();
}
private function checkShoppingCountry($user)
{
@ -108,26 +123,6 @@ class MembershipController extends Controller
public function storePayment($action)
{
$data = Request::all();
// #### remove_abo
if ($action === 'remove_abo') {
if (Request::get('abo_options_remove')) {
$user = User::find(Auth::user()->id);
$user->abo_options = false;
$user->save();
$user->account->payment_data = null;
$user->account->save();
UserHistory::create(['user_id' => $user->id, 'action' => 'abo_options_remove', 'status' => 10]);
\Session()->flash('alert-success', __('msg.abo_deaktivert'));
return back();
}
\Session()->flash('alert-error', __('msg.error_checkbox_not_confirm'));
return back();
}
// #### payment order
// #### shop upgrade
if ($action === 'upgrade_order' || $action === 'payment_order') {
@ -135,12 +130,6 @@ class MembershipController extends Controller
$user = User::find(Auth::user()->id);
Yard::instance('shopping')->destroy();
$product = Product::find(Request::get('switchers-package-wizard'));
$showAboOptions = false;
if (Request::get('abo_options')) {
$showAboOptions = false; // true Abo Option deaktivert
$user->abo_options = false; // true Abo Option deaktivert
$user->save();
}
$shipping_country_id = $this->checkShoppingCountry($user);
if (! $shipping_country_id) {
@ -157,14 +146,14 @@ class MembershipController extends Controller
$image = $product->images->first()->slug;
}
$qty = $product->is_membership_only ? 1 : (Request::get('qty') ? Request::get('qty') : 1);
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), $qty, $product->getPriceWith(\App\Services\UserService::getTaxFree(), false, \App\Services\UserService::$user_country), false, false, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
$cartItem = Yard::instance('shopping')->add($product->id, $product->getLang('name'), $qty, $product->getPriceWith(UserService::getTaxFree(), false, UserService::$user_country), false, false, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
if ($cartItem->qty > 1) {
Yard::instance('shopping')->update($cartItem->rowId, 1);
}
if (\App\Services\UserService::getTaxFree()) {
if (UserService::getTaxFree()) {
Yard::setTax($cartItem->rowId, 0);
} else {
Yard::setTax($cartItem->rowId, $product->getTaxWith(\App\Services\UserService::$user_country));
Yard::setTax($cartItem->rowId, $product->getTaxWith(UserService::$user_country));
}
do {
@ -174,7 +163,7 @@ class MembershipController extends Controller
$data = [];
$data['is_from'] = 'membership';
$data['is_for'] = 'me';
$data['user_price_infos'] = \App\Services\UserService::getUserPriceInfos();
$data['user_price_infos'] = UserService::getUserPriceInfos();
ShoppingInstance::create([
'identifier' => $identifier,
@ -191,7 +180,7 @@ class MembershipController extends Controller
Yard::instance('shopping')->store($identifier);
// add to DB
$path = route('checkout.checkout_card', ['identifier' => $identifier]);
UserHistory::create(['user_id' => $user->id, 'action' => $action, 'status' => 1, 'product_id' => $product->id, 'identifier' => $identifier, 'abo_options' => $showAboOptions]);
UserHistory::create(['user_id' => $user->id, 'action' => $action, 'status' => 1, 'product_id' => $product->id, 'identifier' => $identifier]);
// $path = str_replace('http', 'https', $path);
return redirect()->secure($path);