name = 'cartsguru'; $this->tab = 'advertising_marketing'; $this->version = '1.1.1'; $this->author = 'LINKT IT'; $this->module_key = 'f841e8edc4514a141082e10c797c7c57'; $this->bootstrap = true; $this->img_url = __PS_BASE_URI__ . basename(_PS_MODULE_DIR_) . '/' . $this->name . '/views/img/'; parent::__construct(); $this->displayName = $this->l('Carts Guru'); $this->description = $this->l('The first targeted and automated follow-up solution for abandoned carts through phone and text message !'); if (! function_exists('curl_version')) { $this->warning = $this->l('cURL librairy is not available.'); } if (version_compare(_PS_VERSION_, '1.5.0', '<')) { require(_PS_MODULE_DIR_.$this->name.'/backward_compatibility/backward.php'); } if (version_compare(_PS_VERSION_, '1.5.0', '<')) { if (!(int)Configuration::get('CARTSG_API_SUCCESS')) { $this->warning = $this->l('The module is not configured'); } } else { if ((! Shop::isFeatureActive() || Shop::getContext() == Shop::CONTEXT_SHOP) && !(int)Configuration::get('CARTSG_API_SUCCESS') ) { $this->warning = $this->l('The module is not configured'); } } if ($this->cg_debug) { $this->logger = new FileLogger(0); $this->logger->setFilename(_PS_ROOT_DIR_.'/log/debug_cartguru_'.date('Ymd').'.log'); } } private function logDebug($message) { if ($this->cg_debug && $this->logger) { $this->logger->logDebug($message); } } /** * Module installation * * @see Module->install * @param bool|true $delete_params * use for reinitialisation data * @return bool */ public function install($delete_params = true) { if ($delete_params) { if (! Configuration::updateValue('CARTSG_TOKEN', $this->generateCode('', 15)) || ! Configuration::updateValue('CARTSG_IMAGE_GENERATE', 0)) { return false; } $image_type = new ImageType(); $image_type->name = 'cartsguru'; $image_type->width = '120'; $image_type->height = '120'; $image_type->products = 1; $image_type->categories = 0; $image_type->manufacturers = 0; $image_type->suppliers = 0; $image_type->scenes = 0; $image_type->stores = 0; if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $image_type->store = 0; } $image_type->add(); Configuration::updateValue('CARTSG_IMAGE_TYPE_ID', $image_type->id); } if (parent::install() == false) { return false; } if (version_compare(_PS_VERSION_, '1.5.0', '<')) { if (! $this->registerHook('newOrder') || ! $this->registerHook('postUpdateOrderStatus') || ! $this->registerHook('cart') || ! $this->registerHook('header') || ! $this->registerHook('createAccount')) { return false; } } else { if (! $this->registerHook('actionValidateOrder') || ! $this->registerHook('actionObjectOrderUpdateAfter') || ! $this->registerHook('actionCartSave') || ! $this->registerHook('actionCustomerAccountAdd') || ! $this->registerHook('actionObjectCustomerUpdateAfter') || ! $this->registerHook('actionObjectAddressAddAfter') || ! $this->registerHook('actionObjectAddressUpdateAfter')) { return false; } } return true; } /** * Uninstall the module * * @see * * * * * @param bool|true $delete_params * is use for delete all data in db * @return bool */ public function uninstall($delete_params = true) { if (! parent::uninstall()) { return false; } if ($delete_params && (! Configuration::deleteByName('CARTSG_TOKEN') || ! Configuration::deleteByName('CARTSG_API_AUTH_KEY') || ! Configuration::deleteByName('CARTSG_SITE_ID') || ! Configuration::deleteByName('CARTSG_API_SUCCESS') || ! Configuration::deleteByName('CARTSG_IMAGE_GENERATE'))) { return false; } elseif ($delete_params) { $image_type = new ImageType((int)Configuration::get('CARTSG_IMAGE_TYPE_ID')); if (Validate::isLoadedObject($image_type)) { $this->deleteProductImageCG(); $image_type->delete(); } if (!Configuration::deleteByName('CARTSG_IMAGE_TYPE_ID')) { return false; } } return true; } /** * reset the module, no data is deleted * * @return bool */ public function reset() { if (! $this->uninstall(false)) { return false; } if (! $this->install(false)) { return false; } return true; } /** * * @return String */ public function getContent() { $this->_html = ''; $this->postProcess(); if (version_compare(_PS_VERSION_, '1.5.0', '<') || ! Shop::isFeatureActive() || Shop::getContext() == Shop::CONTEXT_SHOP) { $configs = $this->getConfigFieldsValues(); if (! empty($configs['CARTSG_API_AUTH_KEY']) && ! empty($configs['CARTSG_SITE_ID'])) { $api = new CartGuruRAPI($configs['CARTSG_SITE_ID'], $configs['CARTSG_API_AUTH_KEY']); $result = $api->checkAccess(); $access_cg = ($result ? ($result->info->http_code == 200) : false); if ($access_cg) { Configuration::updateValue('CARTSG_API_SUCCESS', 1); $this->_html .= $this->displayConfirmation($this->l('Success connexion..')); $response = $result->decodeResponse(); if ($response->isNew) { $this->importCarts($configs['CARTSG_SITE_ID']); $this->importOrders($configs['CARTSG_SITE_ID']); } if (!(int)Configuration::get('CARTSG_IMAGE_GENERATE')) { $img_generated = $this->generateProductImageCG(); if ($img_generated === true) { $this->_html .= $this->displayConfirmation($this->l('Image product for Cart Guru were successfully regenerated.')); Configuration::updateValue('CARTSG_IMAGE_GENERATE', 1); } elseif ($img_generated == 'timeout') { $this->_html .= $this->displayWarn( $this->l('Only part of the images have been regenerated. The server timed out before finishing.'). $this->l('Use Image Generation form with option erase to false and click to the button generate.') ); } } } else { Configuration::updateValue('CARTSG_API_SUCCESS', 0); $this->_html .= $this->displayWarn($this->l('Impossible to connect with this credential.')); } } } $this->_html .= $this->renderConfigForm(); if ((int)Configuration::get('CARTSG_API_SUCCESS')) { if (version_compare(_PS_VERSION_, '1.6') < 0) { $this->_html .= '
'; } $this->_html .= $this->renderImageForm(); } return $this->_html; } /** * Configuration update settings */ protected function postProcess() { $errorval = ''; if (Tools::isSubmit('submitSettings')) { $api_auth_key = Tools::getValue('CARTSG_API_AUTH_KEY'); if (empty($api_auth_key)) { $errorval .= $this->displayError(sprintf($this->l('Field \'%1$s\' is required'), $this->l('API auth key'))); } else { Configuration::updateValue('CARTSG_API_AUTH_KEY', $api_auth_key); } $site_id = Tools::getValue('CARTSG_SITE_ID'); if (empty($site_id)) { $errorval .= $this->displayError(sprintf($this->l('Field \'%1$s\' is required'), $this->l('Site Id'))); } else { Configuration::updateValue('CARTSG_SITE_ID', $site_id); } if (! empty($errorval)) { $this->_html .= $errorval; } else { $this->_html .= $this->displayConfirmation($this->l('Settings updated')); } } if (Tools::isSubmit('submitGenerateImage')) { $erase = (int)Tools::getValue('erase', 0); if ($erase) { $this->deleteProductImageCG(); } $img_generated = $this->generateProductImageCG(); if ($img_generated === true) { $this->_html .= $this->displayConfirmation($this->l('Image product for Cart Guru were successfully regenerated.')); Configuration::updateValue('CARTSG_IMAGE_GENERATE', 1); } elseif ($img_generated == 'timeout') { $this->_html .= $this->displayWarn( $this->l('Only part of the images have been regenerated. The server timed out before finishing.').'
'. $this->l('To resume the operation, select no for "Erase previous images", and click "Generate".') ); } } } /** * Generate configuration form */ public function renderConfigForm() { $conf_form_html = ''; $can_configure = (version_compare(_PS_VERSION_, '1.5.0', '<') || ! Shop::isFeatureActive() || Shop::getContext() == Shop::CONTEXT_SHOP); if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $this->context->smarty->assign( array( 'fields_value' => $this->getConfigFieldsValues(), 'request_uri' => Tools::htmlentitiesUTF8($_SERVER['REQUEST_URI']) ) ); $conf_form_html = $this->context->smarty->fetch( _PS_ROOT_DIR_.'/modules/cartsguru/views/templates/admin/cg_conf_form14.tpl' ); } else { $fields_form = array(); $fields_form[0]['form'] = array( 'legend' => array( 'title' => $this->l('Carts Guru authentification'), 'icon' => 'icon-user' ) ); if (! $can_configure) { $fields_form[0]['form']['description'] = $this->l('The multistore option is enabled. If you want configure the module, you must select store.'); } else { $fields_form[0]['form']['input'] = array( array( 'type' => 'text', 'label' => $this->l('API auth key'), 'name' => 'CARTSG_API_AUTH_KEY', 'hint' => $this->l('Provided by Carts Guru') ), array( 'type' => 'text', 'label' => $this->l('Site Id'), 'name' => 'CARTSG_SITE_ID', 'hint' => $this->l('Provided by Carts Guru') ) ); $fields_form[0]['form']['submit'] = array( 'title' => $this->l('Save'), 'class' => 'btn btn-default pull-right', 'name' => 'submitSettings' ); } $helper = new HelperForm(); $helper->show_toolbar = false; $helper->table = $this->name; $lang = new Language((int) Configuration::get('PS_LANG_DEFAULT')); $helper->default_form_language = $lang->id; $helper->module = $this; $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') ? Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') : 0; $helper->identifier = $this->identifier; //$helper->submit_action = 'submitSettings'; $helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false) . '&configure=' . $this->name . '&module_name=' . $this->name; $helper->token = Tools::getAdminTokenLite('AdminModules'); $helper->tpl_vars = array( 'fields_value' => $this->getConfigFieldsValues(), Tools::substr(Tools::encrypt('cartsguru/index'), 0, 10), 'currencies' => Currency::getCurrencies(), 'languages' => $this->context->controller->getLanguages(), 'id_language' => $this->context->language->id ); $conf_form_html = $helper->generateForm($fields_form); } return ($conf_form_html); } /** * */ public function renderImageForm() { $img_form_html = ''; if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $this->context->smarty->assign( array( 'fields_value' => $this->getConfigFieldsValues(), 'request_uri' => Tools::htmlentitiesUTF8($_SERVER['REQUEST_URI']) ) ); $img_form_html = $this->context->smarty->fetch( _PS_ROOT_DIR_.'/modules/cartsguru/views/templates/admin/cg_img_form14.tpl' ); } else { $fields_form = array(); $fields_form[0]['form'] = array( 'legend' => array( 'title' => $this->l('Product Image Generation for Carts Guru'), 'icon' => 'icon-image' ), 'description' => $this->l('The operation is required only if you not see your product image in Carts Guru') ); $fields_form[0]['form']['input'] = array( array( 'type' => (version_compare(_PS_VERSION_, '1.6') < 0) ?'radio' :'switch', 'is_bool' => true, // retro compat 1.5 'class' => 't', 'label' => $this->l('Erase previous images'), 'name' => 'erase', 'hint' => $this->l('Select "No" if your server timed out and you need to resume the regeneration.'), 'values' => array( array( 'id' => 'erase_on', 'value' => 1, 'label' => $this->l('Yes') ), array( 'id' => 'erase_off', 'value' => 0, 'label' => $this->l('No') ) ) ) ); $fields_form[0]['form']['submit'] = array( 'title' => $this->l('Generate'), 'class' => 'btn btn-default pull-right', 'name' => 'submitGenerateImage' ); $helper = new HelperForm(); $helper->show_toolbar = false; $helper->table = $this->name; $lang = new Language((int) Configuration::get('PS_LANG_DEFAULT')); $helper->default_form_language = $lang->id; $helper->module = $this; $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') ? Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') : 0; $helper->identifier = $this->identifier; //$helper->submit_action = 'submitGenerateImage'; $helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false) . '&configure=' . $this->name . '&module_name=' . $this->name; $helper->token = Tools::getAdminTokenLite('AdminModules'); $helper->tpl_vars = array( 'fields_value' => $this->getConfigFieldsValues(), Tools::substr(Tools::encrypt('cartsguru/index'), 0, 10), 'currencies' => Currency::getCurrencies(), 'languages' => $this->context->controller->getLanguages(), 'id_language' => $this->context->language->id ); $img_form_html = $helper->generateForm($fields_form); } return $img_form_html; } /** * Get all configuration * * @return array */ public function getConfigFieldsValues() { return array( 'CARTSG_SITE_ID' => Tools::getValue('CARTSG_SITE_ID', Configuration::get('CARTSG_SITE_ID')), 'erase' => 0, 'CARTSG_API_AUTH_KEY' => Tools::getValue('CARTSG_API_AUTH_KEY', Configuration::get('CARTSG_API_AUTH_KEY')) ); } /** * generate a uniq code for token * * @param string $prefix * @param int $length * @return string */ public static function generateCode($prefix = '', $length = 8) { $code = ''; $possible = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $maxlength = Tools::strlen($possible); if ($length > $maxlength) { $length = $maxlength; } $i = 0; while ($i < $length) { $char = Tools::substr($possible, mt_rand(0, $maxlength - 1), 1); if (! strstr($code, $char)) { $code .= $char; $i ++; } } return $prefix . $code; } /** * Only used for 1.4 * @param $params */ public function hookNewOrder($params) { return $this->hookActionValidateOrder($params); } /** * Only used for 1.4 * @param $params */ public function hookPostUpdateOrderStatus($params) { if ((int)$params['id_order']) { $order = new Order((int)$params['id_order']); $params['object'] = $order; return $this->hookActionObjectOrderUpdateAfter($params); } } /** * When order is validate, indicate it is the reminder permit * journal is close * * @param * $params */ public function hookActionValidateOrder($params) { $order = $params['order']; $params['object'] = $order; return $this->hookActionObjectOrderUpdateAfter($params); } /** * Order update * * @param array $params * @return boolean */ public function hookActionObjectOrderUpdateAfter($params) { $order = $params['object']; if (! Validate::isLoadedObject($order)) { return false; } if (isset($order) && (int) $order->id && (int) $order->id_customer) { if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $this->callCG( CartGuruRAPI::API_PATH_ORDERS, 'order', $order, 0, 0, false, $params ); } else { $this->callCG( CartGuruRAPI::API_PATH_ORDERS, 'order', $order, (int) $order->id_shop_group, (int) $order->id_shop ); } } return true; } /** * Customer add address * * @param array $params * @return boolean */ public function hookActionObjectAddressAddAfter($params) { return $this->hookActionObjectAddressUpdateAfter($params); } /** * Customer update address * * @param array $params * @return boolean */ public function hookActionObjectAddressUpdateAfter($params) { $address = $params['object']; if (! Validate::isLoadedObject($address)) { return false; } if (isset($address) && (int) $address->id) { if ((int) $address->id_customer) { $customer = new Customer((int) $address->id_customer); if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $this->callCG( CartGuruRAPI::API_PATH_ACCOUNTS, 'customer', $customer ); } else { $this->callCG( CartGuruRAPI::API_PATH_ACCOUNTS, 'customer', $customer, (int) $customer->id_shop_group, (int) $customer->id_shop ); } } } return true; } /** * Customer update information * * @param array $params * @return boolean */ public function hookActionObjectCustomerUpdateAfter($params) { $customer = $params['object']; if (! Validate::isLoadedObject($customer)) { return false; } if (isset($customer) && (int) $customer->id) { if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $this->callCG( CartGuruRAPI::API_PATH_ACCOUNTS, 'customer', $customer ); } else { $this->callCG( CartGuruRAPI::API_PATH_ACCOUNTS, 'customer', $customer, (int) $customer->id_shop_group, (int) $customer->id_shop ); } } return true; } /** * hookHeader only used for 1.4 * @param $params * @return boolean */ public function hookHeader($params) { /* Test view controller*/ } /** * Only used for 1.4 * @param $params */ public function hookCreateAccount($params) { return ($this->hookActionCartSave($params)); } /** * Successful create account * * @param array $params * @return boolean */ public function hookActionCustomerAccountAdd($params) { $new_customer = $params['newCustomer']; $params['object'] = $new_customer; return ($this->hookActionObjectCustomerUpdateAfter($params)); } /** * Only used for Prestashop 1.4 * @param $params * @return void|boolean */ public function hookCart($params) { if (!$params['cart']->id) { return; } return ($this->hookActionCartSave($params)); } /** * this hook is call many times update the cart * The module catch only cart have customer logged */ public function hookActionCartSave($params) { $cart = $params['cart']; if (! Validate::isLoadedObject($cart)) { return false; } if (isset($cart) && (int) $cart->id && (int) $cart->id_customer) { if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $this->callCG( CartGuruRAPI::API_PATH_CARTS, 'cart', $cart ); } else { $this->callCG( CartGuruRAPI::API_PATH_CARTS, 'cart', $cart, (int) $cart->id_shop_group, (int) $cart->id_shop ); } } return true; } /** * Map and send data to api * * @param string $path * @param string $mapper_name * @param Object $object * @param int $id_shop_group * @param int $id_shop * @param string $sync * @return boolean */ public function callCG($path, $mapper_name, $object, $id_shop_group = 0, $id_shop = 0, $sync = false, $params = array()) { $success = 0; $site_id = ''; $auth_key = ''; if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $success = Configuration::get('CARTSG_API_SUCCESS'); $site_id = Configuration::get('CARTSG_SITE_ID'); $auth_key = Configuration::get('CARTSG_API_AUTH_KEY'); } else { $success = Configuration::get('CARTSG_API_SUCCESS', null, $id_shop_group, $id_shop); $site_id = Configuration::get('CARTSG_SITE_ID', null, $id_shop_group, $id_shop); $auth_key = Configuration::get('CARTSG_API_AUTH_KEY', null, $id_shop_group, $id_shop); } if ($success && ! empty($site_id) && ! empty($auth_key)) { if ($mapper_name) { $id_lang_default = (int) Configuration::get('PS_LANG_DEFAULT'); $mapper = CartGuruMapperFactory::getMapper( $mapper_name, $id_lang_default, (int) $id_shop_group, (int) $id_shop ); $object = $mapper->create($object, $params); if ($object == null) { return false; } $object['siteId'] = $site_id; } $call_uid = $path.'_'.self::getUniqueVar($object); //Is already call in same request $this->logDebug('UID :'.$call_uid); if (!isset(self::$c_calls[$call_uid])) { try { $api = new CartGuruRAPI($site_id, $auth_key); $this->logDebug('UID:'.$call_uid.' / START CALL API'); $api->post($path, $object, $sync); $this->logDebug('UID:'.$call_uid.' / END CALL API'); self::$c_calls[$call_uid] = 1; return true; } catch (Exception $e) { } } else { $this->logDebug('UID:'.$call_uid.' / NO CALL NEED'); } return true; } return false; } public static function getUniqueVar($data) { if (!isset($data)) { return ''; } return md5(serialize($data)); } /** * Compatibility with all PS Version * * @param array/string $warning * @return string */ public function displayWarn($warning) { $this->context->smarty->assign( array( 'warning' => $warning, 'ps_version' => _PS_VERSION_ ) ); $output = $this->context->smarty->fetch( _PS_ROOT_DIR_.'/modules/cartsguru/views/templates/admin/helper_warning.tpl' ); return $output; } public function generateProductImageCG() { $start_time = time(); ini_set('max_execution_time', $this->max_execution_time); // ini_set may be disabled, we need the real value $this->max_execution_time = (int)ini_get('max_execution_time'); $img_cg_type = new ImageType((int)Configuration::get('CARTSG_IMAGE_TYPE_ID')); $dir = _PS_PROD_IMG_DIR_; foreach (Image::getAllImages() as $image) { $imageObj = new Image($image['id_image']); $existing_img = ''; if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $existing_img = $dir.$image['id_product'].'-'.$image['id_image'].'.jpg'; } else { $existing_img = $dir.$imageObj->getExistingImgPath().'.jpg'; } if (file_exists($existing_img) && filesize($existing_img)) { if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $to_img_file_cg = $dir.$image['id_product'].'-'.$image['id_image'].'-'.Tools::stripslashes($img_cg_type->name).'.jpg'; } else { $to_img_file_cg = $dir.$imageObj->getExistingImgPath().'-'.Tools::stripslashes($img_cg_type->name).'.jpg'; } if (!file_exists($to_img_file_cg)) { if (version_compare(_PS_VERSION_, '1.5.0', '<')) { if (!imageResize( $existing_img, $to_img_file_cg, (int)$img_cg_type->width, (int)$img_cg_type->height ) ) { $this->post_errors[] = sprintf( 'Original image is corrupt (%s) for product ID %2$d or bad permission on folder', $existing_img, (int)$imageObj->id_product ); } } else { if (!ImageManager::resize( $existing_img, $to_img_file_cg, (int)$img_cg_type->width, (int)$img_cg_type->height )) { $this->post_errors[] = sprintf( Tools::displayError('Original image is corrupt (%s) for product ID %2$d or bad permission on folder'), $existing_img, (int)$imageObj->id_product ); } } } } else { $this->post_errors[] = sprintf( 'Original image is missing or empty (%1$s) for product ID %2$d', $existing_img, (int)$imageObj->id_product ); } if (time() - $start_time > $this->max_execution_time - 4) { return 'timeout'; } } return true; } public function deleteProductImageCG() { $productsImages = Image::getAllImages(); $dir = _PS_PROD_IMG_DIR_; foreach ($productsImages as $image) { $imageObj = new Image($image['id_image']); $imageObj->id_product = $image['id_product']; $img_cg_type = new ImageType((int) Configuration::get('CARTSG_IMAGE_TYPE_ID')); if (version_compare(_PS_VERSION_, '1.5.0', '<')) { $img_folder = $dir.$image['id_product'].'-'.$image['id_image']; } else { $img_folder = $dir.$imageObj->getImgFolder(); } if (file_exists($img_folder)) { $toDel = scandir($img_folder); foreach ($toDel as $d) { if (preg_match('/^[0-9]+\-' . $img_cg_type->name . '\.jpg$/', $d)) { if (file_exists($img_folder . $d)) { unlink($img_folder . $d); } } } } } } private function import($siteId, $type, $api_path) { $isMultiStoreSupported = version_compare(_PS_VERSION_, '1.5.0', '>='); $sql = 'SELECT id_' . $type . ' AS id FROM ' . _DB_PREFIX_ . $type; if ($type === 'order') { $sql .= 's'; } //Need filter on the good shop if ($isMultiStoreSupported) { $id_shop = (int)Context::getContext()->shop->id; $sql .= ' WHERE id_shop = ' . $id_shop; } $sql .= ' ORDER BY date_add DESC LIMIT 250'; if ($results = Db::getInstance()->ExecuteS($sql)) { $id_lang_default = (int) Configuration::get('PS_LANG_DEFAULT'); $items = array(); foreach ($results as $row) { $item = $type === 'order' ? new Order($row['id']) : new Cart($row['id']); $mapper = CartGuruMapperFactory::getMapper( $type, $id_lang_default, $isMultiStoreSupported ? $item->id_shop_group : 0, $isMultiStoreSupported ? $item->id_shop : 0 ); $cart_guru_data = $mapper->create($item); if ($cart_guru_data) { $cart_guru_data['siteId'] = $siteId; $items[] = $cart_guru_data; } } $this->callCG( $api_path, null, $items ); } } private function importCarts($siteId) { $this->import($siteId, 'cart', CartGuruRAPI::API_PATH_IMPORT_CARTS); } private function importOrders($siteId) { $this->import($siteId, 'order', CartGuruRAPI::API_PATH_IMPORT_ORDERS); } }