1
0
Эх сурвалжийг харах

Ticket #1904 - Core changes for 'Paid Join'

AntonLV 6 жил өмнө
parent
commit
09e4548857

+ 0 - 1
modules/base/general/classes/BxBaseModGeneralDb.php

@@ -51,7 +51,6 @@ class BxBaseModGeneralDb extends BxDolModuleDb
                 break;
 
             case 'author':
-            	$aMethod['name'] = 'getRow';
                 $aMethod['params'][1]['author'] = (int)$aParams['author'];
 
                 $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_AUTHOR'] . "` = :author";

+ 34 - 0
modules/base/groups/classes/BxBaseModGroupsConfig.php

@@ -11,11 +11,16 @@
 
 class BxBaseModGroupsConfig extends BxBaseModProfileConfig
 {
+    protected $_aHtmlIds;
+
+    protected $_aCurrency;
+
     function __construct($aModule)
     {
         parent::__construct($aModule);
 
         $this->_aMenuItems2MethodsActions = array (
+            'join-group-profile' => 'checkAllowedFanAdd',
             'profile-fan-add' => 'checkAllowedFanAdd',
             'profile-fan-remove' => 'checkAllowedFanRemove',
             'profile-subscribe-add' => 'checkAllowedSubscribeAdd',
@@ -23,6 +28,35 @@ class BxBaseModGroupsConfig extends BxBaseModProfileConfig
             'profile-actions-more' => 'checkAllowedViewMoreMenu',
             'convos-compose' => 'checkAllowedCompose',
         );
+
+        $sHtmlPrefix = str_replace('_', '-', $this->_sName);
+        $this->_aHtmlIds = array(
+            'popup_price' => $sHtmlPrefix . '-popup-price'
+        );
+
+        $oPayments = BxDolPayments::getInstance();
+        $this->_aCurrency = array(
+            'code' => $oPayments->getOption('default_currency_code'),
+            'sign' => $oPayments->getOption('default_currency_sign')
+        );
+    }
+
+    public function getHtmlIds($sKey = '')
+    {
+        if(empty($sKey))
+            return $this->_aHtmlIds;
+
+        return isset($this->_aHtmlIds[$sKey]) ? $this->_aHtmlIds[$sKey] : '';
+    }
+
+    public function getCurrency()
+    {
+    	return $this->_aCurrency;
+    }
+
+    public function isPaidJoin()
+    {
+        return !empty($this->CNF['PARAM_PER_PAID_JOIN']) && getParam($this->CNF['PARAM_PER_PAID_JOIN']) == 'on';
     }
 }
 

+ 54 - 0
modules/base/groups/classes/BxBaseModGroupsCronPruning.php

@@ -0,0 +1,54 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsCronPruning extends BxDolCron
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    public function __construct()
+    {
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct();
+    }
+
+    function processing()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $oPayments = BxDolPayments::getInstance();
+
+        $aRoles = $this->_oModule->_oDb->getRoles(array('type' => 'expired'));
+        foreach($aRoles as $aRole) {
+            $aSubscription = $oPayments->getSubscriptionsInfo(array('subscription_id' => $aRole['order']), true);
+            if(!empty($aSubscription) && is_array($aSubscription)) {
+                $aSubscription = array_shift($aSubscription);
+
+                $iRecurringReserve = 86400 * (int)getParam($CNF['PARAM_RECURRING_RESERVE']);
+                if($aSubscription['data']['status'] == 'active') {
+                    $this->_oModule->_oDb->updateRoles(array('expired' => $aSubscription['data']['cperiod_end'] + $iRecurringReserve), array('id' => $aRole['id']));
+                    continue;
+                }
+                else {
+                    $oPayments->sendSubscriptionExpirationLetters($aSubscription['pending_id'], $aRole['order']);
+
+                    $this->_oModule->_oDb->updateRoles(array('expired' => $aRole['expired'] + $iRecurringReserve), array('id' => $aRole['id']));
+                    continue;
+                }
+            }
+
+            $this->_oModule->unsetRole($aRole['group_profile_id'], $aRole['fan_id']);
+        }
+    }
+}
+
+/** @} */

+ 227 - 30
modules/base/groups/classes/BxBaseModGroupsDb.php

@@ -88,16 +88,28 @@ class BxBaseModGroupsDb extends BxBaseModProfileDb
 
     public function isAdmin ($iGroupProfileId, $iFanId, $aDataEntry = array())
     {
-        if (isset($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']]) && $iFanId == $aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']])
+        $CNF = &$this->_oConfig->CNF;
+
+        if(isset($aDataEntry[$CNF['FIELD_AUTHOR']]) && $iFanId == $aDataEntry[$CNF['FIELD_AUTHOR']])
             return true;
-        if (!isset($this->_oConfig->CNF['TABLE_ADMINS']))
+
+        if(!isset($CNF['TABLE_ADMINS']))
             return false;
-        $sQuery = $this->prepare("SELECT `id` FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ? AND `fan_id` = ?", $iGroupProfileId, $iFanId);
-        return $this->getOne($sQuery) ? true : false;
+
+        $aBindings = array('group_profile_id' => $iGroupProfileId, 'fan_id' => $iFanId);
+        $sWhereClause = " AND `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id";
+        if($this->_oConfig->isPaidJoin()) {
+            $aBindings['role'] = 'BX_BASE_MOD_GROUPS_ROLE_COMMON';
+            $sWhereClause .= " AND `role` <> :role";
+        }
+
+        return $this->getOne("SELECT `id` FROM `" . $CNF['TABLE_ADMINS'] . "` WHERE 1" . $sWhereClause, $aBindings) ? true : false;
     }
 
     public function getAdmin ($iGroupProfileId, $iProfileId, $bRoleOnly = false)
     {
+        $CNF = &$this->_oConfig->CNF;
+
         $sMethod = 'getRow';
         $sSelectClause = '*';
 
@@ -106,45 +118,230 @@ class BxBaseModGroupsDb extends BxBaseModProfileDb
             $sSelectClause = '`role`';
         }
 
