From 5221dc1498e4a7f80cfd852b826067ba973e8501 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrick=20M=C3=BCller?= <p.mueller@pcsg.de>
Date: Wed, 5 Sep 2018 13:07:47 +0200
Subject: [PATCH] refactor: package overhaul inlcluding implementation of new
 frontend-users xml api #5 #1

---
 ajax/memberships/users/getList.php            |   2 +
 .../profile/UserProfile.Membership.html       |  64 ++---
 bin/controls/profile/UserProfile.css          |  84 ++++++-
 bin/controls/profile/UserProfile.html         |   4 +-
 bin/controls/profile/UserProfile.js           |  29 +--
 composer.json                                 |   7 +-
 events.xml                                    |   1 +
 frontend-users.xml                            |  27 ++
 intranet.xml                                  |   6 -
 locale.xml                                    | 234 +++++++++---------
 settings.xml                                  |   6 +
 .../Controls/Profile/Memberships.html         |   1 +
 .../Controls/Profile/Memberships.php          |  55 ++++
 src/QUI/Memberships/Events.php                | 220 +++++++++-------
 src/QUI/Memberships/Handler.php               | 114 +++++----
 src/QUI/Memberships/Membership.php            | 166 +++++++------
 .../Users/AbortCancelVerification.php         |  20 +-
 src/QUI/Memberships/Users/MembershipUser.php  |   2 +
 18 files changed, 630 insertions(+), 412 deletions(-)
 create mode 100644 frontend-users.xml
 delete mode 100644 intranet.xml
 create mode 100644 src/QUI/Memberships/Controls/Profile/Memberships.html
 create mode 100644 src/QUI/Memberships/Controls/Profile/Memberships.php

