diff --git a/src/config/objects.xml b/src/config/objects.xml index f5ca5cf..4480004 100644 --- a/src/config/objects.xml +++ b/src/config/objects.xml @@ -15,6 +15,20 @@ personalDiscount discountCardNumber + id + externalId + nickName + vip + bad + customFields + personalDiscount + discountCardNumber + manager + address + mainCustomerContact + companyInn + company + index country region @@ -62,6 +76,7 @@ shipmentStore shipmentDate shipped + contact payment amount diff --git a/src/include/abstracts/class-wc-retailcrm-abstract-builder.php b/src/include/abstracts/class-wc-retailcrm-abstract-builder.php new file mode 100644 index 0000000..a0842b3 --- /dev/null +++ b/src/include/abstracts/class-wc-retailcrm-abstract-builder.php @@ -0,0 +1,86 @@ +data = $data; + return $this; + } + + /** + * @return array|mixed + */ + public function getData() + { + return $this->data; + } + + /** + * @return $this|\WC_Retailcrm_Builder_Interface + */ + public function reset() + { + $this->data = array(); + return $this; + } + + /** + * Returns key if it's present in data array (or object which implements ArrayAccess). + * Returns default value if key is not present in data, or data is not accessible as array. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + protected function dataValue($key, $default = '') + { + return self::arrayValue($this->data, $key, $default); + } + + /** + * Returns key from array if it's present in array + * + * @param array|\ArrayObject $data + * @param mixed $key + * @param string $default + * + * @return mixed|string + */ + protected static function arrayValue($data, $key, $default = '') + { + if (!is_array($data) && !($data instanceof ArrayAccess)) { + return $default; + } + + if (array_key_exists($key, $data) && !empty($data[$key])) { + return $data[$key]; + } + + return $default; + } + + /** + * @return \WC_Retailcrm_Builder_Interface + */ + abstract public function build(); + + /** + * Returns builder result. Should return null if WC_Retailcrm_Abstract_Builder::isBuilt() == false. + * + * @return mixed|null + */ + abstract public function getResult(); +} diff --git a/src/include/class-wc-retailcrm-customers.php b/src/include/class-wc-retailcrm-customers.php index 5690cf1..4caa04c 100644 --- a/src/include/class-wc-retailcrm-customers.php +++ b/src/include/class-wc-retailcrm-customers.php @@ -480,7 +480,8 @@ if (!class_exists('WC_Retailcrm_Customers')) : } } } else { - $customer = reset($search['customers']); + $dataCustomers = $search['customers']; + $customer = reset($dataCustomers); } } else { $customer = !empty($search['customer']) ? $search['customer'] : false; @@ -536,7 +537,8 @@ if (!class_exists('WC_Retailcrm_Customers')) : if ($returnGroup) { return $search['customersCorporate']; } else { - $customer = reset($search['customersCorporate']); + $dataCorporateCustomers = $search['customersCorporate']; + $customer = reset($dataCorporateCustomers); } } elseif (isset($search['customerCorporate'])) { $customer = $search['customerCorporate']; diff --git a/src/include/class-wc-retailcrm-history.php b/src/include/class-wc-retailcrm-history.php index 729b037..6d4c100 100644 --- a/src/include/class-wc-retailcrm-history.php +++ b/src/include/class-wc-retailcrm-history.php @@ -101,13 +101,14 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : * History customers * * @param string $date - * @param int $since_id + * @param int $sinceId * - * @return null + * @return void * @throws \Exception */ - protected function customersHistory($date, $since_id) + protected function customersHistory($date, $sinceId) { +<<<<<<< HEAD if ($since_id) { $response = $this->retailcrm->customersHistory(array('sinceId' => $since_id)); } else { @@ -133,43 +134,73 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : 'address.country' => 'billing_country', 'address.city' => 'billing_city' ); +======= + $filter = array('startDate' => $date); - foreach ($history as $record) { - if ($record['source'] == 'api' && $record['apiKey']['current'] == true) { + if ($sinceId) { + $filter = array('sinceId' => $sinceId); + } + + $request = new WC_Retailcrm_Paginated_Request(); + $history = $request + ->setApi($this->retailcrm) + ->setMethod('customersHistory') + ->setParams(array($filter, '{{page}}')) + ->setDataKey('history') + ->setLimit(100) + ->execute() + ->getData(); + + if (!empty($history)) { + $builder = new WC_Retailcrm_WC_Customer_Builder(); + $lastChange = end($history); + $customers = WC_Retailcrm_History_Assembler::assemblyCustomer($history); + WC_Retailcrm_Plugin::$history_run = true; +>>>>>>> WIP: Change client in the order (not ready at this point; also tests should fail) + + foreach ($customers as $crmCustomer) { + if (!isset($crmCustomer['externalId'])) { continue; } - if (isset($record['customer']['externalId'])) { - $customer = new WC_Customer($record['customer']['externalId']); + try { + $builder->reset(); - if ($customer->get_id() == 0) { + if (!$builder->loadExternalId($crmCustomer['externalId'])) { + WC_Retailcrm_Logger::addCaller(__METHOD__, sprintf( + 'Customer with id=%s is not found in the DB, skipping...', + $crmCustomer['externalId'] + )); continue; } - } - WC_Retailcrm_Plugin::$history_run = true; + $wcCustomer = $builder + ->setData($crmCustomer) + ->build() + ->getResult(); - if (isset($mapping[$record['field']]) && isset($record['customer']['externalId'])) { - if ($record['newValue']) { - if (is_array($mapping[$record['field']])) { - foreach ($mapping[$record['field']] as $mappingField) { - update_user_meta($record['customer']['externalId'], $mappingField, $record['newValue']); - } - } else { - update_user_meta($record['customer']['externalId'], $mapping[$record['field']], $record['newValue']); - } + if ($wcCustomer instanceof WC_Customer) { + $wcCustomer->save(); } + + WC_Retailcrm_Logger::debug(__METHOD__, 'Updated WC_Customer:', $wcCustomer); + } catch (\Exception $exception) { + WC_Retailcrm_Logger::error(sprintf( + 'Error while trying to process history: %s', + $exception->getMessage() + )); + WC_Retailcrm_Logger::error(sprintf( + '%s:%d', + $exception->getFile(), + $exception->getLine() + )); + WC_Retailcrm_Logger::error($exception->getTraceAsString()); } - - WC_Retailcrm_Plugin::$history_run = false; } - } - if (isset($new_since_id)) { - update_option('retailcrm_customers_history_since_id', $new_since_id); + update_option('retailcrm_customers_history_since_id', $lastChange['id']); + WC_Retailcrm_Plugin::$history_run = false; } - - return; } /** @@ -182,22 +213,33 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : */ protected function ordersHistory($date, $since_id) { + $filter = array('startDate' => $date); $options = array_flip(array_filter($this->retailcrm_settings)); if ($since_id) { - $response = $this->retailcrm->ordersHistory(array('sinceId' => $since_id)); - } else { - $response = $this->retailcrm->ordersHistory(array('startDate' => $date)); + $filter = array('sinceId' => $since_id); } +<<<<<<< HEAD if (!empty($response) && $response->isSuccessful()) { if (empty($response['history'])) { return false; } +======= + $request = new WC_Retailcrm_Paginated_Request(); + $history = $request + ->setApi($this->retailcrm) + ->setMethod('ordersHistory') + ->setParams(array($filter, '{{page}}')) + ->setDataKey('history') + ->setLimit(100) + ->execute() + ->getData(); +>>>>>>> WIP: Change client in the order (not ready at this point; also tests should fail) - $history = $response['history']; + if (!empty($history)) { $last_change = end($history); - $historyAssembly = self::assemblyOrder($response['history']); + $historyAssembly = WC_Retailcrm_History_Assembler::assemblyOrder($history); WC_Retailcrm_Plugin::$history_run = true; @@ -559,7 +601,7 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : $customerId = isset($order['customer']['externalId']) ? $order['customer']['externalId'] : null; - if ($order['customer']['type'] == 'customer_corporate') { + if (self::isOrderCorporate($order)) { $customerId = isset($order['contact']['externalId']) ? $order['contact']['externalId'] : null; } @@ -573,9 +615,18 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : /** @var WC_Order|WP_Error $wc_order */ $wc_order = wc_create_order($args); $customer = $order['customer']; + $contactOrCustomer = array(); $address = isset($order['customer']['address']) ? $order['customer']['address'] : array(); $companyName = ''; + if (self::isOrderCorporate($order)) { + if (isset($order['contact'])) { + $contactOrCustomer = $order['contact']; + } + } else { + $contactOrCustomer = $customer; + } + if ($wc_order instanceof WP_Error) { WC_Retailcrm_Logger::add(sprintf( '[%d] error while creating order: %s', @@ -590,12 +641,12 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : $wc_order->add_order_note($order['managerComment'], 0, false); } - if (isset($order['customer']['type']) - && $order['customer']['type'] == 'customer_corporate' - && !empty($order['customer']['mainCompany']) - && isset($order['customer']['mainCompany']['name']) + // TODO Check if that works; also don't forget to set this company field while creating order from CMS! + if (self::isOrderCorporate($order) + && !empty($order['company']) + && isset($order['company']['name']) ) { - $companyName = $order['customer']['mainCompany']['name']; + $companyName = $order['company']['name']; } $address_shipping = array( @@ -611,11 +662,11 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : ); $address_billing = array( - 'first_name' => $customer['firstName'], - 'last_name' => isset($customer['lastName']) ? $customer['lastName'] : '', + 'first_name' => $contactOrCustomer['firstName'], + 'last_name' => isset($contactOrCustomer['lastName']) ? $contactOrCustomer['lastName'] : '', 'company' => $companyName, 'email' => isset($customer['email']) ? $customer['email'] : '', - 'phone' => isset($customer['phones'][0]['number']) ? $customer['phones'][0]['number'] : '', + 'phone' => isset($contactOrCustomer['phones'][0]['number']) ? $contactOrCustomer['phones'][0]['number'] : '', 'address_1' => isset($address['text']) ? $address['text'] : '', 'address_2' => '', 'city' => isset($address['city']) ? $address['city'] : '', @@ -872,154 +923,15 @@ if ( ! class_exists( 'WC_Retailcrm_History' ) ) : } /** - * @param array $orderHistory + * Returns true if provided crm order is corporate * - * @return array + * @param array $order + * + * @return bool */ - public static function assemblyOrder($orderHistory) + private static function isOrderCorporate($order) { - $fields = array(); - - if (file_exists(__DIR__ . '/../config/objects.xml')) { - $objects = simplexml_load_file(__DIR__ . '/../config/objects.xml'); - - foreach($objects->fields->field as $object) { - $fields[(string)$object["group"]][(string)$object["id"]] = (string)$object; - } - } - - $orders = array(); - - foreach ($orderHistory as $change) { - if ($change['source'] == 'api' - && isset($change['apiKey']['current']) - && $change['apiKey']['current'] == true - ) { - continue; - } - - $change['order'] = self::removeEmpty($change['order']); - if(isset($change['order']['items']) && $change['order']['items']) { - $items = array(); - foreach($change['order']['items'] as $item) { - if(isset($change['created'])) { - $item['create'] = 1; - } - $items[$item['id']] = $item; - } - $change['order']['items'] = $items; - } - - if(isset($change['order']['contragent']['contragentType']) && $change['order']['contragent']['contragentType']) { - $change['order']['contragentType'] = $change['order']['contragent']['contragentType']; - unset($change['order']['contragent']); - } - - if (!empty($orders) && isset($orders[$change['order']['id']])) { - $orders[$change['order']['id']] = array_merge($orders[$change['order']['id']], $change['order']); - } else { - $orders[$change['order']['id']] = $change['order']; - } - - if (isset($change['item']) && $change['item']) { - if(isset($orders[$change['order']['id']]['items'][$change['item']['id']])) { - $orders[$change['order']['id']]['items'][$change['item']['id']] = array_merge($orders[$change['order']['id']]['items'][$change['item']['id']], $change['item']); - } else { - $orders[$change['order']['id']]['items'][$change['item']['id']] = $change['item']; - } - - if ($change['oldValue'] === null - && $change['field'] == 'order_product' - ) { - $orders[$change['order']['id']]['items'][$change['item']['id']]['create'] = true; - } - - if ($change['newValue'] === null - && $change['field'] == 'order_product' - ) { - $orders[$change['order']['id']]['items'][$change['item']['id']]['delete'] = true; - } - - if (!isset($orders[$change['order']['id']]['items'][$change['item']['id']]['create']) - && isset($fields['item'][$change['field']]) - && $fields['item'][$change['field']] - ) { - $orders[$change['order']['id']]['items'][$change['item']['id']][$fields['item'][$change['field']]] = $change['newValue']; - } - } elseif ($change['field'] == 'payments' && isset($change['payment'])) { - if ($change['newValue'] !== null) { - $orders[$change['order']['id']]['payments'][] = self::newValue($change['payment']); - } - } else { - if (isset($fields['delivery'][$change['field']]) && $fields['delivery'][$change['field']] == 'service') { - $orders[$change['order']['id']]['delivery']['service']['code'] = self::newValue($change['newValue']); - } elseif (isset($fields['delivery'][$change['field']]) && $fields['delivery'][$change['field']]) { - $orders[$change['order']['id']]['delivery'][$fields['delivery'][$change['field']]] = self::newValue($change['newValue']); - } elseif (isset($fields['orderAddress'][$change['field']]) && $fields['orderAddress'][$change['field']]) { - $orders[$change['order']['id']]['delivery']['address'][$fields['orderAddress'][$change['field']]] = $change['newValue']; - } elseif (isset($fields['integrationDelivery'][$change['field']]) && $fields['integrationDelivery'][$change['field']]) { - $orders[$change['order']['id']]['delivery']['service'][$fields['integrationDelivery'][$change['field']]] = self::newValue($change['newValue']); - } elseif (isset($fields['customerContragent'][$change['field']]) && $fields['customerContragent'][$change['field']]) { - $orders[$change['order']['id']][$fields['customerContragent'][$change['field']]] = self::newValue($change['newValue']); - } elseif (strripos($change['field'], 'custom_') !== false) { - $orders[$change['order']['id']]['customFields'][str_replace('custom_', '', $change['field'])] = self::newValue($change['newValue']); - } elseif (isset($fields['order'][$change['field']]) && $fields['order'][$change['field']]) { - $orders[$change['order']['id']][$fields['order'][$change['field']]] = self::newValue($change['newValue']); - } - - if (isset($change['created'])) { - $orders[$change['order']['id']]['create'] = 1; - } - - if (isset($change['deleted'])) { - $orders[$change['order']['id']]['deleted'] = 1; - } - } - } - - return $orders; - } - - public static function assemblyCustomer($customerHistory) - { - $customers = array(); - foreach ($customerHistory as $change) { - $change['order'] = self::removeEmpty($change['customer']); - - if (!empty($customers[$change['customer']['id']]) && $customers[$change['customer']['id']]) { - $customers[$change['customer']['id']] = array_merge($customers[$change['customer']['id']], $change['customer']); - } else { - $customers[$change['customer']['id']] = $change['customer']; - } - } - - return $customers; - } - - public static function newValue($value) - { - if (isset($value['code'])) { - return $value['code']; - } else { - return $value; - } - } - - public static function removeEmpty($inputArray) - { - $outputArray = array(); - if (!empty($inputArray)) { - foreach ($inputArray as $key => $element) { - if(!empty($element) || $element === 0 || $element === '0'){ - if (is_array($element)) { - $element = self::removeEmpty($element); - } - $outputArray[$key] = $element; - } - } - } - - return $outputArray; + return isset($order['customer']['type']) && $order['customer']['type'] == 'customer_corporate'; } } diff --git a/src/include/class-wc-retailcrm-plugin.php b/src/include/class-wc-retailcrm-plugin.php index f66761d..ea5722a 100644 --- a/src/include/class-wc-retailcrm-plugin.php +++ b/src/include/class-wc-retailcrm-plugin.php @@ -164,7 +164,6 @@ class WC_Retailcrm_Plugin { return $result; } - /** * Check running history * diff --git a/src/include/components/class-wc-retailcrm-customer-data-replacer.php b/src/include/components/class-wc-retailcrm-customer-data-replacer.php new file mode 100644 index 0000000..ec9eee7 --- /dev/null +++ b/src/include/components/class-wc-retailcrm-customer-data-replacer.php @@ -0,0 +1,152 @@ +reset(); + } + + /** + * In fact, this will execute customer change in provided order. + * This will not build anything. + * + * @return $this|\WC_Retailcrm_Builder_Interface + */ + public function build() + { + $this->data->validate(); + + $wcOrder = $this->data->getWcOrder(); + $newCustomer = $this->data->getNewCustomer(); + $newCorporateCustomer = $this->data->getNewCorporateCustomer(); + $newContact = $this->data->getNewContact(); + $newCompany = $this->data->getNewCompany(); + + if (!empty($newCustomer)) { + $this->processChangeToRegular($wcOrder, $newCustomer); + return $this; + } + + if (!empty($newContact)) { + $this->processChangeToContactPerson($wcOrder, $newContact); + } + + if (!empty($newCompany)) { + $this->updateCompany($wcOrder, $newCorporateCustomer, $newCompany); + } + + return $this; + } + + /** + * Change order customer to regular one + * + * @param \WC_Order $wcOrder + * @param array $newCustomer + */ + public function processChangeToRegular($wcOrder, $newCustomer) + { + // TODO: Implement + } + + /** + * Change order customer to corporate one (we only care about it's contact) + * + * @param \WC_Order $wcOrder + * @param array $newContact + */ + public function processChangeToContactPerson($wcOrder, $newContact) + { + // TODO: Implement + } + + /** + * Update company in the order + * + * @param WC_Order $wcOrder + * @param array $corporateCustomer + * @param array $company + */ + public function updateCompany($wcOrder, $corporateCustomer, $company) + { + // TODO: Implement + } + + /** + * @return $this|\WC_Retailcrm_Builder_Interface + */ + public function reset() + { + $this->data = new WC_Retailcrm_Customer_Data_Replacer_State(); + $this->result = null; + return $this; + } + + /** + * Set initial state into component + * + * @param \WC_Retailcrm_Customer_Data_Replacer_State $data + * + * @return $this|\WC_Retailcrm_Builder_Interface + */ + public function setData($data) + { + if (!($data instanceof WC_Retailcrm_Customer_Data_Replacer_State)) { + throw new \InvalidArgumentException('Invalid data type'); + } + + $this->data = $data; + return $this; + } + + /** + * @return \WC_Retailcrm_Customer_Data_Replacer_Result|null + */ + public function getResult() + { + return $this->result; + } + + /** + * @return \WC_Retailcrm_Customer_Data_Replacer_State + */ + public function getData() + { + return $this->data; + } + + /** + * Returns WC_Customer by id. Returns null if there's no such customer. + * + * @param int $id + * + * @return \WC_Customer|null + */ + private function getWcCustomerById($id) + { + try { + return new WC_Customer($id); + } catch (\Exception $exception) { + return null; + } + } +} diff --git a/src/include/components/class-wc-retailcrm-history-assembler.php b/src/include/components/class-wc-retailcrm-history-assembler.php new file mode 100644 index 0000000..85ffe09 --- /dev/null +++ b/src/include/components/class-wc-retailcrm-history-assembler.php @@ -0,0 +1,402 @@ + &$customer) { + if (empty($customer['id']) && !empty($id)) { + $customer['id'] = $id; + $customer['deleted'] = true; + } + } + + return $customersCorporate; + } + + /** + * Returns mapping data for retailCRM entities. Used to assembly entities from history. + * + * @param array $groupFilter + * + * @return array + */ + private static function getMappingValues($groupFilter = array()) + { + $fields = array(); + $mappingFile = implode( + DIRECTORY_SEPARATOR, + array(WP_CONTENT_DIR, 'plugins', 'woo-retailcrm', 'config', 'objects.xml') + ); + + if (file_exists($mappingFile)) { + $objects = simplexml_load_file($mappingFile); + + foreach($objects->fields->field as $object) { + if (empty($groupFilter) || in_array($object["group"], $groupFilter)) { + $fields[(string)$object["group"]][(string)$object["id"]] = (string)$object; + } + } + } + + return $fields; + } + + /** + * Value accessor + * + * @param array $value + * + * @return mixed + */ + private static function newValue($value) + { + if (isset($value['code'])) { + return $value['code']; + } else { + return $value; + } + } + + /** + * Returns array without values which are considered empty + * + * @param array|\ArrayAccess $inputArray + * + * @return array + */ + private static function removeEmpty($inputArray) + { + $outputArray = array(); + + if (!empty($inputArray)) { + foreach ($inputArray as $key => $element) { + if(!empty($element) || $element === 0 || $element === '0'){ + if (is_array($element)) { + $element = self::removeEmpty($element); + } + + $outputArray[$key] = $element; + } + } + } + + return $outputArray; + } + + /** + * Filters out history by these terms: + * - Changes from current API key will be added only if CMS changes are more actual than history. + * - All other changes will be merged as usual. + * It fixes these problems: + * - Changes from current API key are merged when it's not needed. + * - Changes from CRM can overwrite more actual changes from CMS due to ignoring current API key changes. + * + * @param array $historyEntries Raw history from CRM + * @param string $recordType Entity field name, e.g. `customer` or `order`. + * + * @return array + */ + private static function filterHistory($historyEntries, $recordType) + { + $history = array(); + $organizedHistory = array(); + $notOurChanges = array(); + + foreach ($historyEntries as $entry) { + if (!isset($entry[$recordType]['externalId'])) { + if ($entry['source'] == 'api' + && isset($change['apiKey']['current']) + && $entry['apiKey']['current'] == true + && $entry['field'] != 'externalId' + ) { + continue; + } else { + $history[] = $entry; + } + + continue; + } + + $externalId = $entry[$recordType]['externalId']; + $field = $entry['field']; + + if (!isset($organizedHistory[$externalId])) { + $organizedHistory[$externalId] = array(); + } + + if (!isset($notOurChanges[$externalId])) { + $notOurChanges[$externalId] = array(); + } + + if ($entry['source'] == 'api' + && isset($entry['apiKey']['current']) + && $entry['apiKey']['current'] == true + ) { + if (isset($notOurChanges[$externalId][$field]) || $entry['field'] == 'externalId') { + $organizedHistory[$externalId][] = $entry; + } else { + continue; + } + } else { + $organizedHistory[$externalId][] = $entry; + $notOurChanges[$externalId][$field] = true; + } + } + + unset($notOurChanges); + + foreach ($organizedHistory as $historyChunk) { + $history = array_merge($history, $historyChunk); + } + + return $history; + } +} \ No newline at end of file diff --git a/src/include/class-wc-retailcrm-logger.php b/src/include/components/class-wc-retailcrm-logger.php similarity index 50% rename from src/include/class-wc-retailcrm-logger.php rename to src/include/components/class-wc-retailcrm-logger.php index 90816eb..319ecb4 100644 --- a/src/include/class-wc-retailcrm-logger.php +++ b/src/include/components/class-wc-retailcrm-logger.php @@ -52,24 +52,60 @@ if (!class_exists('WC_Retailcrm_Logger') && class_exists('WC_Log_Levels')): } /** - * Debug logging. Contains a lot of debug data like full requests & responses. + * Regular logging with caller prefix * - * @param string $method + * @param string $caller * @param string $message * @param string $level */ - public static function debug($method, $message, $level = WC_Log_Levels::DEBUG) + public static function addCaller($caller, $message, $level = WC_Log_Levels::NOTICE) + { + self::add(sprintf('<%s> => %s', $caller, $message), $level); + } + + /** + * Log error + * + * @param string $message + */ + public static function error($message) + { + self::add($message, WC_Log_Levels::ERROR); + } + + /** + * Debug logging. Contains a lot of debug data like full requests & responses. + * This log will work only if debug mode is enabled (see retailcrm_is_debug() for details). + * Caller should be specified, or message will be ignored at all. + * + * @param string $method + * @param array $messages + */ + public static function debug($method, ...$messages) { if (retailcrm_is_debug()) { - if (!empty($method)) { - $message = sprintf( - '<%s> => %s', - $method, - $message + if (!empty($method) && !empty($messages)) { + $result = substr( + array_reduce( + $messages, + function ($carry, $item) { + $carry .= ' ' . print_r($item, true); + return $carry; + } + ), + 1 + ); + + self::getInstance()->add( + self::HANDLE . '_debug', + sprintf( + '<%s> => %s', + $method, + $result + ), + WC_Log_Levels::DEBUG ); } - - self::getInstance()->add(self::HANDLE . '_debug', $message, $level); } } } diff --git a/src/include/components/class-wc-retailcrm-paginated-request.php b/src/include/components/class-wc-retailcrm-paginated-request.php new file mode 100644 index 0000000..9c5fa28 --- /dev/null +++ b/src/include/components/class-wc-retailcrm-paginated-request.php @@ -0,0 +1,188 @@ +reset(); + } + + /** + * Sets retailCRM api client to request + * + * @param \WC_Retailcrm_Proxy|\WC_Retailcrm_Client_V5 $api + * + * @return self + */ + public function setApi($api) + { + $this->api = $api; + return $this; + } + + /** + * Sets API client method to request + * + * @param string $method + * + * @return self + */ + public function setMethod($method) + { + $this->method = $method; + return $this; + } + + /** + * Sets method params for API client (leave `{{page}}` instead of page and `{{limit}}` instead of limit) + * + * @param array $params + * + * @return self + */ + public function setParams($params) + { + $this->params = $params; + return $this; + } + + /** + * Sets dataKey (key with data in response) + * + * @param string $dataKey + * + * @return self + */ + public function setDataKey($dataKey) + { + $this->dataKey = $dataKey; + return $this; + } + + /** + * Sets record limit per request + * + * @param int $limit + * + * @return self + */ + public function setLimit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Executes request + * + * @return $this + */ + public function execute() + { + $this->data = array(); + $response = true; + $page = 1; + + do { + $response = call_user_func_array( + array($this->api, $this->method), + $this->buildParams($this->params, $page) + ); + + if ($response instanceof WC_Retailcrm_Response && $response->offsetExists($this->dataKey)) { + $this->data = array_merge($response[$this->dataKey]); + $page = $response['pagination']['currentPage'] + 1; + } + + time_nanosleep(0, 300000000); + } while ($response && (isset($response['pagination']) + && $response['pagination']['currentPage'] < $response['pagination']['totalPageCount'])); + + return $this; + } + + /** + * Returns data + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Reset paginated request + * + * @return $this + */ + public function reset() + { + $this->method = ''; + $this->limit = 100; + $this->data = array(); + + return $this; + } + + /** + * buildParams + * + * @param array $placeholderParams + * @param int $currentPage + * + * @return mixed + */ + private function buildParams($placeholderParams, $currentPage) + { + foreach ($placeholderParams as $key => $param) { + if ($param == '{{page}}') { + $placeholderParams[$key] = $currentPage; + } + + if ($param == '{{limit}}') { + $placeholderParams[$key] = $this->limit; + } + } + + return $placeholderParams; + } +} diff --git a/src/include/customer/woocommerce/class-wc-retailcrm-wc-customer-builder.php b/src/include/customer/woocommerce/class-wc-retailcrm-wc-customer-builder.php new file mode 100644 index 0000000..f65cf77 --- /dev/null +++ b/src/include/customer/woocommerce/class-wc-retailcrm-wc-customer-builder.php @@ -0,0 +1,235 @@ +reset(); + } + + /** + * @param string $firstName + * + * @return $this + */ + public function setFirstName($firstName) + { + $this->data['firstName'] = $firstName; + return $this; + } + + /** + * @param string $lastName + * + * @return $this + */ + public function setLastName($lastName) + { + $this->data['lastName'] = $lastName; + return $this; + } + + /** + * @param string $email + * + * @return $this + */ + public function setEmail($email) + { + $this->data['email'] = $email; + return $this; + } + + /** + * @param string $externalId + * + * @return $this + */ + public function setExternalId($externalId) + { + $this->data['externalId'] = $externalId; + return $this; + } + + /** + * @param array $phones + * + * @return $this + */ + public function setPhones($phones) + { + if (self::isPhonesArrayValid($phones)) { + $this->data['phones'] = $phones; + } + + return $this; + } + + /** + * @param array $address + * + * @return $this + */ + public function setAddress($address) + { + if (is_array($address)) { + $this->data['address'] = $address; + } + + return $this; + } + + /** + * @param \WC_Customer $customer + * + * @return $this + */ + public function setWcCustomer($customer) + { + if ($customer instanceof WC_Customer) { + $this->customer = $customer; + } + + return $this; + } + + /** + * Sets provided externalId and loads associated customer from DB (it it exists there). + * Returns true if everything went find; returns false if customer wasn't found. + * + * @param string $externalId + * + * @return bool + * @throws \Exception + */ + public function loadExternalId($externalId) + { + try { + $wcCustomer = new WC_Customer($externalId); + } catch (\Exception $exception) { + return false; + } + + $this->setExternalId($externalId); + $this->setWcCustomer($wcCustomer); + + return true; + } + + public function reset() + { + parent::reset(); + $this->customer = new WC_Customer(); + + return $this; + } + + /** + * Fill WC_Customer fields with customer data from retailCRM. + * If field is not present in retailCRM customer - it will remain unchanged. + * + * @return $this|\WC_Retailcrm_Builder_Interface + */ + public function build() + { + $this->checkBuilderValidity(); + WC_Retailcrm_Logger::debug(__METHOD__, 'Building WC_Customer from data:', $this->data); + $this->customer->set_first_name($this->dataValue('firstName', $this->customer->get_first_name())); + $this->customer->set_last_name($this->dataValue('lastName', $this->customer->get_last_name())); + $this->customer->set_billing_email($this->dataValue('email', $this->customer->get_billing_email())); + $phones = $this->dataValue('phones', array()); + + if (count($phones) > 0) { + $phoneData = reset($phones); + + if (is_array($phoneData) && isset($phoneData['number'])) { + $this->customer->set_billing_phone($phoneData['number']); + } + } + + if (!empty($this->dataValue('address'))) { + $address = $this->dataValue('address'); + + $this->customer->set_billing_state(self::arrayValue( + $address, + 'region', + $this->customer->get_billing_state()) + ); + $this->customer->set_billing_postcode(self::arrayValue( + $address, + 'index', + $this->customer->get_billing_postcode()) + ); + $this->customer->set_billing_country(self::arrayValue( + $address, + 'country', + $this->customer->get_billing_country()) + ); + $this->customer->set_billing_city(self::arrayValue( + $address, + 'city', + $this->customer->get_billing_city()) + ); + } + + return $this; + } + + /** + * @return mixed|\WC_Customer|null + */ + public function getResult() + { + return $this->customer; + } + + /** + * Throws an exception if internal state is not ready for data building. + * + * @throws \RuntimeException + */ + private function checkBuilderValidity() + { + if (empty($this->data)) { + throw new \RuntimeException('Empty data'); + } + + if (!is_array($this->data)) { + throw new \RuntimeException('Data must be an array'); + } + } + + /** + * Returns true if provided variable contains array with customer phones. + * + * @param mixed $phones + * + * @return bool + */ + private static function isPhonesArrayValid($phones) + { + if (!is_array($phones)) { + return false; + } + + foreach ($phones as $phone) { + if (!is_array($phone) || count($phone) != 1 || !array_key_exists('number', $phone)) { + return false; + } + } + + return true; + } +} diff --git a/src/include/customer/woocommerce/class-wc-retailcrm-wc-customer-corporate-builder.php b/src/include/customer/woocommerce/class-wc-retailcrm-wc-customer-corporate-builder.php new file mode 100644 index 0000000..784ffd7 --- /dev/null +++ b/src/include/customer/woocommerce/class-wc-retailcrm-wc-customer-corporate-builder.php @@ -0,0 +1,248 @@ +reset(); + } + + /** + * @param array $contactPerson + * + * @return \WC_Retailcrm_WC_Customer_Corporate_Builder + */ + public function setContactPerson($contactPerson) + { + $this->contactPerson = $contactPerson; + return $this; + } + + /** + * @return array + */ + public function getContactPerson() + { + return $this->contactPerson; + } + + /** + * @param \WC_Retailcrm_Builder_Interface $customerBuilder + */ + public function setCustomerBuilder($customerBuilder) + { + $this->customerBuilder = $customerBuilder; + } + + /** + * @param string $firstName + * + * @return $this + */ + public function setFirstName($firstName) + { + $this->contactPerson['firstName'] = $firstName; + return $this; + } + + /** + * @param string $lastName + * + * @return $this + */ + public function setLastName($lastName) + { + $this->contactPerson['lastName'] = $lastName; + return $this; + } + + /** + * @param string $email + * + * @return $this + */ + public function setEmail($email) + { + $this->contactPerson['email'] = $email; + return $this; + } + + /** + * @param string $externalId + * + * @return $this + */ + public function setExternalId($externalId) + { + $this->contactPerson['externalId'] = $externalId; + return $this; + } + + /** + * @param array $phones + * + * @return $this + */ + public function setPhones($phones) + { + if (self::isPhonesArrayValid($phones)) { + $this->contactPerson['phones'] = $phones; + } + + return $this; + } + + /** + * @param array $address + * + * @return $this + */ + public function setAddress($address) + { + if (is_array($address)) { + $this->contactPerson['address'] = $address; + } + + return $this; + } + + /** + * @param \WC_Customer $customer + * + * @return $this + */ + public function setWcCustomer($customer) + { + if ($customer instanceof WC_Customer) { + $this->customer = $customer; + } + + return $this; + } + + /** + * Sets provided externalId and loads associated customer from DB (it it exists there). + * Returns true if everything went find; returns false if customer wasn't found. + * + * @param string $externalId + * + * @return bool + * @throws \Exception + */ + public function loadExternalId($externalId) + { + try { + $wcCustomer = new WC_Customer($externalId); + } catch (\Exception $exception) { + return false; + } + + $this->setExternalId($externalId); + $this->setWcCustomer($wcCustomer); + + return true; + } + + public function reset() + { + parent::reset(); + $this->customer = new WC_Customer(); + $this->contactPerson = array(); + $this->customerBuilder = new WC_Retailcrm_WC_Customer_Builder(); + + return $this; + } + + /** + * Fill WC_Customer fields with customer data from retailCRM. + * If field is not present in retailCRM customer - it will remain unchanged. + * + * @return $this|\WC_Retailcrm_Builder_Interface + */ + public function build() + { + $this->checkBuilderValidity(); + WC_Retailcrm_Logger::debug( + __METHOD__, + 'Building WC_Customer from corporate data:', + $this->data, + "\nContact:", + $this->contactPerson + ); + + $wcCustomer = $this->customerBuilder + ->setData($this->contactPerson) + ->build() + ->getResult(); + + return $this; + } + + /** + * @return mixed|\WC_Customer|null + */ + public function getResult() + { + return $this->customer; + } + + /** + * Throws an exception if internal state is not ready for data building. + * + * @throws \RuntimeException + */ + private function checkBuilderValidity() + { + if (empty($this->data)) { + throw new \RuntimeException('Empty data'); + } + + if (!is_array($this->data)) { + throw new \RuntimeException('Data must be an array'); + } + } + + /** + * Returns true if provided variable contains array with customer phones. + * + * @param mixed $phones + * + * @return bool + */ + private static function isPhonesArrayValid($phones) + { + if (!is_array($phones)) { + return false; + } + + foreach ($phones as $phone) { + if (!is_array($phone) || count($phone) != 1 || !array_key_exists('number', $phone)) { + return false; + } + } + + return true; + } +} diff --git a/src/include/interfaces/class-wc-retailcrm-builder-interface.php b/src/include/interfaces/class-wc-retailcrm-builder-interface.php new file mode 100644 index 0000000..a493078 --- /dev/null +++ b/src/include/interfaces/class-wc-retailcrm-builder-interface.php @@ -0,0 +1,48 @@ +wcCustomer = $wcCustomer; + $this->wcOrder = $wcOrder; + + if ((!is_null($this->wcCustomer) && !($this->wcCustomer instanceof WC_Customer)) + || !($this->wcOrder instanceof WC_Order) + ) { + throw new \InvalidArgumentException(sprintf('Incorrect data provided to %s', __CLASS__)); + } + } + + /** + * @return \WC_Customer|null + */ + public function getWcCustomer() + { + return $this->wcCustomer; + } + + /** + * @return \WC_Order + */ + public function getWcOrder() + { + return $this->wcOrder; + } +} diff --git a/src/include/models/class-wc-retailcrm-customer-data-replacer-state.php b/src/include/models/class-wc-retailcrm-customer-data-replacer-state.php new file mode 100644 index 0000000..e7a0180 --- /dev/null +++ b/src/include/models/class-wc-retailcrm-customer-data-replacer-state.php @@ -0,0 +1,146 @@ +wcOrder; + } + + /** + * @param \WC_Order $wcOrder + * + * @return WC_Retailcrm_Customer_Data_Replacer_State + */ + public function setWcOrder($wcOrder) + { + $this->wcOrder = $wcOrder; + return $this; + } + + /** + * @return array + */ + public function getNewCustomer() + { + return $this->newCustomer; + } + + /** + * @param array $newCustomer + * + * @return WC_Retailcrm_Customer_Data_Replacer_State + */ + public function setNewCustomer($newCustomer) + { + $this->newCustomer = $newCustomer; + return $this; + } + + /** + * @return array + */ + public function getNewContact() + { + return $this->newContact; + } + + /** + * @param array $newContact + * + * @return WC_Retailcrm_Customer_Data_Replacer_State + */ + public function setNewContact($newContact) + { + $this->newContact = $newContact; + return $this; + } + + /** + * @return array + */ + public function getNewCorporateCustomer() + { + return $this->newCorporateCustomer; + } + + /** + * @param array $newCorporateCustomer + * + * @return WC_Retailcrm_Customer_Data_Replacer_State + */ + public function setNewCorporateCustomer($newCorporateCustomer) + { + $this->newCorporateCustomer = $newCorporateCustomer; + return $this; + } + + /** + * @return array + */ + public function getNewCompany() + { + return $this->newCompany; + } + + /** + * @param array $newCompany + * + * @return WC_Retailcrm_Customer_Data_Replacer_State + */ + public function setNewCompany($newCompany) + { + $this->newCompany = $newCompany; + return $this; + } + + /** + * Throws an exception if state is not valid + * + * @throws \InvalidArgumentException + * @return void + */ + public function validate() + { + if (empty($this->getWcOrder())) { + throw new \InvalidArgumentException('Empty WC_Order.'); + } + + if (empty($this->getNewCustomer()) + && empty($this->getNewContact()) + && empty($this->getNewCorporateCustomer()) + ) { + throw new \InvalidArgumentException('New customer, new contact and new corporate customer is empty.'); + } + + if (!empty($this->getNewCustomer()) + && (!empty($this->getNewContact()) || !empty($this->getNewCorporateCustomer())) + ) { + throw new \InvalidArgumentException( + 'Too much data in state - cannot determine which customer should be used.' + ); + } + } +} diff --git a/src/retailcrm.php b/src/retailcrm.php index 8c1392f..e3247fb 100644 --- a/src/retailcrm.php +++ b/src/retailcrm.php @@ -41,10 +41,19 @@ if (!class_exists( 'WC_Integration_Retailcrm')) : $this->load_plugin_textdomain(); if (class_exists( 'WC_Integration' )) { - require_once(dirname(__FILE__ ) . '/include/class-wc-retailcrm-logger.php'); + require_once(dirname(__FILE__) . '/include/interfaces/class-wc-retailcrm-builder-interface.php'); + require_once(dirname(__FILE__ ) . '/include/models/class-wc-retailcrm-customer-data-replacer-state.php'); + require_once(dirname(__FILE__ ) . '/include/models/class-wc-retailcrm-customer-data-replacer-result.php'); + require_once(dirname(__FILE__ ) . '/include/components/class-wc-retailcrm-logger.php'); + require_once(dirname(__FILE__ ) . '/include/components/class-wc-retailcrm-history-assembler.php'); + require_once(dirname(__FILE__ ) . '/include/components/class-wc-retailcrm-paginated-request.php'); + require_once(dirname(__FILE__ ) . '/include/components/class-wc-retailcrm-customer-data-replacer.php'); + require_once(dirname(__FILE__ ) . '/include/components/class-wc-retailcrm-customer-data-replacer.php'); + require_once(dirname(__FILE__ ) . '/include/abstracts/class-wc-retailcrm-abstract-builder.php'); require_once(dirname(__FILE__ ) . '/include/abstracts/class-wc-retailcrm-abstracts-settings.php'); require_once(dirname(__FILE__ ) . '/include/abstracts/class-wc-retailcrm-abstracts-data.php'); require_once(dirname(__FILE__ ) . '/include/abstracts/class-wc-retailcrm-abstracts-address.php'); + require_once(dirname(__FILE__ ) . '/include/customer/woocommerce/class-wc-retailcrm-wc-customer-builder.php'); require_once(dirname(__FILE__ ) . '/include/order/class-wc-retailcrm-order.php'); require_once(dirname(__FILE__ ) . '/include/order/class-wc-retailcrm-order-payment.php'); require_once(dirname(__FILE__ ) . '/include/order/class-wc-retailcrm-order-item.php');