-        return $this->$sMethod("SELECT " . $sSelectClause . " FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id LIMIT 1", array(
-            'group_profile_id' => $iGroupProfileId,
-            'fan_id' => $iProfileId
-        ));
+        $aBindings = array('group_profile_id' => $iGroupProfileId, 'fan_id' => $iProfileId);
+        $sWhereClause = " AND `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id";
+        if($this->_oConfig->isPaidJoin()) {
+            $aBindings['role'] = 'BX_BASE_MOD_GROUPS_ROLE_COMMON';
+            $sWhereClause .= " AND `role` <> :role";
+        }
+
+        return $this->$sMethod("SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_ADMINS'] . "` WHERE 1" . $sWhereClause . " LIMIT 1", $aBindings);
     }
 
     public function getAdmins ($iGroupProfileId, $iStart = 0, $iLimit = 0)
     {
-        if ($iLimit > 0)
-            $sQuery = $this->prepare("SELECT `fan_id` FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ? LIMIT ?, ?", $iGroupProfileId, $iStart, $iLimit);
-        else
-            $sQuery = $this->prepare("SELECT `fan_id` FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ?", $iGroupProfileId);
+        $CNF = &$this->_oConfig->CNF;
+
+        $aBindings = array('group_profile_id' => $iGroupProfileId);
+        $sWhereClause = " AND `group_profile_id` = :group_profile_id";
+        if($this->_oConfig->isPaidJoin()) {
+            $aBindings['role'] = 'BX_BASE_MOD_GROUPS_ROLE_COMMON';
+            $sWhereClause .= " AND `role` <> :role";
+        }
+
+        $sLimitClause = "";
+        if($iLimit > 0)
+            $sLimitClause = " LIMIT " . (int)$iStart . ", " . (int)$iLimit;
+
+        return $this->getColumn("SELECT `fan_id` FROM `" . $CNF['TABLE_ADMINS'] . "` WHERE 1" . $sWhereClause . $sLimitClause, $aBindings);
+    }
+
+    public function getRole($iGroupProfileId, $iProfileId)
+    {
+        return (int)$this->getRoles(array('type' => 'by_gf_id', 'group_profile_id' => $iGroupProfileId, 'fan_id' => $iProfileId, 'role_only' => true));
+    }
+
+    public function getRoles($aParams)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`ta`.*";
+        $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `ta`.`id`=:id";
+                break;
+
+            case 'by_gf_id':
+                $aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = array(
+                    'group_profile_id' => $aParams['group_profile_id'],
+                    'fan_id' => $aParams['fan_id'],
+                );
+
+                $sWhereClause = "AND `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id";
+                $sLimitClause = 1;
+
+                if(!empty($aParams['role_only'])) {
+                    $aMethod['name'] = 'getOne';
+                    $sSelectClause = "`ta`.`role`";
+                }
+                break;
+
+            case 'expired':
+                $sWhereClause .= "AND `ta`.`added` < UNIX_TIMESTAMP() AND `ta`.`expired` <> 0 AND `ta`.`expired` < UNIX_TIMESTAMP()";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = "ORDER BY " . $sOrderClause;
+        
+        if(!empty($sLimitClause))
+            $sLimitClause = "LIMIT " . $sLimitClause;
+
+        $sSql = "SELECT {select} FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` AS `ta` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " {order} {limit}";
+
+        $aMethod['params'][0] = str_replace(array('{select}', '{order}', '{limit}'), array($sSelectClause, $sOrderClause, $sLimitClause), $sSql);
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function updateRoles($aSet, $aWhere)
+    {
+        $sWhereClause = 1;
+        if(!empty($aWhere))
+            $sWhereClause = $this->arrayToSQL($aWhere, ' AND ');
+
+        return (int)$this->query("UPDATE `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` SET " . $this->arrayToSQL($aSet) . " WHERE " . $sWhereClause) > 0;
+    }
+    
+    public function deleteRoles($aWhere)
+    {
+    	if(empty($aWhere))
+            return false;
 
-        return $this->getColumn($sQuery);
+        return (int)$this->query("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE " . $this->arrayToSQL($aWhere, ' AND ')) > 0;
     }
 
-    public function getRole ($iGroupProfileId, $iProfileId)
+    public function setRole ($iGroupProfileId, $iProfileId, $iRole, $mixedPeriod = false, $sOrder = '')
     {
-        return (int)$this->getOne("SELECT `role` FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id LIMIT 1", array(
+        $CNF = &$this->_oConfig->CNF;
+
+        $iNow = time();
+
+        $aBindings = array(
             'group_profile_id' => $iGroupProfileId,
-            'fan_id' => $iProfileId
-        ));
+            'fan_id' => $iProfileId,
+            'role' => $iRole,
+            'order' => $sOrder,
+            'added' => $iNow
+        );
+        $sSetClause = "`group_profile_id` = :group_profile_id, `fan_id` = :fan_id, `role` = :role, `added` = :added";
+
+        $sSetExpired = ""; 
+        if(!empty($mixedPeriod)) {
+            if(is_numeric($mixedPeriod))
+        	$mixedPeriod = array('period' => (int)$mixedPeriod, 'period_unit' => BX_BASE_MOD_GROUPS_PERIOD_UNIT_DAY);
+
+            $aBindings['period'] = (int)$mixedPeriod['period'];
+
+            switch($mixedPeriod['period_unit']) {
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_DAY:
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_WEEK:
+                    if($mixedPeriod['period_unit'] == BX_BASE_MOD_GROUPS_PERIOD_UNIT_WEEK)
+                        $aBindings['period'] *= 7;
+
+                    $sSetExpired = "DATE_ADD(FROM_UNIXTIME(:added), INTERVAL :period DAY)";
+                    break;
+
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_MONTH:
+                    $sSetExpired = "DATE_ADD(FROM_UNIXTIME(:added), INTERVAL :period MONTH)";
+                    break;
+
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_YEAR:
+                    $sSetExpired = "DATE_ADD(FROM_UNIXTIME(:added), INTERVAL :period YEAR)";
+                    break;
+            }
+
+            if(!empty($sSetExpired) && !empty($mixedPeriod['period_reserve'])) {
+                $aBindings['reserve'] = (int)$mixedPeriod['period_reserve'];
+
+                $sSetExpired = "DATE_ADD(" . $sSetExpired . ", INTERVAL :reserve DAY)";
+            }
+
+            if(!empty($sSetExpired))
+                $sSetClause .= ", `expired` = UNIX_TIMESTAMP(" . $sSetExpired . ")";
+        }
+
+        return !$this->query("REPLACE INTO `" . $CNF['TABLE_ADMINS'] . "` SET " . $sSetClause, $aBindings) ? false : true;
     }
 
-    public function setRole ($iGroupProfileId, $iProfileId, $iRole)
+    public function unsetRole($iGroupProfileId, $iProfileId)
     {
-        if($iRole == 0)
-            $mixedResult = $this->query("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id", array(
-                'group_profile_id' => $iGroupProfileId,
-                'fan_id' => $iProfileId,
-            ));
-        else
-            $mixedResult = $this->query("REPLACE INTO `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` SET `group_profile_id` = :group_profile_id, `fan_id` = :fan_id, `role` = :role", array(
-                'group_profile_id' => $iGroupProfileId,
-                'fan_id' => $iProfileId,
-                'role' => $iRole
-            ));
+        return $this->deleteRoles(array('group_profile_id' => $iGroupProfileId, 'fan_id' => $iProfileId));
+    }
 
-        return !$mixedResult ? false : true;
+    public function getPrices($aParams)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`tp`.*";
+        $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+            $sOrderClause = "ORDER BY `tp`.`Order` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause .= "AND `tp`.`id`=:id";
+                break;
+
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause .= "AND `tp`.`name`=:name";
+                break;
+
+            case 'by_profile_id':
+            	$aMethod['params'][1] = array(
+                    'profile_id' => $aParams['profile_id']
+                );
+
+                $sWhereClause .= "AND `tp`.`profile_id`=:profile_id";
+                break;
+
+            case 'by_prpp':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'profile_id' => $aParams['profile_id'],
+                    'role_id' => $aParams['role_id'],
+                    'period' => $aParams['period'],
+                    'period_unit' => $aParams['period_unit'],
+                );
+
+                $sWhereClause .= "AND `tp`.`profile_id`=:profile_id AND `tp`.`role_id`=:role_id AND `tp`.`period`=:period AND `tp`.`period_unit`=:period_unit";
+                break;
+        }
+
+        $sSql = "SELECT {select} FROM `" . $this->_oConfig->CNF['TABLE_PRICES'] . "` AS `tp` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " {order} {limit}";
+
+        $aMethod['params'][0] = str_replace(array('{select}', '{order}', '{limit}'), array($sSelectClause, $sOrderClause, $sLimitClause), $sSql);
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function getPriceOrderMax($iRoleId)
+    {
+        return (int)$this->getOne("SELECT MAX(`order`) FROM `" . $this->_oConfig->CNF['TABLE_PRICES'] . "` WHERE `role_id`=:role_id", array(
+            'role_id' => $iRoleId
+        ));
+    }
+
+    public function deletePrices($aWhere)
+    {
+        return (int)$this->query("DELETE FROM `" . $this->_oConfig->CNF['TABLE_PRICES'] . "` WHERE " . $this->arrayToSQL($aWhere, " AND ")) > 0;
     }
 
     public function insertInvite($sKey, $sGroupProfileId, $iAuthorProfileId, $iInvitedProfileId)