diff --git a/ajax/memberships/users/getList.php b/ajax/memberships/users/getList.php
index 8fef826..ec0d11f 100644
--- a/ajax/memberships/users/getList.php
+++ b/ajax/memberships/users/getList.php
@@ -22,6 +22,8 @@ function ($membershipId, $searchParams) {
         $Membership      = $Memberships->getChild((int)$membershipId);
         $membershipUsers = array();
 
+//        $Membership->addUser(QUI::getUserBySession());
+
         foreach ($Membership->searchUsers($searchParams) as $membershipUserId) {
             /** @var MembershipUser $MembershipUser */
             $MembershipUser    = $MembershipUsers->getChild($membershipUserId);
diff --git a/bin/controls/profile/UserProfile.Membership.html b/bin/controls/profile/UserProfile.Membership.html
index ce4177d..d97efc2 100644
--- a/bin/controls/profile/UserProfile.Membership.html
+++ b/bin/controls/profile/UserProfile.Membership.html
@@ -1,34 +1,42 @@
-<div class="quiqqer-memberships-profile-userprofile-membership-info grid-50">
-    <h2 class="quiqqer-memberships-profile-userprofile-membership-info-title">
-        {{membershipTitle}}
-    </h2>
-    <p>
-        {{membershipShort}}
-    </p>
-</div>
-<div class="quiqqer-memberships-profile-userprofile-membership-status-container grid-50 grid-parent">
-    <div class="grid-100">
-        <div class="grid-60">
-            {{labelAddedDate}}
+<section class="quiqqer-memberships-profile-userprofile-membership">
+
+    <header class="quiqqer-memberships-profile-userprofile-membership-header">
+        <div class="quiqqer-memberships-profile-userprofile-membership-header-title">
+            <h3>
+                {{membershipTitle}}
+            </h3>
         </div>
-        <div class="grid-40">
-            {{addedDate}}
+        <div class="quiqqer-memberships-profile-userprofile-membership-header-short">
+            <span>{{membershipShort}}</span>
+            <div class="quiqqer-memberships-profile-userprofile-membership-header-info"></div>
         </div>
-    </div>
-    <div class="grid-100">
-        <div class="grid-60">
-            {{labelEndDate}}
+    </header>
+
+    <div class="quiqqer-memberships-profile-userprofile-membership-body">
+        <div class="quiqqer-memberships-profile-userprofile-membership-body-entry">
+            <div class="quiqqer-memberships-profile-userprofile-membership-body-entry-label">
+                {{labelAddedDate}}
+            </div>
+            <div class="quiqqer-memberships-profile-userprofile-membership-body-entry-value">
+                {{addedDate}}
+            </div>
         </div>
-        <div class="grid-40">
-            {{endDate}}
+        <div class="quiqqer-memberships-profile-userprofile-membership-body-entry">
+            <div class="quiqqer-memberships-profile-userprofile-membership-body-entry-label">
+                {{labelEndDate}}
+            </div>
+            <div class="quiqqer-memberships-profile-userprofile-membership-body-entry-value">
+                {{endDate}}
+            </div>
         </div>
-    </div>
-    <div class="grid-100">
-        <div class="grid-60">
-            {{labelStatus}}
+        <div class="quiqqer-memberships-profile-userprofile-membership-body-entry">
+            <div class="quiqqer-memberships-profile-userprofile-membership-body-entry-label">
+                {{labelStatus}}
+            </div>
+            <div class="quiqqer-memberships-profile-userprofile-membership-body-entry-value quiqqer-memberships-profile-userprofile-status"></div>
+            <div class="quiqqer-memberships-profile-userprofile-status-modifier"></div>
         </div>
-        <div class="quiqqer-memberships-profile-userprofile-status grid-40"></div>
+        <div class="quiqqer-memberships-profile-userprofile-btn"></div>
     </div>
-    <div class="quiqqer-memberships-profile-userprofile-status-modifier grid-100"></div>
-    <div class="quiqqer-memberships-profile-userprofile-btn grid-100"></div>
-</div>
\ No newline at end of file
+
+</section>
\ No newline at end of file
diff --git a/bin/controls/profile/UserProfile.css b/bin/controls/profile/UserProfile.css
index 3b2edd0..be7faa6 100644
--- a/bin/controls/profile/UserProfile.css
+++ b/bin/controls/profile/UserProfile.css
@@ -1,25 +1,66 @@
+.quiqqer-memberships-profile-userprofile-info-header {
+    margin: 0 0 10px 0;
+}
+
 .quiqqer-memberships-profile-userprofile-membership {
+    border: 1px #ddd solid;
+    clear: both;
     float: left;
+    margin-bottom: 20px;
     width: 100%;
 }
 
-.quiqqer-memberships-profile-userprofile-membership:not(:last-of-type) {
-    margin-bottom: 20px;
+/*****************
+ * Header
+ *****************/
+.quiqqer-memberships-profile-userprofile-membership-header {
+    background: #f6f6f6;
+    border-bottom: 1px #ddd solid;
+    float: left;
+    padding: 10px;
+    width: 100%;
+}
+
+.quiqqer-memberships-profile-userprofile-membership-header-title h3 {
+    margin: 0 0 5px 0;
 }
 
-.quiqqer-memberships-profile-userprofile-membership-info-content:hover {
+.quiqqer-memberships-profile-userprofile-membership-header-info-icon:hover {
     cursor: pointer;
     opacity: 0.7;
 }
 
-.quiqqer-memberships-profile-userprofile-membership-info-content {
-    float: right;
-    font-size: 20px;
+.quiqqer-memberships-profile-userprofile-membership-header-info {
+    display: inline-block;
+}
+
+/*****************
+ * Body
+ *****************/
+.quiqqer-memberships-profile-userprofile-membership-body {
+    float: left;
+    padding: 10px;
+    width: 100%;
+}
+
+.quiqqer-memberships-profile-userprofile-membership-body-entry {
+    clear: both;
+    float: left;
+    width: 100%;
+}
+
+.quiqqer-memberships-profile-userprofile-membership-body-entry-label {
+    float: left;
+    min-width: 200px;
+}
+
+.quiqqer-memberships-profile-userprofile-membership-body-entry-value {
+    float: left;
 }
 
 .quiqqer-memberships-profile-userprofile-status-modifier {
-    font-size: 14px;
-    text-align: center;
+    float: left;
+    padding-left: 5px;
 }
 
 .quiqqer-memberships-profile-userprofile-status-cancelled {
@@ -34,9 +75,30 @@
     color: #008000;
 }
 
+.quiqqer-memberships-profile-userprofile-btn {
+    float: left;
+    width: 100%;
+    margin-top: 10px;
+}
+
 .quiqqer-memberships-profile-userprofile-btn .qui-button {
-    border: 1px solid #ff0000 !important;
     float: right;
-    font-size: 12px;
-    line-height: 20px;
+}
+
+@media (max-width: 768px) {
+    .quiqqer-memberships-profile-userprofile-membership-body-entry-label {
+        width: 100%;
+    }
+
+    .quiqqer-memberships-profile-userprofile-membership-body-entry {
+        margin-bottom: 10px;
+    }
+
+    .quiqqer-memberships-profile-userprofile-status-modifier {
+        padding: 0;
+    }
+
+    .quiqqer-memberships-profile-userprofile-btn .qui-button {
+        width: 100%;
+    }
 }
diff --git a/bin/controls/profile/UserProfile.html b/bin/controls/profile/UserProfile.html
index 654b992..aa8312c 100644
--- a/bin/controls/profile/UserProfile.html
+++ b/bin/controls/profile/UserProfile.html
@@ -1,3 +1,3 @@
-<h1>{{header}}</h1>
+<h2 class="quiqqer-memberships-profile-userprofile-info-header">{{header}}</h2>
 <p class="quiqqer-memberships-profile-userprofile-info"></p>
-<div class="quiqqer-memberships-profile-userprofile-memberships grid-parent"></div>
\ No newline at end of file
+<div class="quiqqer-memberships-profile-userprofile-memberships-container"></div>
\ No newline at end of file
diff --git a/bin/controls/profile/UserProfile.js b/bin/controls/profile/UserProfile.js
index 0ac9a6c..3fd7761 100644
--- a/bin/controls/profile/UserProfile.js
+++ b/bin/controls/profile/UserProfile.js
@@ -5,18 +5,6 @@
  *
  * @module package/quiqqer/memberships/bin/controls/profile/UserProfile
  * @author www.pcsg.de (Patrick Müller)
- *
- * @require qui/controls/Control
- * @require qui/controls/loader/Loader
- * @require qui/controls/windows/Confirm
- * @require qui/controls/buttons/Button
- * @require package/quiqqer/memberships/bin/MembershipUsers
- * @require Locale
- * @require Ajax
- * @require Mustache
- * @require text!package/quiqqer/memberships/bin/controls/profile/UserProfile.html
- * @require text!package/quiqqer/memberships/bin/controls/profile/UserProfile.MembershipStatus.html
- * @require css!package/quiqqer/memberships/bin/controls/profile/UserProfile.css
  */
 define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
 
@@ -48,7 +36,7 @@ define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
         Type   : 'package/quiqqer/memberships/bin/controls/profile/UserProfile',
 
         Binds: [
-            '$onInject',
+            '$onImport',
             'refresh',
             '$build',
             '$openCancelConfirm',
@@ -67,14 +55,14 @@ define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
             this.$memberships = [];
 
             this.addEvents({
-                onInject: this.$onInject
+                onImport: this.$onImport
             });
         },
 
         /**
          * Event: onImport
          */
-        $onInject: function () {
+        $onImport: function () {
             this.$Elm.addClass('quiqqer-memberships-membershipusersarchive');
 
             var lgPrefix = 'controls.profile.userprofile.template.';
@@ -110,7 +98,7 @@ define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
          */
         $build: function () {
             var MembershipsElm = this.$Elm.getElement(
-                '.quiqqer-memberships-profile-userprofile-memberships'
+                '.quiqqer-memberships-profile-userprofile-memberships-container'
             );
 
             MembershipsElm.set('html', '');
@@ -173,15 +161,14 @@ define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
             }
 
             var MembershipElm = new Element('div', {
-                'class': 'quiqqer-memberships-profile-userprofile-membership grid-100',
-                html   : Mustache.render(membershipTemplate, {
+                html: Mustache.render(membershipTemplate, {
                     membershipTitle: Membership.membershipTitle,
                     membershipShort: Membership.membershipShort,
                     labelAddedDate : QUILocale.get(lg, lgPrefix + 'labelAddedDate'),
                     addedDate      : Membership.addedDate,
                     labelEndDate   : endDateLabel,
                     endDate        : endDateValue,
-                    labelStatus    : QUILocale.get(lg, lgPrefix + 'labelStatus'),
+                    labelStatus    : QUILocale.get(lg, lgPrefix + 'labelStatus')
                 })
             });
 
@@ -217,11 +204,11 @@ define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
             // show content btn
             if (Membership.membershipContent !== '') {
                 var ShowContentElm = MembershipElm.getElement(
-                    '.quiqqer-memberships-profile-userprofile-membership-info-title'
+                    '.quiqqer-memberships-profile-userprofile-membership-header-info'
                 );
 
                 new Element('span', {
-                    'class': 'fa fa-info-circle quiqqer-memberships-profile-userprofile-membership-info-content',
+                    'class': 'fa fa-info-circle quiqqer-memberships-profile-userprofile-membership-header-info-icon',
                     events : {
                         click: function () {
                             self.$showMembershipContent(Membership);
diff --git a/composer.json b/composer.json
index 22f45d1..977fee8 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,10 @@
   "type": "quiqqer-module",
   "description": "The Membership Module is an easy to use Membership Plugin. It will give you the ability to confidently create, manage and track membership subscriptions. In addition to these powerful abilities, the Membership Module will allow you to grant and revoke permissions to Users.",
   "version": "dev-dev",
-  "license": ["PCSG QL-1.0", "CC BY-NC-SA 4.0"],
+  "license": [
+    "PCSG QL-1.0",
+    "CC BY-NC-SA 4.0"
+  ],
   "authors": [
     {
       "name": "Patrick Müller",
@@ -17,7 +20,7 @@
     "url": "http://www.pcsg.de"
   },
   "require": {
-    "quiqqer/verification": "dev-dev"
+    "quiqqer/verification": "1.*|*@dev"
   },
   "autoload": {
     "psr-4": {
diff --git a/events.xml b/events.xml
index 61f4bae..a78477f 100644
--- a/events.xml
+++ b/events.xml
@@ -3,4 +3,5 @@
     <event on="onPackageSetup" fire="\QUI\Memberships\Events::onPackageSetup"/>
     <event on="onUserSave" fire="\QUI\Memberships\Events::onUserSave"/>
     <event on="onOrderSuccess" fire="\QUI\Memberships\Events::onOrderSuccess"/>
+    <event on="onQuiqqerProductsFieldDelete" fire="\QUI\Memberships\Events::onQuiqqerProductsFieldDelete"/>
 </events>
\ No newline at end of file
diff --git a/frontend-users.xml b/frontend-users.xml
new file mode 100644
index 0000000..c23dc95
--- /dev/null
+++ b/frontend-users.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<quiqqer>
+    <frontend-users>
+        <profile>
+            <categories>
+                <category name="memberships">
+                    <title>
+                        <locale group="quiqqer/memberships" var="profile.category.memberships.title"/>
+                    </title>
+
+                    <!-- User Data -->
+                    <settings control="\QUI\Memberships\Controls\Profile\Memberships"
+                              name="mymemberships"
+                              icon="fa fa-id-card-o"
+                              showinprofilebar="1"
+                    >
+                        <title>
+                            <locale group="quiqqer/memberships" var="profile.mymemberships.title"/>
+                        </title>
+                    </settings>
+
+                </category>
+            </categories>
+
+        </profile>
+    </frontend-users>
+</quiqqer>
diff --git a/intranet.xml b/intranet.xml
deleted file mode 100644
index 2e601af..0000000
--- a/intranet.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<menu>
-    <item require="package/quiqqer/memberships/bin/controls/profile/UserProfile" icon="fa fa-id-card-o" name="memberships-profile">
-        <locale group="quiqqer/memberships" var="profile.button.text" />
-    </item>
-</menu>
diff --git a/locale.xml b/locale.xml
index a0ffe4f..011489a 100644
--- a/locale.xml
+++ b/locale.xml
@@ -169,6 +169,14 @@
             <de><![CDATA[Mitgliedschaft, in die alle Benutzer im System automatisch gesetzt werden. Benutzer haben keine Möglichkeit diese Mitgliedschaft zu kündigen.]]></de>
             <en><![CDATA[Membership in which all users in the system are automatically put. Users have no option to cancel this membership.]]></en>
         </locale>
+        <locale name="settings.membershipFieldId.title">
+            <de><![CDATA[Mitgliedschaft Produktfeld]]></de>
+            <en><![CDATA[Membership product field]]></en>
+        </locale>
+        <locale name="settings.membershipFieldId.description">
+            <de><![CDATA[Legt das Produktfeld fest, über welches Produkten jeweils eine Mitgliedschaft zugeordnet wird.]]></de>
+            <en><![CDATA[Determines the product field that is used to assign a memebership to a product.]]></en>
+        </locale>
 
         <!-- Cron -->
         <locale name="cron.checkMembershipUsers.title">
@@ -181,10 +189,14 @@
         </locale>
 
         <!-- Profile -->
-        <locale name="profile.button.text">
+        <locale name="profile.category.memberships.title">
             <de><![CDATA[Mitgliedschaften]]></de>
             <en><![CDATA[Memberships]]></en>
         </locale>
+        <locale name="profile.mymemberships.title">
+            <de><![CDATA[Meine Mitgliedschaften]]></de>
+            <en><![CDATA[My memberships]]></en>
+        </locale>
 
         <!-- quiqqer/products -->
         <locale name="products.field.membership">
@@ -208,6 +220,112 @@
 
     <groups name="quiqqer/memberships" datatype="js">
 
+        <!-- Control: profile/UserProfile -->
+        <locale name="controls.profile.userprofile.template.header">
+            <de><![CDATA[Meine Mitgliedschaften]]></de>
+            <en><![CDATA[My memberships]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.template.headerMembership">
+            <de><![CDATA[Mitgliedschaft]]></de>
+            <en><![CDATA[Membership]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.template.headerMembershipData">
+            <de><![CDATA[Details]]></de>
+            <en><![CDATA[Details]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.labelAddedDate">
+            <de><![CDATA[In Mitgliedschaft seit]]></de>
+            <en><![CDATA[In membership since]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.labelEndDate.autoExtend">
+            <de><![CDATA[Autom. Verlängerung am]]></de>
+            <en><![CDATA[Auto extend at]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.labelEndDate.noAutoExtend">
+            <de><![CDATA[Mitgliedschaft endet]]></de>
+            <en><![CDATA[Membership ends]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.endDate.infinite">
+            <de><![CDATA[-]]></de>
+            <en><![CDATA[-]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.labelStatus">
+            <de><![CDATA[Status]]></de>
+            <en><![CDATA[Status]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.status.cancelled">
+            <de><![CDATA[gekündigt]]></de>
+            <en><![CDATA[cancelled]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.status.active">
+            <de><![CDATA[aktiv]]></de>
+            <en><![CDATA[active]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.btn.cancel.text">
+            <de><![CDATA[Mitgliedschaft kündigen]]></de>
+            <en><![CDATA[Cancel membership]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.btn.abortcancel.text">
+            <de><![CDATA[Kündigung zurückziehen]]></de>
+            <en><![CDATA[Withdraw cancellation]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.cancelconfirm.info" html="true">
+            <de><![CDATA[Sind Sie sicher, dass Sie die Mitgliedschaft <b>[title]</b> kündigen wollen? Bitte beachten Sie, dass Ihre Mitgliedschaft dann zum <b>[endDate]</b> ausläuft. Bis dahin können Sie weiterhin alle Vorteile Ihrer Mitgliedschaft nutzen. Bis zum Ablaufdatum können Sie die Kündigung jederzeit zurückziehen.]]></de>
+            <en><![CDATA[Are you sure you want to cancel your <b>[title]</b> membership? Please note that your membership expires by <b>[endDate]</b>. Until then, you can continue to take full advantage of your membership. Until the expiration date, you can withdraw the cancellation at any time.]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.cancelconfirm.text">
+            <de><![CDATA[Kündigung der Mitgliedschaft "[title]"]]></de>
+            <en><![CDATA[Cancellation of membership "[title]"]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.cancelconfirm.title">
+            <de><![CDATA[Mitgliedschaft kündigen]]></de>
+            <en><![CDATA[Cancel membership]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.cancelconfirm.cancel">
+            <de><![CDATA[Abbrechen]]></de>
+            <en><![CDATA[Abbrechen]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.cancelconfirm.ok">
+            <de><![CDATA[Mitgliedschaft kündigen]]></de>
+            <en><![CDATA[Cancel membership]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.abortcancel.info" html="true">
+            <de><![CDATA[Sind Sie sicher, dass Sie die Kündigung Ihrer Mitgliedschaft <b>[title]</b> zurückziehen wollen?]]></de>
+            <en><![CDATA[Are you sure you want to withdraw the cancellation of your <b>[title]</b> membership?]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.abortcancel.text">
+            <de><![CDATA[Kündigung der Mitgliedschaft "[title]" zurückziehen]]></de>
+            <en><![CDATA[Withdraw cancellation of membership "[title]"]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.abortcancel.title">
+            <de><![CDATA[Mitgliedschafts-Kündigung zurückziehen]]></de>
+            <en><![CDATA[Withdraw cancellation membership]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.abortcancel.cancel">
+            <de><![CDATA[Abbrechen]]></de>
+            <en><![CDATA[Abbrechen]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.abortcancel.ok">
+            <de><![CDATA[Kündigung zurückziehen]]></de>
+            <en><![CDATA[Withdraw cancellation]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.info.no.memberships">
+            <de><![CDATA[Es sind z.Z. keine Mitgliedschaften in Ihrem Nutzerprofil vorhanden.]]></de>
+            <en><![CDATA[There are currently no memberships in your user profile.]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.showcontent.title">
+            <de><![CDATA[Mitgliedschaft "[title]"]]></de>
+            <en><![CDATA[Membership "[title]"]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.status.modifier.cancel_confirm">
+            <de><![CDATA[(Kündigung beantragt - Bestätigung ausstehend)]]></de>
+            <en><![CDATA[(Cancellation requested - confirmation pending)]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.status.modifier.abortcancel_confirm">
+            <de><![CDATA[(Kündigung zurückgezogen - Bestätigung ausstehend)]]></de>
+            <en><![CDATA[(Cancellation withdrawn - confirmation pending)]]></en>
+        </locale>
+
         <!-- Control: MembershipsSearchPopup -->
         <locale name="controls.membershipssearchpopup.title">
             <de><![CDATA[Mitgliedschafts-Verwaltung]]></de>
@@ -800,112 +918,6 @@
             <en><![CDATA[Send confirmation]]></en>
         </locale>
 
-        <!-- Control: profile/UserProfile -->
-        <locale name="controls.profile.userprofile.template.header">
-            <de><![CDATA[Ihre Mitgliedschaften]]></de>
-            <en><![CDATA[Your memberships]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.template.headerMembership">
-            <de><![CDATA[Mitgliedschaft]]></de>
-            <en><![CDATA[Membership]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.template.headerMembershipData">
-            <de><![CDATA[Details]]></de>
-            <en><![CDATA[Details]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.labelAddedDate">
-            <de><![CDATA[In Mitgliedschaft seit]]></de>
-            <en><![CDATA[In membership since]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.labelEndDate.autoExtend">
-            <de><![CDATA[Autom. Verlängerung am]]></de>
-            <en><![CDATA[Auto extend at]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.labelEndDate.noAutoExtend">
-            <de><![CDATA[Mitgliedschaft endet]]></de>
-            <en><![CDATA[Membership ends]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.endDate.infinite">
-            <de><![CDATA[-]]></de>
-            <en><![CDATA[-]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.labelStatus">
-            <de><![CDATA[Status]]></de>
-            <en><![CDATA[Status]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.status.cancelled">
-            <de><![CDATA[gekündigt]]></de>
-            <en><![CDATA[cancelled]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.datatable.status.active">
-            <de><![CDATA[aktiv]]></de>
-            <en><![CDATA[active]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.btn.cancel.text">
-            <de><![CDATA[Mitgliedschaft kündigen]]></de>
-            <en><![CDATA[Cancel membership]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.btn.abortcancel.text">
-            <de><![CDATA[Kündigung zurückziehen]]></de>
-            <en><![CDATA[Withdraw cancellation]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.cancelconfirm.info" html="true">
-            <de><![CDATA[Sind Sie sicher, dass Sie die Mitgliedschaft <b>[title]</b> kündigen wollen? Bitte beachten Sie, dass Ihre Mitgliedschaft dann zum <b>[endDate]</b> ausläuft. Bis dahin können Sie weiterhin alle Vorteile Ihrer Mitgliedschaft nutzen. Bis zum Ablaufdatum können Sie die Kündigung jederzeit zurückziehen.]]></de>
-            <en><![CDATA[Are you sure you want to cancel your <b>[title]</b> membership? Please note that your membership expires by <b>[endDate]</b>. Until then, you can continue to take full advantage of your membership. Until the expiration date, you can withdraw the cancellation at any time.]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.cancelconfirm.text">
-            <de><![CDATA[Kündigung der Mitgliedschaft "[title]"]]></de>
-            <en><![CDATA[Cancellation of membership "[title]"]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.cancelconfirm.title">
-            <de><![CDATA[Mitgliedschaft kündigen]]></de>
-            <en><![CDATA[Cancel membership]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.cancelconfirm.cancel">
-            <de><![CDATA[Abbrechen]]></de>
-            <en><![CDATA[Abbrechen]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.cancelconfirm.ok">
-            <de><![CDATA[Mitgliedschaft kündigen]]></de>
-            <en><![CDATA[Cancel membership]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.abortcancel.info" html="true">
-            <de><![CDATA[Sind Sie sicher, dass Sie die Kündigung Ihrer Mitgliedschaft <b>[title]</b> zurückziehen wollen?]]></de>
-            <en><![CDATA[Are you sure you want to withdraw the cancellation of your <b>[title]</b> membership?]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.abortcancel.text">
-            <de><![CDATA[Kündigung der Mitgliedschaft "[title]" zurückziehen]]></de>
-            <en><![CDATA[Withdraw cancellation of membership "[title]"]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.abortcancel.title">
-            <de><![CDATA[Mitgliedschafts-Kündigung zurückziehen]]></de>
-            <en><![CDATA[Withdraw cancellation membership]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.abortcancel.cancel">
-            <de><![CDATA[Abbrechen]]></de>
-            <en><![CDATA[Abbrechen]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.abortcancel.ok">
-            <de><![CDATA[Kündigung zurückziehen]]></de>
-            <en><![CDATA[Withdraw cancellation]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.info.no.memberships">
-            <de><![CDATA[Es sind z.Z. keine Mitgliedschaften in Ihrem Nutzerprofil vorhanden.]]></de>
-            <en><![CDATA[There are currently no memberships in your user profile.]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.showcontent.title">
-            <de><![CDATA[Mitgliedschaft "[title]"]]></de>
-            <en><![CDATA[Membership "[title]"]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.status.modifier.cancel_confirm">
-            <de><![CDATA[(Kündigung beantragt - Bestätigung ausstehend)]]></de>
-            <en><![CDATA[(Cancellation requested - confirmation pending)]]></en>
-        </locale>
-        <locale name="controls.profile.userprofile.status.modifier.abortcancel_confirm">
-            <de><![CDATA[(Kündigung zurückgezogen - Bestätigung ausstehend)]]></de>
-            <en><![CDATA[(Cancellation withdrawn - confirmation pending)]]></en>
-        </locale>
-
         <!-- Templates -->
         <locale name="templates.mail.greeting">
             <de><![CDATA[Hallo [name]!]]></de>
@@ -978,12 +990,12 @@
             <en><![CDATA[The membership has already been cancelled.]]></en>
         </locale>
         <locale name="verification.abortcancel.success.autoExtend">
-            <de><![CDATA[Die Kündigung Ihrer Mitgliedschaft wurde erfolgreich zurückgezogen! Die Mitgliedschaft erneuert sich weiterhin automatisch am [endDate].]]></de>
-            <en><![CDATA[The termination of your membership has been withdrawn successfully! You membership will automatically extend at [endDate].]]></en>
+            <de><![CDATA[Die Kündigung Ihrer Mitgliedschaft "[membershipTitle]" wurde erfolgreich zurückgezogen! Die Mitgliedschaft erneuert sich weiterhin automatisch am [endDate].]]></de>
+            <en><![CDATA[The termination of your membership "[membershipTitle]" has been withdrawn successfully! You membership will automatically extend at [endDate].]]></en>
         </locale>
         <locale name="verification.abortcancel.success.noAutoExtend">
-            <de><![CDATA[Die Kündigung Ihrer Mitgliedschaft wurde erfolgreich zurückgezogen. Sie läuft automatisch zum [endDate] aus.]]></de>
-            <en><![CDATA[The termination of your membership has been withdrawn successfully. It automatically expires at [endDate].]]></en>
+            <de><![CDATA[Die Kündigung Ihrer Mitgliedschaft "[membershipTitle]" wurde erfolgreich zurückgezogen. Sie läuft automatisch zum [endDate] aus.]]></de>
+            <en><![CDATA[The termination of your membership "[membershipTitle]" has been withdrawn successfully. It automatically expires at [endDate].]]></en>
         </locale>
         <locale name="verification.abortcancel.error.general">
             <de><![CDATA[Beim Zurückziehen Ihrer Kündigung ist ein Fehler aufgetreten. Bitte starten Sie den Vorgang erneut oder kontaktieren Sie einen Administrator.]]></de>
diff --git a/settings.xml b/settings.xml
index 56d21c7..8f0aa07 100644
--- a/settings.xml
+++ b/settings.xml
@@ -54,6 +54,12 @@
                 <conf name="categoryId">
                     <type><![CDATA[integer]]></type>
                 </conf>
+                <conf name="membershipFieldId">
+                    <type><![CDATA[integer]]></type>
+                </conf>
+                <conf name="membershipFlagFieldId">
+                    <type><![CDATA[integer]]></type>
+                </conf>
             </section>
 
         </config>
diff --git a/src/QUI/Memberships/Controls/Profile/Memberships.html b/src/QUI/Memberships/Controls/Profile/Memberships.html
new file mode 100644
index 0000000..6d1d8dd
--- /dev/null
+++ b/src/QUI/Memberships/Controls/Profile/Memberships.html
@@ -0,0 +1 @@
+<div class="quiqqer-memberships-profile-memberships"></div>
\ No newline at end of file
diff --git a/src/QUI/Memberships/Controls/Profile/Memberships.php b/src/QUI/Memberships/Controls/Profile/Memberships.php
new file mode 100644
index 0000000..8e34e47
--- /dev/null
+++ b/src/QUI/Memberships/Controls/Profile/Memberships.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace QUI\Memberships\Controls\Profile;
+
+use QUI;
+use QUI\FrontendUsers\Controls\Profile\AbstractProfileControl;
+
+class Memberships extends AbstractProfileControl
+{
+    /**
+     * Constructor
+     *
+     * @param array $attributes
+     */
+    public function __construct(array $attributes = [])
+    {
+        parent::__construct($attributes);
+        $this->setJavaScriptControl('package/quiqqer/memberships/bin/controls/profile/UserProfile');
+    }
+
+    /**
+     * Return the inner body of the element
+     * Can be overwritten
+     *
+     * @return string
+     * @throws \QUI\Exception
+     */
+    public function getBody()
+    {
+        $Engine = QUI::getTemplateManager()->getEngine();
+        return $Engine->fetch(dirname(__FILE__).'/Memberships.html');
+    }
+
+    /**
+     * Method is called, when on save is triggered
+     *
+     * @return mixed|void
+     */
+    public function onSave()
+    {
+        \QUI\System\Log::writeRecursive("onSave");
+    }
+
+    /**
+     * Validate the send data
+     *
+     * @return mixed|void
+     * @throws \Exception
+     */
+    public function validate()
+    {
+        \QUI\System\Log::writeRecursive("validate");
+    }
+
+}
diff --git a/src/QUI/Memberships/Events.php b/src/QUI/Memberships/Events.php
index 1042d51..74515c3 100644
--- a/src/QUI/Memberships/Events.php
+++ b/src/QUI/Memberships/Events.php
@@ -8,6 +8,7 @@
 use QUI\Memberships\Users\Handler as MembershipUsersHandler;
 use QUI\Memberships\Products\MembershipField;
 use QUI\ERP\Products\Handler\Fields as ProductFields;
+use QUI\ERP\Products\Field\Field as ProductField;
 use QUI\ERP\Products\Handler\Categories as ProductCategories;
 use QUI\ERP\Products\Handler\Search as ProductSearchHandler;
 use QUI\ERP\Products\Product\Product;
@@ -34,17 +35,21 @@ public static function onPackageSetup(Package $Package)
 
         $packages = Utils::getInstalledMembershipPackages();
 
-        foreach ($packages as $package) {
-            switch ($package) {
-                case 'quiqqer/products':
-                    self::createProductFields();
-                    self::createProductCategory();
-                    break;
-
-                case 'quiqqer/contracts':
-                    // @todo setup routine for quiqqer/contracts
-                    break;
+        try {
+            foreach ($packages as $package) {
+                switch ($package) {
+                    case 'quiqqer/products':
+                        self::createProductFields();
+                        self::createProductCategory();
+                        break;
+
+                    case 'quiqqer/contracts':
+                        // @todo setup routine for quiqqer/contracts
+                        break;
+                }
             }
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
         }
     }
 
@@ -56,8 +61,14 @@ public static function onPackageSetup(Package $Package)
      */
     public static function onQuiqqerProductsProductDelete(Product $Product)
     {
+        $membershipFieldId = Handler::getProductMembershipField()->getId();
+
+        if (!$membershipFieldId) {
+            return;
+        }
+
         // check if Product is assigned to a Membership
-        $membershipId = $Product->getFieldValue(MembershipsHandler::PRODUCTS_FIELD_MEMBERSHIP);
+        $membershipId = $Product->getFieldValue($membershipFieldId);
 
         if (empty($membershipId)) {
             return;
@@ -68,9 +79,9 @@ public static function onQuiqqerProductsProductDelete(Product $Product)
             $Membership      = MembershipsHandler::getInstance()->getChild($membershipId);
             $MembershipUsers = MembershipUsersHandler::getInstance();
 
-            $membershipUserIds = $Membership->searchUsers(array(
+            $membershipUserIds = $Membership->searchUsers([
                 'productId' => $Product->getId()
-            ));
+            ]);
 
             foreach ($membershipUserIds as $membershipUserId) {
                 $MembershipUser = $MembershipUsers->getChild($membershipUserId);
@@ -79,8 +90,8 @@ public static function onQuiqqerProductsProductDelete(Product $Product)
             }
         } catch (\Exception $Exception) {
             QUI\System\Log::addError(
-                self::class . ' :: onQuiqqerProductsProductDelete -> '
-                . $Exception->getMessage()
+                self::class.' :: onQuiqqerProductsProductDelete -> '
+                .$Exception->getMessage()
             );
         }
     }
@@ -113,82 +124,93 @@ public static function onUserSave(QUI\Users\User $User)
     /**
      * quiqqer/products
      *
-     * Create a Membership field with a fixed id and a membership flag field that
-     * identifies a product as a Membership product
+     * Create necessary membership product fields and save their IDs to the config
      *
      * @return void
+     * @throws \QUI\Exception
      */
     protected static function createProductFields()
     {
-        $L = new QUI\Locale();
-
-        // Membership field
-        $translations = array(
-            'de' => '',
-            'en' => ''
-        );
+        $L    = new QUI\Locale();
+        $Conf = QUI::getPackage('quiqqer/memberships')->getConfig();
 
-        foreach ($translations as $l => $t) {
-            $L->setCurrent($l);
-            $translations[$l] = $L->get(
-                'quiqqer/memberships',
-                'products.field.membership'
-            );
-        }
+        // Membership field (create new one is not configured)
+        $MembershipField = Handler::getProductMembershipField();
+
+        if ($MembershipField === false) {
+            $translations = [
+                'de' => '',
+                'en' => ''
+            ];
+
+            foreach ($translations as $l => $t) {
+                $L->setCurrent($l);
+                $translations[$l] = $L->get(
+                    'quiqqer/memberships',
+                    'products.field.membership'
+                );
+            }
 
-        try {
-            $NewField = ProductFields::createField(array(
-                'id'            => MembershipsHandler::PRODUCTS_FIELD_MEMBERSHIP,
-                'type'          => MembershipField::TYPE,
-                'titles'        => $translations,
-                'workingtitles' => $translations
-            ));
-
-            $NewField->setAttribute('search_type', ProductSearchHandler::SEARCHTYPE_TEXT);
-            $NewField->save();
-        } catch (\QUI\ERP\Products\Field\Exception $Exception) {
-            // nothing, field exists
-        } catch (\Exception $Exception) {
-            QUI\System\Log::addError(self::class . ' :: createProductFields');
-            QUI\System\Log::writeException($Exception);
+            try {
+                $MembershipField = ProductFields::createField([
+                    'type'          => MembershipField::TYPE,
+                    'titles'        => $translations,
+                    'workingtitles' => $translations
+                ]);
+
+                $MembershipField->setAttribute('search_type', ProductSearchHandler::SEARCHTYPE_TEXT);
+                $MembershipField->save();
+
+                // add field id to config
+                $Conf->set('products', 'membershipFieldId', $MembershipField->getId());
+                $Conf->save();
+            } catch (\Exception $Exception) {
+                QUI\System\Log::addError(self::class.' :: createProductFields');
+                QUI\System\Log::writeException($Exception);
+            }
         }
 
-        // membership flag field
-        $translations = array(
-            'de' => '',
-            'en' => ''
-        );
-
-        foreach ($translations as $l => $t) {
-            $L->setCurrent($l);
-            $translations[$l] = $L->get(
-                'quiqqer/memberships',
-                'products.field.membershipflag'
-            );
-        }
+        // Membership flag field (create new one is not configured)
+        $MembershipFlagField = Handler::getProductMembershipFlagField();
+
+        if ($MembershipFlagField === false) {
+            $translations = [
+                'de' => '',
+                'en' => ''
+            ];
+
+            foreach ($translations as $l => $t) {
+                $L->setCurrent($l);
+                $translations[$l] = $L->get(
+                    'quiqqer/memberships',
+                    'products.field.membershipflag'
+                );
+            }
 
-        try {
-            $NewField = ProductFields::createField(array(
-                'id'            => MembershipsHandler::PRODUCTS_FIELD_MEMBERSHIPFLAG,
-                'type'          => ProductFields::TYPE_BOOL,
-                'titles'        => $translations,
-                'workingtitles' => $translations
-            ));
-
-            $NewField->setAttribute('search_type', ProductSearchHandler::SEARCHTYPE_BOOL);
-            $NewField->save();
-
-            // add Flag field to backend search
-            $BackendSearch                    = ProductSearchHandler::getBackendSearch();
-            $searchFields                     = $BackendSearch->getSearchFields();
-            $searchFields[$NewField->getId()] = true;
-
-            $BackendSearch->setSearchFields($searchFields);
-        } catch (\QUI\ERP\Products\Field\Exception $Exception) {
-            // nothing, field exists
-        } catch (\Exception $Exception) {
-            QUI\System\Log::addError(self::class . ' :: createProductFields');
-            QUI\System\Log::writeException($Exception);
+            try {
+                $MembershipFlagField = ProductFields::createField([
+                    'type'          => ProductFields::TYPE_BOOL,
+                    'titles'        => $translations,
+                    'workingtitles' => $translations
+                ]);
+
+                $MembershipFlagField->setAttribute('search_type', ProductSearchHandler::SEARCHTYPE_BOOL);
+                $MembershipFlagField->save();
+
+                // add Flag field to backend search
+                $BackendSearch                               = ProductSearchHandler::getBackendSearch();
+                $searchFields                                = $BackendSearch->getSearchFields();
+                $searchFields[$MembershipFlagField->getId()] = true;
+
+                $BackendSearch->setSearchFields($searchFields);
+
+                // add field id to config
+                $Conf->set('products', 'membershipFlagFieldId', $MembershipFlagField->getId());
+                $Conf->save();
+            } catch (\Exception $Exception) {
+                QUI\System\Log::addError(self::class.' :: createProductFields');
+                QUI\System\Log::writeException($Exception);
+            }
         }
     }
 
@@ -207,6 +229,12 @@ public static function onOrderSuccess(AbstractOrder $Order)
         }
     }
 
+    public static function onQuiqqerProductsFieldDelete(ProductField $Field)
+    {
+        $Conf = QUI::getPackage('quiqqer/memberships')->getConfig();
+        $membershipFieldId =
+    }
+
     /**
      * Create a product category for memberships
      *
@@ -224,22 +252,22 @@ protected static function createProductCategory()
         try {
             $Category = ProductCategories::createCategory();
         } catch (\Exception $Exception) {
-            QUI\System\Log::addError(self::class . ' :: createProductCategory()');
+            QUI\System\Log::addError(self::class.' :: createProductCategory()');
             QUI\System\Log::writeException($Exception);
 
             return;
         }
 
         $catId  = $Category->getId();
-        $titles = array(
+        $titles = [
             'de' => '',
             'en' => ''
-        );
+        ];
 
-        $descriptions = array(
+        $descriptions = [
             'de' => '',
             'en' => ''
-        );
+        ];
 
         $L = new QUI\Locale();
 
@@ -248,36 +276,36 @@ protected static function createProductCategory()
             $t = $L->get('quiqqer/memberships', 'products.category.title');
             $d = $L->get('quiqqer/memberships', 'products.category.description');
 
-            $titles[$l]                 = $t;
-            $titles[$l . '_edit']       = $t;
-            $descriptions[$l]           = $d;
-            $descriptions[$l . '_edit'] = $d;
+            $titles[$l]               = $t;
+            $titles[$l.'_edit']       = $t;
+            $descriptions[$l]         = $d;
+            $descriptions[$l.'_edit'] = $d;
         }
 
         // change title and description
         QUI\Translator::edit(
             'quiqqer/products',
-            'products.category.' . $catId . '.title',
+            'products.category.'.$catId.'.title',
             'quiqqer/products',
             array_merge(
                 $titles,
-                array(
+                [
                     'datatype' => 'php,js',
                     'html'     => 0
-                )
+                ]
             )
         );
 
         QUI\Translator::edit(
             'quiqqer/products',
-            'products.category.' . $catId . '.description',
+            'products.category.'.$catId.'.description',
             'quiqqer/products',
             array_merge(
                 $descriptions,
-                array(
+                [
                     'datatype' => 'php,js',
                     'html'     => 0
-                )
+                ]
             )
         );
 
diff --git a/src/QUI/Memberships/Handler.php b/src/QUI/Memberships/Handler.php
index bd88576..620c6c3 100644
--- a/src/QUI/Memberships/Handler.php
+++ b/src/QUI/Memberships/Handler.php
@@ -21,17 +21,11 @@ class Handler extends Factory
     const PERMISSION_DELETE     = 'quiqqer.memberships.delete';
     const PERMISSION_FORCE_EDIT = 'quiqqer.memberships.force_edit';
 
-    /**
-     * quiqqer/products field IDs
-     */
-    const PRODUCTS_FIELD_MEMBERSHIP     = 102;
-    const PRODUCTS_FIELD_MEMBERSHIPFLAG = 103;
-
     /**
      * @inheritdoc
      * @throws QUI\Memberships\Exception
      */
-    public function createChild($data = array())
+    public function createChild($data = [])
     {
         Permission::checkPermission(self::PERMISSION_CREATE);
 
@@ -42,13 +36,13 @@ public function createChild($data = array())
         $title = trim($data['title']);
 
         if (empty($title)) {
-            throw new QUI\Memberships\Exception(array(
+            throw new QUI\Memberships\Exception([
                 'quiqqer/memberships',
                 'exception.handler.no.title'
-            ));
+            ]);
         }
 
-        $data['title'] = array();
+        $data['title'] = [];
 
         foreach (QUI::availableLanguages() as $lang) {
             $data['title'][$lang] = $title;
@@ -63,10 +57,10 @@ public function createChild($data = array())
         if (empty($groupIds)
             || !is_array($groupIds)
         ) {
-            throw new QUI\Memberships\Exception(array(
+            throw new QUI\Memberships\Exception([
                 'quiqqer/memberships',
                 'exception.handler.no.groups'
-            ));
+            ]);
         }
 
         foreach ($groupIds as $groupId) {
@@ -74,7 +68,7 @@ public function createChild($data = array())
             $Groups->get((int)$groupId);
         }
 
-        $data['groupIds'] = ',' . implode(',', $groupIds) . ',';
+        $data['groupIds'] = ','.implode(',', $groupIds).',';
         $data['duration'] = '1-month';
 
         /** @var Membership $NewMembership */
@@ -119,7 +113,7 @@ public function getChildClass()
      */
     public function getChildAttributes()
     {
-        return array(
+        return [
             'title',
             'description',
             'content',
@@ -133,7 +127,7 @@ public function getChildAttributes()
             'paymentInterval'
 
             // @todo additional fields for quiqqer/contracts
-        );
+        ];
     }
 
     /**
@@ -145,12 +139,12 @@ public function getChildAttributes()
      */
     public function search($searchParams, $countOnly = false)
     {
-        $memberships = array();
+        $memberships = [];
         $Grid        = new Grid($searchParams);
         $gridParams  = $Grid->parseDBParams($searchParams);
 
-        $binds = array();
-        $where = array();
+        $binds = [];
+        $where = [];
 
         if ($countOnly) {
             $sql = "SELECT COUNT(*)";
@@ -158,14 +152,14 @@ public function search($searchParams, $countOnly = false)
             $sql = "SELECT id";
         }
 
-        $sql .= " FROM `" . $this->getDataBaseTableName() . "`";
+        $sql .= " FROM `".$this->getDataBaseTableName()."`";
 
         if (!empty($searchParams['userId'])) {
             $memberhsipUsers = MembershipUsersHandler::getInstance()->getMembershipUsersByUserId(
                 (int)$searchParams['userId']
             );
 
-            $membershipIds = array();
+            $membershipIds = [];
 
             /** @var QUI\Memberships\Users\MembershipUser $MembershipUser */
             foreach ($memberhsipUsers as $MembershipUser) {
@@ -173,63 +167,63 @@ public function search($searchParams, $countOnly = false)
             }
 
             if (!empty($membershipIds)) {
-                $where[] = '`id` IN (' . implode(',', $membershipIds) . ')';
+                $where[] = '`id` IN ('.implode(',', $membershipIds).')';
             }
         }
 
         if (!empty($searchParams['search'])) {
-            $searchColumns = array(
+            $searchColumns = [
                 'title',
                 'description',
                 'content'
-            );
+            ];
 
-            $whereOr = array();
+            $whereOr = [];
 
             foreach ($searchColumns as $searchColumn) {
-                $whereOr[] = '`' . $searchColumn . '` LIKE :search';
+                $whereOr[] = '`'.$searchColumn.'` LIKE :search';
             }
 
             if (!empty($whereOr)) {
-                $where[] = '(' . implode(' OR ', $whereOr) . ')';
+                $where[] = '('.implode(' OR ', $whereOr).')';
 
-                $binds['search'] = array(
-                    'value' => '%' . $searchParams['search'] . '%',
+                $binds['search'] = [
+                    'value' => '%'.$searchParams['search'].'%',
                     'type'  => \PDO::PARAM_STR
-                );
+                ];
             }
         }
 
         // build WHERE query string
         if (!empty($where)) {
-            $sql .= " WHERE " . implode(" AND ", $where);
+            $sql .= " WHERE ".implode(" AND ", $where);
         }
 
         // ORDER
         if (!empty($searchParams['sortOn'])
         ) {
             $sortOn = Orthos::clear($searchParams['sortOn']);
-            $order  = "ORDER BY " . $sortOn;
+            $order  = "ORDER BY ".$sortOn;
 
             if (isset($searchParams['sortBy']) &&
                 !empty($searchParams['sortBy'])
             ) {
-                $order .= " " . Orthos::clear($searchParams['sortBy']);
+                $order .= " ".Orthos::clear($searchParams['sortBy']);
             } else {
                 $order .= " ASC";
             }
 
-            $sql .= " " . $order;
+            $sql .= " ".$order;
         }
 
         // LIMIT
         if (!empty($gridParams['limit'])
             && !$countOnly
         ) {
-            $sql .= " LIMIT " . $gridParams['limit'];
+            $sql .= " LIMIT ".$gridParams['limit'];
         } else {
             if (!$countOnly) {
-                $sql .= " LIMIT " . (int)20;
+                $sql .= " LIMIT ".(int)20;
             }
         }
 
@@ -237,7 +231,7 @@ public function search($searchParams, $countOnly = false)
 
         // bind search values
         foreach ($binds as $var => $bind) {
-            $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']);
+            $Stmt->bindValue(':'.$var, $bind['value'], $bind['type']);
         }
 
         try {
@@ -245,10 +239,10 @@ public function search($searchParams, $countOnly = false)
             $result = $Stmt->fetchAll(\PDO::FETCH_ASSOC);
         } catch (\Exception $Exception) {
             QUI\System\Log::addError(
-                self::class . ' :: searchUsers() -> ' . $Exception->getMessage()
+                self::class.' :: searchUsers() -> '.$Exception->getMessage()
             );
 
-            return array();
+            return [];
         }
 
         if ($countOnly) {
@@ -270,24 +264,24 @@ public function search($searchParams, $countOnly = false)
      */
     public function getMembershipIdsByGroupIds($groupIds)
     {
-        $ids = array();
+        $ids = [];
 
         if (empty($groupIds)) {
             return $ids;
         }
 
-        $sql = 'SELECT `id` FROM ' . self::getDataBaseTableName();
+        $sql = 'SELECT `id` FROM '.self::getDataBaseTableName();
         $sql .= ' WHERE ';
 
-        $whereOr = array();
-        $binds   = array();
+        $whereOr = [];
+        $binds   = [];
 
         foreach ($groupIds as $groupId) {
-            $whereOr[]       = '`groupIds` LIKE :' . $groupId;
-            $binds[$groupId] = array(
-                'value' => '%,' . $groupId . ',%',
+            $whereOr[]       = '`groupIds` LIKE :'.$groupId;
+            $binds[$groupId] = [
+                'value' => '%,'.$groupId.',%',
                 'type'  => \PDO::PARAM_INT
-            );
+            ];
         }
 
         $sql .= implode(" OR ", $whereOr);
@@ -297,7 +291,7 @@ public function getMembershipIdsByGroupIds($groupIds)
 
         // bind search values
         foreach ($binds as $var => $bind) {
-            $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']);
+            $Stmt->bindValue(':'.$var, $bind['value'], $bind['type']);
         }
 
         try {
@@ -305,7 +299,7 @@ public function getMembershipIdsByGroupIds($groupIds)
             $result = $Stmt->fetchAll(\PDO::FETCH_ASSOC);
         } catch (\Exception $Exception) {
             QUI\System\Log::writeException($Exception);
-            return array();
+            return [];
         }
 
         foreach ($result as $row) {
@@ -347,7 +341,7 @@ public static function getProductCategory()
             return ProductCategories::getCategory((int)$categoryId);
         } catch (\Exception $Exception) {
             if ($Exception->getCode() !== 404) {
-                QUI\System\Log::addError(self::class . ' :: getProductCategory()');
+                QUI\System\Log::addError(self::class.' :: getProductCategory()');
                 QUI\System\Log::writeException($Exception);
             }
 
@@ -369,11 +363,16 @@ public static function getProductMembershipField()
         }
 
         try {
-            return ProductFields::getField(self::PRODUCTS_FIELD_MEMBERSHIP);
+            $Conf    = QUI::getPackage('quiqqer/memberships')->getConfig();
+            $fieldId = $Conf->get('products', 'membershipFieldId');
+
+            if (empty($fieldId)) {
+                return false;
+            }
+
+            return ProductFields::getField($fieldId);
         } catch (\Exception $Exception) {
-            QUI\System\Log::addError(self::class . ' :: getProductMembershipField()');
             QUI\System\Log::writeException($Exception);
-
             return false;
         }
     }
@@ -392,11 +391,16 @@ public static function getProductMembershipFlagField()
         }
 
         try {
-            return ProductFields::getField(self::PRODUCTS_FIELD_MEMBERSHIPFLAG);
+            $Conf    = QUI::getPackage('quiqqer/memberships')->getConfig();
+            $fieldId = $Conf->get('products', 'membershipFlagFieldId');
+
+            if (empty($fieldId)) {
+                return false;
+            }
+
+            return ProductFields::getField($fieldId);
         } catch (\Exception $Exception) {
-            QUI\System\Log::addError(self::class . ' :: getProductMembershipFlagField()');
             QUI\System\Log::writeException($Exception);
-
             return false;
         }
     }
diff --git a/src/QUI/Memberships/Membership.php b/src/QUI/Memberships/Membership.php
index f86c83f..8739df5 100644
--- a/src/QUI/Memberships/Membership.php
+++ b/src/QUI/Memberships/Membership.php
@@ -114,13 +114,13 @@ public function update()
         // check groups
         if (empty($attributes['groupIds'])
         ) {
-            throw new QUI\Memberships\Exception(array(
+            throw new QUI\Memberships\Exception([
                 'quiqqer/memberships',
                 'exception.handler.no.groups'
-            ));
+            ]);
         }
 
-        $attributes['groupIds'] = ',' . $attributes['groupIds'] . ',';
+        $attributes['groupIds'] = ','.$attributes['groupIds'].',';
 
         // check duration
         if (empty($attributes['duration'])
@@ -131,10 +131,10 @@ public function update()
             $duration = explode('-', $attributes['duration']);
 
             if ($duration[0] < 1) {
-                throw new QUI\Memberships\Exception(array(
+                throw new QUI\Memberships\Exception([
                     'quiqqer/memberships',
                     'exception.membership.update.duration.invalid'
-                ));
+                ]);
             }
         }
 
@@ -150,10 +150,12 @@ public function update()
     /**
      * Delete membership
      *
-     * Only possible if membership has no users in it
+     * Only possible if membership has no users in it!
      *
      * @return void
-     * @throws QUI\Memberships\Exception
+     * @throws \QUI\Memberships\Exception
+     * @throws \QUI\Permissions\Exception
+     * @throws \QUI\Exception
      */
     public function delete()
     {
@@ -162,10 +164,10 @@ public function delete()
         $MembershipUsers = MembershipUsersHandler::getInstance();
 
         if (count($MembershipUsers->getIdsByMembershipId($this->id))) {
-            throw new Exception(array(
+            throw new Exception([
                 'quiqqer/memberships',
                 'exception.membership.cannot.delete.with.users.left'
-            ));
+            ]);
         }
 
         if ($this->isDefault()) {
@@ -178,8 +180,16 @@ public function delete()
         if (Utils::isQuiqqerProductsInstalled()) {
             /** @var QUI\ERP\Products\Product\Product $Product */
             foreach ($this->getProducts() as $Product) {
-                $MembershipField = $Product->getField(Handler::PRODUCTS_FIELD_MEMBERSHIP);
+                $MembershipField = $Product->getField(
+                    Handler::getProductMembershipField()->getId()
+                );
                 $MembershipField->setValue(null);
+
+                $MembershipFlagField = $Product->getField(
+                    Handler::getProductMembershipFlagField()->getId()
+                );
+                $MembershipFlagField->setValue(null);
+
                 $Product->deactivate();
                 $Product->save();
             }
@@ -201,26 +211,26 @@ public function delete()
      */
     public function getMembershipUser($userId)
     {
-        $result = QUI::getDataBase()->fetch(array(
-            'select' => array(
+        $result = QUI::getDataBase()->fetch([
+            'select' => [
                 'id'
-            ),
+            ],
             'from'   => MembershipUsersHandler::getInstance()->getDataBaseTableName(),
-            'where'  => array(
+            'where'  => [
                 'membershipId' => $this->id,
                 'userId'       => $userId,
                 'archived'     => 0
-            )
-        ));
+            ]
+        ]);
 
         if (empty($result)) {
-            throw new Exception(array(
+            throw new Exception([
                 'quiqqer/memberships',
                 'exception.membership.user.not.found',
-                array(
+                [
                     'userId' => $userId
-                )
-            ), 404);
+                ]
+            ], 404);
         }
 
         return MembershipUsersHandler::getInstance()->getChild($result[0]['id']);
@@ -266,18 +276,18 @@ public function getUniqueGroupIds()
      */
     public function hasMembershipUserId($userId)
     {
-        $result = QUI::getDataBase()->fetch(array(
+        $result = QUI::getDataBase()->fetch([
             'count'  => 1,
-            'select' => array(
+            'select' => [
                 'id'
-            ),
+            ],
             'from'   => MembershipUsersHandler::getInstance()->getDataBaseTableName(),
-            'where'  => array(
+            'where'  => [
                 'membershipId' => $this->id,
                 'userId'       => $userId,
                 'archived'     => 0
-            )
-        ));
+            ]
+        ]);
 
         return current(current($result)) > 0;
     }
@@ -292,13 +302,13 @@ public function hasMembershipUserId($userId)
      */
     public function searchUsers($searchParams, $archivedOnly = false, $countOnly = false)
     {
-        $membershipUserIds = array();
+        $membershipUserIds = [];
         $Grid              = new QUI\Utils\Grid($searchParams);
         $gridParams        = $Grid->parseDBParams($searchParams);
         $tbl               = MembershipUsersHandler::getInstance()->getDataBaseTableName();
         $usersTbl          = QUI::getDBTableName('users');
-        $binds             = array();
-        $where             = array();
+        $binds             = [];
+        $where             = [];
 
         if ($countOnly) {
             $sql = "SELECT COUNT(*)";
@@ -306,10 +316,10 @@ public function searchUsers($searchParams, $archivedOnly = false, $countOnly = f
             $sql = "SELECT `musers`.id";
         }
 
-        $sql .= " FROM `" . $tbl . "` musers, `" . $usersTbl . "` users";
+        $sql .= " FROM `".$tbl."` musers, `".$usersTbl."` users";
 
         $where[] = '`musers`.userId = `users`.id';
-        $where[] = '`musers`.membershipId = ' . $this->id;
+        $where[] = '`musers`.membershipId = '.$this->id;
 
         if ($archivedOnly === false) {
             $where[] = '`musers`.archived = 0';
@@ -318,36 +328,36 @@ public function searchUsers($searchParams, $archivedOnly = false, $countOnly = f
         }
 
         if (!empty($searchParams['search'])) {
-            $whereOR = array();
+            $whereOR = [];
 
-            $searchColumns = array(
+            $searchColumns = [
                 '`users`.username',
                 '`users`.firstname',
                 '`users`.lastname'
-            );
+            ];
 
             foreach ($searchColumns as $tbl => $column) {
-                $whereOR[]       = $column . ' LIKE :search';
-                $binds['search'] = array(
-                    'value' => '%' . $searchParams['search'] . '%',
+                $whereOR[]       = $column.' LIKE :search';
+                $binds['search'] = [
+                    'value' => '%'.$searchParams['search'].'%',
                     'type'  => \PDO::PARAM_STR
-                );
+                ];
             }
 
-            $where[] = '(' . implode(' OR ', $whereOR) . ')';
+            $where[] = '('.implode(' OR ', $whereOR).')';
         }
 
         if (!empty($searchParams['productId'])) {
             $where[]            = '`musers`.productId = :productId';
-            $binds['productId'] = array(
+            $binds['productId'] = [
                 'value' => (int)$searchParams['productId'],
                 'type'  => \PDO::PARAM_INT
-            );
+            ];
         }
 
         // build WHERE query string
         if (!empty($where)) {
-            $sql .= " WHERE " . implode(" AND ", $where);
+            $sql .= " WHERE ".implode(" AND ", $where);
         }
 
         // ORDER
@@ -359,34 +369,34 @@ public function searchUsers($searchParams, $archivedOnly = false, $countOnly = f
                 case 'username':
                 case 'firstname':
                 case 'lastname':
-                    $sortOn = '`users`.' . $sortOn;
+                    $sortOn = '`users`.'.$sortOn;
                     break;
 
                 default:
-                    $sortOn = '`musers`.' . $sortOn;
+                    $sortOn = '`musers`.'.$sortOn;
             }
 
-            $order = "ORDER BY " . $sortOn;
+            $order = "ORDER BY ".$sortOn;
 
             if (isset($searchParams['sortBy']) &&
                 !empty($searchParams['sortBy'])
             ) {
-                $order .= " " . Orthos::clear($searchParams['sortBy']);
+                $order .= " ".Orthos::clear($searchParams['sortBy']);
             } else {
                 $order .= " ASC";
             }
 
-            $sql .= " " . $order;
+            $sql .= " ".$order;
         }
 
         // LIMIT
         if (!empty($gridParams['limit'])
             && !$countOnly
         ) {
-            $sql .= " LIMIT " . $gridParams['limit'];
+            $sql .= " LIMIT ".$gridParams['limit'];
         } else {
             if (!$countOnly) {
-                $sql .= " LIMIT " . (int)20;
+                $sql .= " LIMIT ".(int)20;
             }
         }
 
@@ -394,7 +404,7 @@ public function searchUsers($searchParams, $archivedOnly = false, $countOnly = f
 
         // bind search values
         foreach ($binds as $var => $bind) {
-            $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']);
+            $Stmt->bindValue(':'.$var, $bind['value'], $bind['type']);
         }
 
         try {
@@ -402,10 +412,10 @@ public function searchUsers($searchParams, $archivedOnly = false, $countOnly = f
             $result = $Stmt->fetchAll(\PDO::FETCH_ASSOC);
         } catch (\Exception $Exception) {
             QUI\System\Log::addError(
-                self::class . ' :: searchUsers() -> ' . $Exception->getMessage()
+                self::class.' :: searchUsers() -> '.$Exception->getMessage()
             );
 
-            return array();
+            return [];
         }
 
         if ($countOnly) {
@@ -445,13 +455,13 @@ public function calcEndDate($start = null)
 
         switch ($durationMode) {
             case 'day':
-                $endTime    = strtotime($start . ' +' . $durationCount . ' ' . $durationScope);
+                $endTime    = strtotime($start.' +'.$durationCount.' '.$durationScope);
                 $beginOfDay = strtotime("midnight", $endTime);
                 $end        = strtotime("tomorrow", $beginOfDay) - 1;
                 break;
 
             default:
-                $end = strtotime($start . ' +' . $durationCount . ' ' . $durationScope);
+                $end = strtotime($start.' +'.$durationCount.' '.$durationScope);
         }
 
         return Utils::getFormattedTimestamp($end);
@@ -467,25 +477,35 @@ public function calcEndDate($start = null)
     public function getProducts()
     {
         if (!Utils::isQuiqqerProductsInstalled()) {
-            return array();
+            return [];
         }
 
-        $Search = new BackendSearch();
-
         try {
-            $result = $Search->search(array(
-                'fields' => array(
-                    Handler::PRODUCTS_FIELD_MEMBERSHIP => "$this->id" // has to be string
-                )
-            ));
-        } catch (QUI\Permissions\Exception $Exception) {
-            return array();
+            $Search          = new BackendSearch();
+            $MembershipField = Handler::getProductMembershipField();
+
+            if ($MembershipField === false) {
+                return [];
+            }
+
+            $result = $Search->search([
+                'fields' => [
+                    $MembershipField->getId() => "$this->id" // has to be string
+                ]
+            ]);
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
+            return [];
         }
 
-        $products = array();
+        $products = [];
 
         foreach ($result as $id) {
-            $products[] = ProductsHandler::getProduct($id);
+            try {
+                $products[] = ProductsHandler::getProduct($id);
+            } catch (\Exception $Exception) {
+                QUI\System\Log::writeException($Exception);
+            }
         }
 
         return $products;
@@ -507,8 +527,8 @@ public function createProduct()
             return false;
         }
 
-        $categories = array();
-        $fields     = array();
+        $categories = [];
+        $fields     = [];
 
         $Category = Handler::getProductCategory();
 
@@ -598,7 +618,7 @@ public function isLocked()
      */
     protected function getLockKey()
     {
-        return 'membership_' . $this->id;
+        return 'membership_'.$this->id;
     }
 
     /**
@@ -608,12 +628,12 @@ protected function getLockKey()
      */
     public function getBackendViewData()
     {
-        return array(
+        return [
             'id'          => $this->getId(),
             'title'       => $this->getTitle(),
             'description' => $this->getDescription(),
             'content'     => $this->getContent()
-        );
+        ];
     }
 
     /**
@@ -650,9 +670,9 @@ public function isDefault()
      */
     public function addUser(QUI\Users\User $User)
     {
-        return MembershipUsersHandler::getInstance()->createChild(array(
+        return MembershipUsersHandler::getInstance()->createChild([
             'userId'       => $User->getId(),
             'membershipId' => $this->id
-        ));
+        ]);
     }
 }
diff --git a/src/QUI/Memberships/Users/AbortCancelVerification.php b/src/QUI/Memberships/Users/AbortCancelVerification.php
index 5e8ff8b..3efc636 100644
--- a/src/QUI/Memberships/Users/AbortCancelVerification.php
+++ b/src/QUI/Memberships/Users/AbortCancelVerification.php
@@ -18,6 +18,7 @@ class AbortCancelVerification extends QUI\Verification\AbstractVerification
      *
      * @return int|false - duration in minutes;
      * if this method returns false use the module setting default value
+     * @throws \QUI\Exception
      */
     public function getValidDuration()
     {
@@ -33,6 +34,7 @@ public function getValidDuration()
      * Execute this method on successful verification
      *
      * @return void
+     * @throws \QUI\Exception
      */
     public function onSuccess()
     {
@@ -55,28 +57,32 @@ public function onError()
      * This message is displayed to the user on successful verification
      *
      * @return string
+     * @throws \QUI\Exception
      */
     public function getSuccessMessage()
     {
         /** @var MembershipUser $MembershipUser */
         $MembershipUser = MembershipUsersHandler::getInstance()->getChild($this->getIdentifier());
+        $Membership     = $MembershipUser->getMembership();
         $data           = $MembershipUser->getFrontendViewData();
 
-        if ($MembershipUser->getMembership()->isAutoExtend()) {
+        if ($Membership->isAutoExtend()) {
             $msg = QUI::getLocale()->get(
                 'quiqqer/memberships',
                 'verification.abortcancel.success.autoExtend',
-                array(
-                    'endDate' => $data['endDate']
-                )
+                [
+                    'endDate'         => $data['endDate'],
+                    'membershipTitle' => $Membership->getTitle()
+                ]
             );
         } else {
             $msg = QUI::getLocale()->get(
                 'quiqqer/memberships',
                 'verification.abortcancel.success.noAutoExtend',
-                array(
-                    'endDate' => $data['endDate']
-                )
+                [
+                    'endDate'         => $data['endDate'],
+                    'membershipTitle' => $Membership->getTitle()
+                ]
             );
         }
 
diff --git a/src/QUI/Memberships/Users/MembershipUser.php b/src/QUI/Memberships/Users/MembershipUser.php
index 0293b56..86a85b1 100644
--- a/src/QUI/Memberships/Users/MembershipUser.php
+++ b/src/QUI/Memberships/Users/MembershipUser.php
@@ -475,6 +475,7 @@ public function getUserId()
      * Get QUIQQER User
      *
      * @return QUI\Users\User
+     * @throws \QUI\Exception
      */
     public function getUser()
     {
@@ -534,6 +535,7 @@ public function getHistory()
      *
      * @param string $date - Formatted date YYYY-MM-DD HH:MM:SS
      * @return string|false - formatted date or false on error
+     * @throws \QUI\Exception
      */
     protected function formatDate($date)
     {
-- 
GitLab