+ 69 - 8
modules/base/groups/classes/BxBaseModGroupsGridConnections.php

@@ -20,10 +20,17 @@ class BxBaseModGroupsGridConnections extends BxDolGridConnections
     protected $_iGroupProfileId;
     protected $_aContentInfo = array();
 
+    protected $_bPaidJoin;
+    protected $_bMember;
+    protected $_bManageMembers;
+
     public function __construct ($aOptions, $oTemplate = false)
     {
         $this->_oModule = BxDolModule::getInstance($this->_sContentModule);
-        $this->_sObjectConnections = $this->_oModule->_oConfig->CNF['OBJECT_CONNECTIONS'];
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_sObjectConnections = $CNF['OBJECT_CONNECTIONS'];
 
         parent::__construct ($aOptions, $oTemplate);
         if(!$this->_bInit) 
@@ -38,6 +45,11 @@ class BxBaseModGroupsGridConnections extends BxDolGridConnections
         if($this->_oModule->checkAllowedEdit($this->_aContentInfo) === CHECK_ACTION_RESULT_ALLOWED || $this->_iGroupProfileId == bx_get_logged_profile_id())
             $this->_bOwner = true;
 
+        $this->_bPaidJoin = $this->_oModule->isPaidJoinByProfile($this->_iGroupProfileId);
+
+        $this->_bMember = $this->_oModule->isFan($this->_aContentInfo[$CNF['FIELD_ID']]);
+        $this->_bManageMembers = $this->_oModule->checkAllowedManageFans($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED || $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED;
+
         $aSQLParts = $this->_oConnection->getConnectedInitiatorsAsSQLParts('p', 'id', $this->_iGroupProfileId, $this->_bOwner ? false : true);
         $this->addMarkers(array(
             'profile_id' => $this->_iGroupProfileId,
@@ -65,8 +77,8 @@ class BxBaseModGroupsGridConnections extends BxDolGridConnections
         if(!$this->_bRoles)
             return echoJson(array());
 
-        if($this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
-            return echoJson(array('msg' => _t('_sys_txt_access_denied')));
+        if(($mixedResult = $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson(array('msg' => $mixedResult));
 
         list($iId, $iViewedId) = $this->_prepareIds();
         if(!$iId)
@@ -105,17 +117,16 @@ class BxBaseModGroupsGridConnections extends BxDolGridConnections
         if(!$this->_bRoles)
             return echoJson(array());
 
-        if($this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
-            return echoJson(array('msg' => _t('_sys_txt_access_denied')));
+        if(($mixedResult = $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson(array('msg' => $mixedResult));
 
         list($iId, $iViewedId) = $this->_prepareIds();
 
         if(!$iId || bx_get('role') === false)
             return echoJson(array('msg' => _t('_sys_txt_error_occured')));
 
-        $aResult = $this->_oModule->setRole($this->_iGroupProfileId, $iId, (int)bx_get('role'));
-        if($aResult['code'] != 0)
-            return echoJson(array('msg' => $aResult['msg']));
+        if(!$this->_oModule->setRole($this->_iGroupProfileId, $iId, (int)bx_get('role')))
+            return echoJson(array('msg' => _t('_error occured')));
 
         echoJson(array('grid' => $this->getCode(false), 'blink' => $iId));
     }
@@ -131,6 +142,56 @@ class BxBaseModGroupsGridConnections extends BxDolGridConnections
         return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
     }
 
+    protected function _getCellHeaderRoleAdded($sKey, $aField)
+    {
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return '';
+
+        return parent::_getCellHeaderDefault ($sKey, $aField);
+    }
+
+    protected function _getCellRoleAdded($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = '';
+
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return $mixedValue;
+
+        $iProfileId = (int)$aRow[$this->_aOptions['field_id']];
+        if($this->_bManageMembers || $iProfileId == bx_get_logged_profile_id()) {
+            $aRole = $this->_oModule->_oDb->getRoles(array('type' => 'by_gf_id', 'group_profile_id' => $this->_iGroupProfileId, 'fan_id' => $iProfileId));
+            if(!empty($aRole) && is_array($aRole))
+                $mixedValue = bx_time_js($aRole['added'], BX_FORMAT_DATE, true);
+        }
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellHeaderRoleExpired($sKey, $aField)
+    {
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return '';
+        
+        return parent::_getCellHeaderDefault ($sKey, $aField);
+    }
+
+    protected function _getCellRoleExpired($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = '';
+
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return '';
+
+        $iProfileId = (int)$aRow[$this->_aOptions['field_id']];
+        if($this->_bManageMembers || $iProfileId == bx_get_logged_profile_id()) {
+            $aRole = $this->_oModule->_oDb->getRoles(array('type' => 'by_gf_id', 'group_profile_id' => $this->_iGroupProfileId, 'fan_id' => $iProfileId));
+            if(!empty($aRole) && is_array($aRole))
+                $mixedValue = bx_time_js($aRole['expired'], BX_FORMAT_DATE, true);
+        }
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
     protected function _getActionSetRole ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
     {
         if ($this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)

+ 97 - 0
modules/base/groups/classes/BxBaseModGroupsGridPrices.php

@@ -0,0 +1,97 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+
+class BxBaseModGroupsGridPrices extends BxTemplGrid
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_sParamsDivider = '#-#';
+
+    protected $_iGroupProfileId;
+    protected $_iGroupContentId;
+    protected $_aGroupContentInfo;
+
+    protected $_aRoles;
+    protected $_aPeriodUnits;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+    	$this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct ($aOptions, $oTemplate);
+
+        $CNF = $this->_oModule->_oConfig->CNF;
+
+        $this->_aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+        $this->_aPeriodUnits = BxDolForm::getDataItems($CNF['OBJECT_FORM_PRELISTS_PERIOD_UNITS']);
+
+        $this->_iGroupProfileId = 0;
+        $this->_iGroupContentId = 0;
+        $this->_aGroupContentInfo = array();
+        if(($iGroupProfileId = bx_get('profile_id')) !== false)
+            $this->setProfileId($iGroupProfileId);
+    }
+
+    public function setProfileId($iProfileId)
+    {
+        $this->_iGroupProfileId = (int)$iProfileId;
+        $this->_aQueryAppend['profile_id'] = $this->_iGroupProfileId;
+
+        $oGroupProfile = BxDolProfile::getInstance($this->_iGroupProfileId);
+        if($oGroupProfile) {
+            $this->_iGroupContentId = (int)$oGroupProfile->getContentId();
+            $this->_aGroupContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_iGroupContentId);
+        }
+    }
+
+    protected function _getCellPeriod($mixedValue, $sKey, $aField, $aRow)
+    {
+        $CNF = $this->_oModule->_oConfig->CNF;
+
+        if((int)$mixedValue == 0)
+            $mixedValue = _t('_lifetime');
+        else
+            $mixedValue = _t($CNF['T']['txt_n_unit'], $mixedValue, _t($this->_aPeriodUnits[$aRow['period_unit']]));
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellPrice($mixedValue, $sKey, $aField, $aRow)
+    {
+        if((float)$mixedValue != 0) {
+            $aCurrency = $this->_oModule->_oConfig->getCurrency();
+
+            $mixedValue = $aCurrency['sign'] . $mixedValue;
+        }
+        else 
+            $mixedValue = _t('_free');
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getIds()
+    {
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) {
+            $iId = (int)bx_get('id');
+            if(!$iId) 
+                return false;
+
+            $aIds = array($iId);
+        }
+
+        return $aIds;
+    }
+}
+
+/** @} */

+ 220 - 0
modules/base/groups/classes/BxBaseModGroupsGridPricesManage.php

@@ -0,0 +1,220 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+require_once('BxBaseModGroupsGridPrices.php');
+
+class BxBaseModGroupsGridPricesManage extends BxBaseModGroupsGridPrices
+{
+    protected $_iRoleId;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_iRoleId = 0;
+        if(($iRoleId = (int)bx_get('role_id')) !== false)
+            $this->setRoleId($iRoleId);
+    }
+
+    public function setRoleId($iRoleId)
+    {
+        $this->_iRoleId = (int)$iRoleId;
+        $this->_aQueryAppend['role_id'] = $this->_iRoleId;
+    }
+
+    public function getCode($isDisplayHeader = true)
+    {
+        $sResult = parent::getCode($isDisplayHeader);
+        if(empty($sResult))
+            return $sResult;
+
+        $sJsCode = $this->_oModule->_oTemplate->getJsCode('prices', array(
+            'sObjNameGrid' => $this->_sObject, 
+            'aHtmlIds' => $this->_oModule->_oConfig->getHtmlIds()
+        ));
+
+        return $sJsCode . $sResult;
+    }
+
+    public function performActionAdd()
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	$sAction = 'add';
+
+        $sFilter = bx_get('filter');
+        if(strpos($sFilter, $this->_sParamsDivider) !== false)
+            list($this->_iRoleId, $sFilter) = explode($this->_sParamsDivider, $sFilter);
+
+        if(!is_numeric($this->_iRoleId) && ($iRoleId = bx_get('role_id')) !== false)
+            $this->_iRoleId = (int)$iRoleId;
+
+    	$oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_PRICE'], $CNF['OBJECT_FORM_PRICE_DISPLAY_ADD']);
+    	$oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . bx_append_url_params('grid.php', array('o' => $this->_sObject, 'a' => $sAction, 'profile_id' => $this->_iGroupProfileId, 'role_id' => $this->_iRoleId));
+    	$oForm->aInputs['role_id']['value'] = $this->_iRoleId;
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $iPeriod = $oForm->getCleanValue('period');
+            $sPeriodUnit = $oForm->getCleanValue('period_unit');
+
+            if(!empty($iPeriod) && empty($sPeriodUnit)) 
+                return echoJson(array('msg' => _t('_bx_groups_form_price_input_err_period_unit')));
+
+            $aPrice = $this->_oModule->_oDb->getPrices(array('type' => 'by_prpp', 'profile_id' => $this->_iGroupProfileId, 'role_id' => $this->_iRoleId, 'period' => $iPeriod, 'period_unit' => $sPeriodUnit));
+            if(!empty($aPrice) && is_array($aPrice))
+                return echoJson(array('msg' => _t('_bx_groups_err_price_duplicate')));
+
+            $sName = uriGenerate(strtolower(strmaxtextlen($this->_aGroupContentInfo[$CNF['FIELD_TITLE']], 8, '')) . ' ' . strtolower($this->_aRoles[$this->_iRoleId]) . ' ' . $iPeriod . ' ' . $sPeriodUnit, $CNF['TABLE_PRICES'], 'name');
+
+            $iId = (int)$oForm->insert(array('name' => $sName, 'profile_id' => $this->_iGroupProfileId, 'order' => $this->_oModule->_oDb->getPriceOrderMax($this->_iRoleId) + 1));
+            if($iId != 0)
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $iId);
+            else
+                $aRes = array('msg' => _t('_bx_groups_err_cannot_perform'));
+
+            echoJson($aRes);
+            return;
+        }
+
+        bx_import('BxTemplFunctions');
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oModule->_oConfig->getHtmlIds('popup_price'), _t('_bx_groups_popup_title_price_add'), $this->_oModule->_oTemplate->parseHtmlByName('popup_price.html', array(
+            'form_id' => $oForm->aFormAttrs['id'],
+            'form' => $oForm->getCode(true),
+            'object' => $this->_sObject,
+            'action' => $sAction
+        )));
+
+        echoJson(array('popup' => array('html' => $sContent, 'options' => array('closeOnOuterClick' => false))));
+    }
+
+    public function performActionEdit()
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sAction = 'edit';
+
+        $aIds = $this->_getIds();
+        if($aIds === false)
+            return echoJson(array());
+
+        $aItem = $this->_oModule->_oDb->getPrices(array('type' => 'by_id', 'value' => array_shift($aIds)));
+        if(!is_array($aItem) || empty($aItem))
+            return echoJson(array());
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_PRICE'], $CNF['OBJECT_FORM_PRICE_DISPLAY_EDIT']);
+    	$oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . bx_append_url_params('grid.php', array('o' => $this->_sObject, 'a' => $sAction, 'profile_id' => $this->_iGroupProfileId, 'role_id' => $this->_iRoleId));
+
+        $oForm->initChecker($aItem);
+        if($oForm->isSubmittedAndValid()) {
+            if($oForm->update($aItem['id']) !== false)
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $aItem['id']);
+            else
+                $aRes = array('msg' => _t('_bx_groups_err_cannot_perform'));
+
+            return echoJson($aRes);
+        }
+
+        bx_import('BxTemplFunctions');
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oModule->_oConfig->getHtmlIds('popup_price'), _t('_bx_groups_popup_title_price_edit'), $this->_oModule->_oTemplate->parseHtmlByName('popup_price.html', array(
+            'form_id' => $oForm->aFormAttrs['id'],
+            'form' => $oForm->getCode(true),
+            'object' => $this->_sObject,
+            'action' => $sAction
+        )));
+
+        $aRes = array('popup' => array('html' => $sContent, 'options' => array('closeOnOuterClick' => false)));
+
+        return echoJson($aRes);
+    }
+
+    public function performActionDelete()
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	$aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds))
+            return echoJson(array());
+
+        $iAffected = 0;
+        $aIdsAffected = array();
+        foreach($aIds as $iId)
+            if($this->_oModule->_oDb->deletePrices(array('id' => $iId))) {
+                $aIdsAffected[] = $iId;
+                $iAffected++;
+            }
+
+        return echoJson($iAffected ? array('grid' => $this->getCode(false), 'blink' => $aIdsAffected) : array('msg' => _t('_bx_groups_err_cannot_perform')));
+    }
+
+    protected function _addJsCss()
+    {
+        parent::_addJsCss();
+
+        $this->_oModule->_oTemplate->addJs(array('jquery.form.min.js', 'prices.js'));
+        $this->_oModule->_oTemplate->addCss(array('prices.css'));
+    }
+
+    protected function _getFilterControls()
+    {
+        parent::_getFilterControls();
+
+        $sContent = '';
+        $oForm = new BxTemplFormView(array());
+
+        $aInputRoles = array(
+            'type' => 'select',
+            'name' => 'role',
+            'attrs' => array(
+                'id' => 'bx-grid-level-' . $this->_sObject,
+                'onChange' => 'javascript:' . $this->_oModule->_oConfig->getJsObject('prices') . '.onChangeRole()'
+            ),
+            'value' => $this->_iRoleId,
+            'values' => $this->_aRoles
+        );
+        $sContent .=  $oForm->genRow($aInputRoles);
+
+        $aInputSearch = array(
+            'type' => 'text',
+            'name' => 'keyword',
+            'attrs' => array(
+                'id' => 'bx-grid-search-' . $this->_sObject,
+            ),
+        );
+        $sContent .= $oForm->genRow($aInputSearch);
+
+        return $sContent;
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        if(strpos($sFilter, $this->_sParamsDivider) !== false) {
+            list($iRoleId, $sFilter) = explode($this->_sParamsDivider, $sFilter);
+            if(!is_numeric($iRoleId))
+                $iRoleId = 0;
+
+            $this->setRoleId($iRoleId);
+        }
+
+        $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString("AND `profile_id`=? AND `role_id`=? ", $this->_iGroupProfileId, $this->_iRoleId);
+
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);;
+    }
+
+    protected function _isVisibleGrid ($a)
+    {
+        return $this->_oModule->checkAllowedManageAdmins($this->_aGroupContentInfo) == CHECK_ACTION_RESULT_ALLOWED;
+    }
+}
+
+/** @} */

+ 120 - 0
modules/base/groups/classes/BxBaseModGroupsGridPricesView.php

@@ -0,0 +1,120 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+require_once('BxBaseModGroupsGridPrices.php');
+
+class BxBaseModGroupsGridPricesView extends BxBaseModGroupsGridPrices
+{
+    protected $_oPayment;
+    protected $_bTypeSingle;
+    protected $_bTypeRecurring;
+
+    protected $_iSeller;
+    protected $_iClient;
+    protected $_iClientRole;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_oPayment = BxDolPayments::getInstance();
+
+        $this->_iSeller = $this->_aGroupContentInfo[$CNF['FIELD_AUTHOR']];
+        $this->_iClient = bx_get_logged_profile_id();
+        $this->_iClientRole = $this->_oModule->getRole($this->_iGroupProfileId, $this->_iClient);
+
+        $this->_bTypeSingle = $this->_oPayment->isAcceptingPayments($this->_iSeller, BX_PAYMENT_TYPE_SINGLE);
+        $this->_bTypeRecurring = $this->_oPayment->isAcceptingPayments($this->_iSeller, BX_PAYMENT_TYPE_RECURRING);
+    }
+
+    public function performActionChoose()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aIds = $this->_getIds();
+        if($aIds === false)
+            return echoJson(array());
+
+        $aItem = $this->_oModule->_oDb->getPrices(array('type' => 'by_id', 'value' => $aIds[0]));
+        if(!is_array($aItem) || empty($aItem) || (float)$aItem['price'] != 0)
+            return echoJson(array());
+
+        $aResult = array();
+        if($this->_oModule->setRole($this->_iGroupProfileId, $this->_iClient, $aItem['role_id'], array('period' => $aItem['period'], 'period_unit' => $aItem['period_unit'])))
+            $aResult = array('grid' => $this->getCode(false), 'blink' => $aItem['id'], 'msg' => _t($CNF['T']['msg_performed']));
+        else
+            $aResult = array('msg' => _t($CNF['T']['err_cannot_perform']));
+
+        return echoJson($aResult);
+    }
+
+    public function getCode($isDisplayHeader = true)
+    {
+    	return $this->_oPayment->getCartJs() . parent::getCode($isDisplayHeader);
+    }
+
+    protected function _getCellRoleId($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault($this->_aRoles[$mixedValue], $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionChoose ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if((float)$aRow['price'] != 0 || $this->_oModule->getRole($this->_iGroupProfileId, $this->_iClient) === (int)$aRow['role_id'])
+            return '';
+
+        return  parent::_getActionDefault($sType, $sKey, $a, false, $isDisabled, $aRow);
+    }
+
+    protected function _getActionBuy ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if((float)$aRow['price'] == 0 || !$this->_bTypeSingle || $this->_bTypeRecurring)
+            return '';
+
+        $aJs = $this->_oPayment->getAddToCartJs($this->_iSeller, $this->_sModule, $aRow['id'], 1, true);
+        if(!empty($aJs) && is_array($aJs)) {
+            list($sJsCode, $sJsMethod) = $aJs;
+
+            $a['attr'] = array(
+                'title' => bx_html_attribute(_t($CNF['T']['txt_buy_title'])),
+                'onclick' => $sJsMethod
+            );
+        }
+
+        return  parent::_getActionDefault($sType, $sKey, $a, false, $isDisabled, $aRow);
+    }
+
+    protected function _getActionSubscribe ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if((float)$aRow['price'] == 0 || !$this->_bTypeRecurring)
+            return '';
+
+        $aJs = $this->_oPayment->getSubscribeJs($this->_iSeller, '', $this->_sModule, $aRow['id'], 1);
+        if(!empty($aJs) && is_array($aJs)) {
+            list($sJsCode, $sJsMethod) = $aJs;
+
+            $a['attr'] = array(
+                'title' => bx_html_attribute(_t($CNF['T']['txt_subscribe_title'])),
+                'onclick' => $sJsMethod
+            );
+        }
+
+        return  parent::_getActionDefault($sType, $sKey, $a, false, $isDisabled, $aRow);
+    }
+}
+
+/** @} */

+ 23 - 0
modules/base/groups/classes/BxBaseModGroupsMenuSnippetMeta.php

@@ -26,6 +26,29 @@ class BxBaseModGroupsMenuSnippetMeta extends BxBaseModProfileMenuSnippetMeta
         }
     }
 
+    protected function _getMenuItemJoinPaid($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!isLogged() || $this->_oModule->checkAllowedFanAdd($this->_aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        if(!$oConnection)
+            return false;
+        
+        $sTitle = $this->_getMenuItemConnectionsTitle('add', $oConnection);
+        if(empty($sTitle))
+            return false;
+
+        return $this->getUnitMetaItemButtonSmall(_t('_bx_groups_menu_item_title_pay_and_join'), array(
+            'href' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php', array(
+                'i' => $CNF['URI_JOIN_ENTRY'],
+                'profile_id' => $this->_oContentProfile->id()
+            ))
+        ));
+    }
+
     protected function _getMenuItemJoin($aItem)
     {
         if (isset($this->_oModule->_oConfig->CNF['OBJECT_CONNECTIONS']))

+ 5 - 0
modules/base/groups/classes/BxBaseModGroupsMenuViewActionsAll.php

@@ -24,6 +24,11 @@ class BxBaseModGroupsMenuViewActionsAll extends BxBaseModProfileMenuViewActionsA
         return $this->_getMenuItemByNameActions($aItem);
     }
 
+    protected function _getMenuItemProfileFanAddPaid($aItem)
+    {
+        return $this->_getMenuItemByNameActions($aItem);
+    }
+
     protected function _getMenuItemProfileFanRemove($aItem)
     {
         return $this->_getMenuItemByNameActionsMore($aItem);

+ 372 - 17
modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -9,6 +9,7 @@
  * @{
  */
 
+define('BX_BASE_MOD_GROUPS_ROLE_COMMON', 0);
 define('BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR', 1);
 define('BX_BASE_MOD_GROUPS_ROLE_MODERATOR', 2);
 
@@ -19,6 +20,11 @@ define('BX_BASE_MOD_GROUPS_ACTION_MANAGE_FANS', 'manage_fans');
 define('BX_BASE_MOD_GROUPS_ACTION_MANAGE_ROLES', 'manage_roles');
 define('BX_BASE_MOD_GROUPS_ACTION_DELETE', 'delete');
 
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_DAY', 'day');
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_WEEK', 'week');
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_MONTH', 'month');
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_YEAR', 'year');
+
 /**
  * Groups profiles module.
  */
@@ -438,6 +444,47 @@ class BxBaseModGroupsModule extends BxBaseModProfileModule
         return $this->_serviceBrowse ('created_entries', array('author' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
     }
     
+    public function serviceEntityPricing($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        if(!$this->_oConfig->isPaidJoin())
+            return '';
+
+        if($this->checkAllowedManageAdmins($iProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox(_t('_Access denied'));
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_PRICES_MANAGE']);
+        if(!$oGrid)
+            return '';
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceEntityJoin($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        if(!$this->_oConfig->isPaidJoin())
+            return '';
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_PRICES_VIEW']);
+        if(!$oGrid)
+            return '';
+
+        return $oGrid->getCode();
+    }
+    
     public function serviceEntityInvite ($iContentId = 0, $bErrorMsg = true)
     {
         if (isset($this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE']))
@@ -471,7 +518,242 @@ class BxBaseModGroupsModule extends BxBaseModProfileModule
         ));
     }
 
-	/**
+    public function serviceIsPricingAvaliable($iProfileId)
+    {
+        if(!$this->_oConfig->isPaidJoin())
+            return false;
+
+        if($this->checkAllowedManageAdmins($iProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return true;
+    }
+
+    public function serviceIsPaidJoinAvaliable($iProfileId)
+    {
+        return $this->isPaidJoinByProfile($iProfileId);
+    }
+
+    public function serviceIsPaidJoinAvaliableByContent($iContentId)
+    {
+        $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if(!$oProfile)
+            return false;
+
+        return $this->isPaidJoinByProfile($oProfile->id());
+    }
+
+    public function serviceIsFreeJoinAvaliable($iProfileId)
+    {
+        return !$this->isPaidJoinByProfile($iProfileId);
+    }
+
+    public function serviceIsFreeJoinAvaliableByContent($iContentId)
+    {
+        $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if(!$oProfile)
+            return false;
+
+        return !$this->isPaidJoinByProfile($oProfile->id());
+    }
+
+    public function isPaidJoinByProfile($iProfileId)
+    {
+        if(!$this->_oConfig->isPaidJoin())
+            return false;
+
+        $aPrices = $this->_oDb->getPrices(array('type' => 'by_profile_id', 'profile_id' => $iProfileId));
+        if(empty($aPrices) || !is_array($aPrices))
+            return false;
+
+        return true;
+    }
+
+    /**
+     * Integration with Payments.
+     */
+    public function serviceGetPaymentData()
+    {
+        return $this->_aModule;
+    }
+
+    public function serviceGetCartItem($mixedItemId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$mixedItemId)
+            return array();
+
+        if(is_numeric($mixedItemId))
+            $aItem = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => (int)$mixedItemId));
+        else 
+            $aItem = $this->_oDb->getPrices(array('type' => 'by_name', 'value' => $mixedItemId));
+
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        if(!$this->isPaidJoinByProfile($aItem['profile_id']))
+            return array();
+
+        $oGroupProfile = BxDolProfile::getInstance($aItem['profile_id']);
+        if(!$oGroupProfile)
+            return array();
+
+        $aGroupProfile = $this->_oDb->getContentInfoById($oGroupProfile->getContentId());
+        
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        return array (
+            'id' => $aItem['id'],
+            'author_id' => $aGroupProfile[$CNF['FIELD_AUTHOR']],
+            'name' => $aItem['name'],
+            'title' => _t($CNF['T']['txt_cart_item_title'], $oGroupProfile->getDisplayName(), $aRoles[$aItem['role_id']], $aItem['period'], $aItem['period_unit']),
+            'description' => '',
+            'url' => $oGroupProfile->getUrl(),
+            'price_single' => $aItem['price'],
+            'price_recurring' => $aItem['price'],
+            'period_recurring' => $aItem['period'],
+            'period_unit_recurring' => $aItem['period_unit']
+        );
+    }
+
+    public function serviceGetCartItems($iSellerId)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	if(empty($iSellerId))
+    	    return array();
+
+        $sModule = $this->getName();
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        $aGroups = $this->_oDb->getEntriesBy(array('type' => 'author', 'author' => $iSellerId));
+
+        $aResult = array();
+        foreach($aGroups as $aGroup) {
+            $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aGroup[$CNF['FIELD_ID']], $sModule);
+            if(!$oGroupProfile)
+                continue;
+
+            $aPrices = $this->_oDb->getPrices(array('type' => 'by_profile_id', 'profile_id' => $oGroupProfile->id()));
+            if(empty($aPrices) || !is_array($aPrices))
+                continue;
+
+            $sTitle = $oGroupProfile->getDisplayName();
+            $sUrl = $oGroupProfile->getUrl();
+
+            foreach($aPrices as $aPrice)
+                $aResult[] = array(
+                    'id' => $aPrice['id'],
+                    'author_id' => $iSellerId,
+                    'name' => $aPrice['name'],
+                    'title' => _t($CNF['T']['txt_cart_item_title'], $sTitle, $aRoles[$aPrice['role_id']], $aPrice['period'], $aPrice['period_unit']),
+                    'description' => '',
+                    'url' => $sUrl,
+                    'price_single' => $aPrice['price'],
+                    'price_recurring' => $aPrice['price'],
+                    'period_recurring' => $aPrice['period'],
+                    'period_unit_recurring' => $aPrice['period_unit']
+               );
+        }
+
+        return $aResult;
+    }
+
+    public function serviceRegisterCartItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceRegisterSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceReregisterCartItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        return $this->_serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder);
+    }
+
+    public function serviceReregisterSubscriptionItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        return $this->_serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder);
+    }
+
+    public function serviceUnregisterCartItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceUnregisterSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+    	return $this->_serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder); 
+    }
+
+    public function serviceCancelSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+    	return true;
+    }
+
+    protected function _serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aItem = $this->serviceGetCartItem($iItemId);
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        $aItemInfo = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemId));
+
+        $mixedPeriod = false;
+        if((int)$aItemInfo['period'] != 0)
+            $mixedPeriod = array(
+                'period' => (int)$aItemInfo['period'], 
+                'period_unit' => $aItemInfo['period_unit'], 
+                'period_reserve' => $CNF['PARAM_RECURRING_RESERVE']
+            );
+
+        if(!$this->setRole($aItemInfo['profile_id'], $iClientId, $aItemInfo['role_id'], $mixedPeriod, $sOrder))
+            return array();
+
+        return $aItem;
+    }
+
+    protected function _serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        $aItem = $this->serviceGetCartItem($iItemIdNew);
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        $aItemInfoOld = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemIdOld));
+        if(empty($aItemInfoOld) || !is_array($aItemInfoOld))
+            return array();
+
+        $aItemInfoNew = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemIdNew));
+        if(empty($aItemInfoNew) || !is_array($aItemInfoNew))
+            return array();
+
+        if(!$this->unsetRole($aItemInfoOld['profile_id'], $iClientId))
+            return array();
+        
+        $aResult = $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemIdNew, 1, $sOrder);
+        if(empty($aResult) || !is_array($aResult))
+            return array();
+
+    	return $aItem;
+    }
+
+    protected function _serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+        $aItemInfo = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemId));
+        if(empty($aItemInfo) || !is_array($aItemInfo))
+            return false;
+
+        return $this->unsetRole($aItemInfo['profile_id'], $iClientId);
+    }
+
+
+    /**
      * Data for Notifications module
      */
     public function serviceGetNotificationsData()
@@ -897,12 +1179,26 @@ class BxBaseModGroupsModule extends BxBaseModProfileModule
     
     public function isFan ($iContentId, $iProfileId = false) 
     {
+        $CNF = &$this->_oConfig->CNF;
+
         $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
-        if (isset($this->_oConfig->CNF['OBJECT_CONNECTIONS']))
-            return $oGroupProfile && ($oConnection = BxDolConnection::getObjectInstance($this->_oConfig->CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+        if($oGroupProfile && isset($CNF['OBJECT_CONNECTIONS']))
+            return ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+
         return false;
     }
-    
+
+    public function isFanByGroupProfileId ($iGroupProfileId, $iProfileId = false) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if($oGroupProfile && isset($CNF['OBJECT_CONNECTIONS']))
+            return ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+
+        return false;
+    }
+
     public function isInvited ($sKey, $iProfileId) 
     {
         $CNF = &$this->_oConfig->CNF;
@@ -916,17 +1212,35 @@ class BxBaseModGroupsModule extends BxBaseModProfileModule
         return true;
     }
 
-    public function setRole($iGroupProfileId, $iFanProfileId, $iRole)
+    public function getRole($iGroupProfileId, $iFanProfileId)
     {
-        if(($mixedResult = $this->checkAllowedManageAdmins($iGroupProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
-            return array('code' => 1, 'msg' => $mixedResult);
+        if(!$this->isFanByGroupProfileId($iGroupProfileId, $iFanProfileId))
+            return false;
+
+        return $this->_oDb->getRole($iGroupProfileId, $iFanProfileId);
+    }
+
+    public function setRole($iGroupProfileId, $iFanProfileId, $iRole, $mixedPeriod = false, $sOrder = '')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if(!$oConnection || !$oGroupProfile)
+            return false;
 
-        if(!$this->_oDb->setRole($iGroupProfileId, $iFanProfileId, $iRole))
-            return array('code' => 2, 'msg' => _t('_error occured'));
+        if(!$oConnection->isConnected($iFanProfileId, $iGroupProfileId, true) && !($oConnection->addConnection($iFanProfileId, $iGroupProfileId) && $oConnection->addConnection($iGroupProfileId, $iFanProfileId)))
+            return false;
+
+        if(!$this->_oDb->setRole($iGroupProfileId, $iFanProfileId, $iRole, $mixedPeriod, $sOrder))
+            return false;
 
         $this->onSetRole($iGroupProfileId, $iFanProfileId, $iRole);
 
-        return array('code' => 0);
+        return true;
     }
 
     public function onSetRole($iGroupProfileId, $iFanProfileId, $iRole)
@@ -946,13 +1260,6 @@ class BxBaseModGroupsModule extends BxBaseModProfileModule
                 'Role' => $aRoles[$iRole]
             ), BX_EMAIL_NOTIFY);
 
-        // subscribe advanced group members automatically
-        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
-        if($iRole != 0)
-            $oConnection->addConnection($iFanProfileId, $iGroupProfileId);
-        else 
-            $oConnection->removeConnection($iFanProfileId, $iGroupProfileId);
-
         bx_alert($this->getName(), 'set_role', $aGroupProfileInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
             'object_author_id' => $iGroupProfileId,
             'performer_id' => $iProfileId, 
@@ -968,6 +1275,54 @@ class BxBaseModGroupsModule extends BxBaseModProfileModule
         $this->doAudit($iGroupProfileId, $iFanProfileId, '_sys_audit_action_group_role_changed');
     }
 
+    public function unsetRole($iGroupProfileId, $iFanProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if(!$oConnection || !$oGroupProfile)
+            return false;
+
+        if($oConnection->isConnected($iFanProfileId, $iGroupProfileId, true) && !$oConnection->removeConnection($iFanProfileId, $iGroupProfileId))
+            return false;
+
+        $iRole = $this->_oDb->getRole($iGroupProfileId, $iFanProfileId);
+
+        if(!$this->_oDb->unsetRole($iGroupProfileId, $iFanProfileId))
+            return false;
+
+        $this->onUnsetRole($iGroupProfileId, $iFanProfileId, $iRole);
+
+        return true;
+    }
+
+    public function onUnsetRole($iGroupProfileId, $iFanProfileId, $iRole)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iProfileId = bx_get_logged_profile_id();
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        $aGroupProfileInfo = $this->_oDb->getContentInfoById((int)$oGroupProfile->getContentId());
+
+        bx_alert($this->getName(), 'set_role', $aGroupProfileInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            'object_author_id' => $iGroupProfileId,
+            'performer_id' => $iProfileId, 
+            'fan_id' => $iFanProfileId,
+
+            'content' => $aGroupProfileInfo,
+            'role' => $iRole,
+
+            'group_profile' => $iGroupProfileId, 
+            'profile' => $iProfileId
+        ));
+
+        $this->doAudit($iGroupProfileId, $iFanProfileId, '_sys_audit_action_group_role_changed');
+    }
+
     protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
     {
         $CNF = &$this->_oConfig->CNF;

+ 19 - 0
modules/base/groups/template/popup_price.html

@@ -0,0 +1,19 @@
+<div class="bx-base-group-price-form">
+    <div class="bx-base-group-price-form-cnt">
+        __form__
+        <script language="javascript">
+            $(document).ready(function () {
+                $("#__form_id__").ajaxForm({ 
+                    dataType: "json",
+                    beforeSubmit: function (formData, jqForm, options) {
+                        bx_loading($("#__form_id__"), true);
+                    },
+                    success: function (data) {
+                        $(".bx-popup-applied:visible").dolPopupHide();
+                        glGrids.__object__.processJson(data, '__action__');
+                    }
+                });
+            });
+        </script>
+    </div>
+</div>

+ 4 - 0
modules/boonex/english/data/langs/system/en.xml

@@ -51,12 +51,14 @@
     <string name="_feature_err_duplicate_feature"><![CDATA[The item was already added in featured list.]]></string>
     <string name="_feature_err_not_enabled"><![CDATA[The feature is not available right now. Please, try again later.]]></string>
 	<string name="_Female"><![CDATA[Woman]]></string>
+        <string name="_free"><![CDATA[free]]></string>
 	<string name="_From"><![CDATA[From]]></string>
 	<string name="_Header"><![CDATA[Header]]></string>
 	<string name="_Incorrect Captcha"><![CDATA[Incorrect Captcha]]></string>
 	<string name="_Incorrect Email"><![CDATA[Incorrect Email]]></string>
 	<string name="_Info"><![CDATA[Info]]></string>
 	<string name="_Keyword"><![CDATA[Keyword]]></string>
+        <string name="_lifetime"><![CDATA[lifetime]]></string>
 	<string name="_LookinFemale"><![CDATA[Woman]]></string>
 	<string name="_LookinMale"><![CDATA[Man]]></string>
 	<string name="_MEMBERSHIP_EXPIRES_IN_DAYS"><![CDATA[expires: in {0} day(s)]]></string>
@@ -2496,6 +2498,7 @@ If it is not enabled then please consider implement this optimization, since it
     <string name="_sys_menu_item_title_sm_unfriend"><![CDATA[Remove friend]]></string>
     <string name="_sys_menu_item_title_sm_unfriend_cancel"><![CDATA[Cancel friend request]]></string>
     <string name="_sys_menu_item_title_sm_unfriend_reject"><![CDATA[Reject friend request]]></string>
+    <string name="_sys_menu_item_title_sm_join_paid"><![CDATA[Pay & Join]]></string>
     <string name="_sys_menu_item_title_sm_join"><![CDATA[Join]]></string>
     <string name="_sys_menu_item_title_sm_join_confirm"><![CDATA[Accept join request]]></string>
     <string name="_sys_menu_item_title_sm_leave"><![CDATA[Leave]]></string>
@@ -2581,6 +2584,7 @@ If it is not enabled then please consider implement this optimization, since it
     <string name="_sys_menu_item_title_system_sm_actions"><![CDATA[Actions]]></string>
     <string name="_sys_menu_item_title_system_sm_befriend"><![CDATA[Add Friend]]></string>
     <string name="_sys_menu_item_title_system_sm_unfriend"><![CDATA[Remove Friend]]></string>
+    <string name="_sys_menu_item_title_system_sm_join_paid"><![CDATA[Pay & Join]]></string>
     <string name="_sys_menu_item_title_system_sm_join"><![CDATA[Join]]></string>
     <string name="_sys_menu_item_title_system_sm_leave"><![CDATA[Leave]]></string>
     <string name="_sys_menu_item_title_system_sm_nl"><![CDATA[New Line]]></string>

+ 4 - 0
modules/boonex/russian/data/langs/system/ru.xml

@@ -51,12 +51,14 @@
     <string name="_feature_err_duplicate_feature"><![CDATA[Данный объект уже добавлен в список продвинутых.]]></string>
     <string name="_feature_err_not_enabled"><![CDATA[Действие не доступно в данный момент. Пожалуйста, попробуйте позднее.]]></string>
 	<string name="_Female"><![CDATA[Женщина]]></string>
+        <string name="_free"><![CDATA[бесплатно]]></string>
 	<string name="_From"><![CDATA[От]]></string>
 	<string name="_Header"><![CDATA[Заголовок]]></string>
 	<string name="_Incorrect Captcha"><![CDATA[Неправильная капча]]></string>
 	<string name="_Incorrect Email"><![CDATA[Введите правильный адрес электронной почты]]></string>
 	<string name="_Info"><![CDATA[Инфо]]></string>
 	<string name="_Keyword"><![CDATA[Ключевое слово]]></string>
+        <string name="_lifetime"><![CDATA[пожизненно]]></string>
 	<string name="_LookinFemale"><![CDATA[Женщина]]></string>
 	<string name="_LookinMale"><![CDATA[Мужчина]]></string>
 	<string name="_MEMBERSHIP_EXPIRES_IN_DAYS"><![CDATA[окончание срока: через {0} дней]]></string>
@@ -2482,6 +2484,7 @@
     <string name="_sys_menu_item_title_sm_unfriend"><![CDATA[Удалить из друзей]]></string>
     <string name="_sys_menu_item_title_sm_unfriend_cancel"><![CDATA[Отменить запрос о дружбе]]></string>
     <string name="_sys_menu_item_title_sm_unfriend_reject"><![CDATA[Отвергнуть запрос о дружбе]]></string>
+    <string name="_sys_menu_item_title_sm_join_paid"><![CDATA[Оплатить и присоединиться]]></string>
     <string name="_sys_menu_item_title_sm_join"><![CDATA[Присоединиться]]></string>
     <string name="_sys_menu_item_title_sm_join_confirm"><![CDATA[Одобрить присоединение]]></string>
     <string name="_sys_menu_item_title_sm_leave"><![CDATA[Покинуть]]></string>
@@ -2566,6 +2569,7 @@
     <string name="_sys_menu_item_title_system_sm_actions"><![CDATA[Действия]]></string>
     <string name="_sys_menu_item_title_system_sm_befriend"><![CDATA[Добавить в друзья]]></string>
     <string name="_sys_menu_item_title_system_sm_unfriend"><![CDATA[Удалить из друзей]]></string>
+    <string name="_sys_menu_item_title_system_sm_join_paid"><![CDATA[Оплатить и присоединиться]]></string>
     <string name="_sys_menu_item_title_system_sm_join"><![CDATA[Присоединиться]]></string>
     <string name="_sys_menu_item_title_system_sm_leave"><![CDATA[Покинуть]]></string>
     <string name="_sys_menu_item_title_system_sm_nl"><![CDATA[Новая строка]]></string>