opt=wzhf_get_option(); } private function accounts_base(){ $dc=isset($this->opt['dc'])?$this->opt['dc']:'.eu'; return 'https://accounts.zoho'.$dc; } private function api_base(){ $dc=isset($this->opt['dc'])?$this->opt['dc']:'.eu'; return 'https://www.zohoapis'.$dc.'/inventory/v1'; } private function tokens(){ $t=get_option('wzhf_tokens'); return is_array($t)?$t:array(); } private function save_tokens($t){ update_option('wzhf_tokens',$t,false); } public function is_connected(){ $t=$this->tokens(); return !empty($t['access_token']) && !empty($t['refresh_token']); } public function ensure_token(){ $t=$this->tokens(); if(empty($t['access_token']) || time()>(int)(isset($t['expires_at'])?$t['expires_at']:0)){ if(empty($t['refresh_token'])) return false; $res=wp_remote_post($this->accounts_base().'/oauth/v2/token', array('body'=>array( 'grant_type'=>'refresh_token','refresh_token'=>$t['refresh_token'],'client_id'=>isset($this->opt['cid'])?$this->opt['cid']:'','client_secret'=>isset($this->opt['sec'])?$this->opt['sec']:'' ), 'timeout'=>25 )); if(is_wp_error($res)){ WZHF_Logger::log('Refresh token error',array('err'=>$res->get_error_message())); return false; } $data=json_decode(wp_remote_retrieve_body($res),true); if(!empty($data['access_token'])){ $t['access_token']=$data['access_token']; $t['expires_at']=time()+((int)(isset($data['expires_in'])?$data['expires_in']:3600))-60; $this->save_tokens($t); } else { WZHF_Logger::log('Refresh response missing token',array('raw'=>$data)); return false; } } return isset($t['access_token'])?$t['access_token']:false; } public static function oauth_callback(WP_REST_Request $req){ $code=$req->get_param('code'); $opt=wzhf_get_option(); if(!$code) return new WP_REST_Response(array('error'=>'no_code'),400); $res=wp_remote_post('https://accounts.zoho'.(isset($opt['dc'])?$opt['dc']:'.eu').'/oauth/v2/token', array( 'body'=>array( 'grant_type'=>'authorization_code','code'=>$code,'client_id'=>isset($opt['cid'])?$opt['cid']:'','client_secret'=>isset($opt['sec'])?$opt['sec']:'','redirect_uri'=>rest_url(WZHF_NS.'/oauth/callback') ), 'timeout'=>25 )); if(is_wp_error($res)) return new WP_REST_Response(array('error'=>$res->get_error_message()),500); $data=json_decode(wp_remote_retrieve_body($res),true); if(empty($data['access_token'])) return new WP_REST_Response(array('error'=>'token_exchange_failed','raw'=>$data),500); update_option('wzhf_tokens',array( 'access_token'=>$data['access_token'],'refresh_token'=>isset($data['refresh_token'])?$data['refresh_token']:'','expires_at'=>time()+((int)(isset($data['expires_in'])?$data['expires_in']:3600))-60 ), false); wp_safe_redirect(admin_url('admin.php?page='.WZHF_SLUG.'&connected=1')); exit; } private function headers(){ $t=$this->ensure_token(); if(!$t) return array('Authorization'=>''); return array('Authorization'=>'Zoho-oauthtoken '.$t, 'Content-Type'=>'application/json;charset=UTF-8'); } private function request($method,$path,$args=null,$query=array()){ $url=$this->api_base().$path; if(!isset($query['organization_id'])) $query['organization_id']=isset($this->opt['org'])?$this->opt['org']:''; $url .= (strpos($url,'?')===false?'?':'&').http_build_query($query); $res=wp_remote_request($url, array('method'=>$method,'headers'=>$this->headers(),'body'=>$args?wp_json_encode($args):null,'timeout'=>30)); if(is_wp_error($res)){ WZHF_Logger::log('Zoho HTTP error',array('err'=>$res->get_error_message(),'url'=>$url)); return array(null,$res->get_error_message()); } $code=wp_remote_retrieve_response_code($res); $body=json_decode(wp_remote_retrieve_body($res),true); if($code>=400){ WZHF_Logger::log('Zoho API error',array('code'=>$code,'body'=>$body,'path'=>$path)); return array(null,$body); } return array($body,null); } private function clamp($s,$max){ $s = is_string($s) ? $s : trim((string)$s); $s = preg_replace('/\s+/u',' ', trim($s)); if(function_exists('mb_strlen') && function_exists('mb_substr')){ if(mb_strlen($s,'UTF-8')>$max){ return mb_substr($s,0,$max,'UTF-8'); } } else { if(strlen($s)>$max){ return substr($s,0,$max); } } return $s; } private function pick($a,$b){ $a = trim((string)$a); $b = trim((string)$b); return $a!=='' ? $a : $b; } private function get_contact($contact_id){ list($res,$err) = $this->request('GET','/contacts/'.rawurlencode($contact_id),null,array()); if($res && !empty($res['contact'])) return $res['contact']; return null; } private function update_contact_addresses($contact_id,$order,$mode){ // Build billing $b_addr1 = $this->pick($order->get_billing_address_1(), $order->get_shipping_address_1()); $b_addr2 = $this->pick($order->get_billing_address_2(), $order->get_shipping_address_2()); $b_city = $this->pick($order->get_billing_city(), $order->get_shipping_city()); $b_state = $this->pick($order->get_billing_state(), $order->get_shipping_state()); $b_zip = $this->pick($order->get_billing_postcode(), $order->get_shipping_postcode()); $b_ctry = $this->pick($order->get_billing_country(), $order->get_shipping_country()); $b_line = trim($b_addr1.' '.($b_addr2?:'')); // Build shipping $s_addr1 = $this->pick($order->get_shipping_address_1(), $order->get_billing_address_1()); $s_addr2 = $this->pick($order->get_shipping_address_2(), $order->get_billing_address_2()); $s_city = $this->pick($order->get_shipping_city(), $order->get_billing_city()); $s_state = $this->pick($order->get_shipping_state(), $order->get_billing_state()); $s_zip = $this->pick($order->get_shipping_postcode(), $order->get_billing_postcode()); $s_ctry = $this->pick($order->get_shipping_country(), $order->get_billing_country()); $s_line = trim($s_addr1.' '.($s_addr2?:'')); $new_ba = array( 'address' => $this->clamp($b_line, 90), 'city' => $this->clamp($b_city, 40), 'state' => $this->clamp($b_state, 30), 'zip' => $this->clamp($b_zip, 20), 'country' => $this->clamp($b_ctry, 30) ); $new_sa = array( 'address' => $this->clamp($s_line, 90), 'city' => $this->clamp($s_city, 40), 'state' => $this->clamp($s_state, 30), 'zip' => $this->clamp($s_zip, 20), 'country' => $this->clamp($s_ctry, 30) ); $full = $this->get_contact($contact_id); $cur_ba = $full && isset($full['billing_address']) ? $full['billing_address'] : array(); $cur_sa = $full && isset($full['shipping_address']) ? $full['shipping_address'] : array(); $is_empty_ba = empty(trim(isset($cur_ba['address'])?$cur_ba['address']:'')); $is_empty_sa = empty(trim(isset($cur_sa['address'])?$cur_sa['address']:'')); $opt = wzhf_get_option(); $mode = in_array($mode, array('always','if_empty','never')) ? $mode : (isset($opt['contact_addr_sync'])?$opt['contact_addr_sync']:'always'); $should = false; if($mode==='always'){ $should=true; } else if($mode==='if_empty'){ $should = ($is_empty_ba || $is_empty_sa); } else { $should=false; } if(!$should){ WZHF_Logger::log('contact_update_addresses_skipped', array('contact_id'=>$contact_id,'mode'=>$mode)); return true; } $payload = array('billing_address'=>$new_ba,'shipping_address'=>$new_sa); WZHF_Logger::log('contact_update_addresses_applied', array('contact_id'=>$contact_id,'mode'=>$mode)); list($res,$err) = $this->request('PUT','/contacts/'.rawurlencode($contact_id), $payload); return $res && !empty($res['contact']); } private function create_contact_from_order($order){ // name/company $name = $order->get_billing_company(); if(!$name){ $name = trim($order->get_billing_first_name().' '.$order->get_billing_last_name()); if(!$name) $name = 'WC Customer '.$order->get_billing_email(); } $email = $order->get_billing_email(); // build billing & shipping $b_addr1 = $this->pick($order->get_billing_address_1(), $order->get_shipping_address_1()); $b_addr2 = $this->pick($order->get_billing_address_2(), $order->get_shipping_address_2()); $b_city = $this->pick($order->get_billing_city(), $order->get_shipping_city()); $b_state = $this->pick($order->get_billing_state(), $order->get_shipping_state()); $b_zip = $this->pick($order->get_billing_postcode(), $order->get_shipping_postcode()); $b_ctry = $this->pick($order->get_billing_country(), $order->get_shipping_country()); $b_line = trim($b_addr1.' '.($b_addr2?:'')); $s_addr1 = $this->pick($order->get_shipping_address_1(), $order->get_billing_address_1()); $s_addr2 = $this->pick($order->get_shipping_address_2(), $order->get_billing_address_2()); $s_city = $this->pick($order->get_shipping_city(), $order->get_billing_city()); $s_state = $this->pick($order->get_shipping_state(), $order->get_billing_state()); $s_zip = $this->pick($order->get_shipping_postcode(), $order->get_billing_postcode()); $s_ctry = $this->pick($order->get_shipping_country(), $order->get_billing_country()); $s_line = trim($s_addr1.' '.($s_addr2?:'')); $contact = array( 'contact_name' => $this->clamp($name, 100), 'contact_type' => 'customer', 'company_name' => $this->clamp($order->get_billing_company() ? $order->get_billing_company() : $name, 100), 'email' => $email, 'billing_address' => array( 'address' => $this->clamp($b_line, 90), 'city' => $this->clamp($b_city, 40), 'state' => $this->clamp($b_state, 30), 'zip' => $this->clamp($b_zip, 20), 'country' => $this->clamp($b_ctry, 30), 'phone' => $this->clamp($order->get_billing_phone(), 30), ), 'shipping_address' => array( 'address' => $this->clamp($s_line, 90), 'city' => $this->clamp($s_city, 40), 'state' => $this->clamp($s_state, 30), 'zip' => $this->clamp($s_zip, 20), 'country' => $this->clamp($s_ctry, 30), ), 'contact_persons' => array() ); if($email){ $contact['contact_persons'][] = array( 'first_name' => $this->clamp($order->get_billing_first_name(), 50), 'last_name' => $this->clamp($order->get_billing_last_name(), 50), 'email' => $email, 'is_primary_contact' => true ); } list($res,$err)=$this->request('POST','/contacts',$contact); if($res && !empty($res['contact'])) return array($res['contact'], null); WZHF_Logger::log('contact_create_failed', array('order_id'=>$order->get_id(),'err'=>$err)); return array(null, $err ? $err : 'contact_create_failed'); } private function find_item_by_sku($sku){ if(!$sku) return null; $key='wzhf_item_'.md5($sku); $c=get_transient($key); if($c) return $c; list($res,$err)=$this->request('GET','/items',null,array('search_text'=>$sku)); if($res && !empty($res['items'])){ foreach($res['items'] as $it){ $skuVal = isset($it['sku']) ? $it['sku'] : ''; if($skuVal===$sku){ set_transient($key,$it,3600); return $it; } } } return null; } private function get_normilize_product_title($product) { $title = ''; if (method_exists($product, 'get_name')) { $title = (string) $product->get_name(); } if (!$title) { $title = (string) get_the_title($product->id); } if (!$title) { return ''; } $packageCount = get_field("number_packages", $product->id); if ($packageCount > 1) { $title .= ' ' . $packageCount . '-Pack Bundle'; } return $title; } private function create_item_from_wc($product){ $rate = $product->get_regular_price(); if($rate==='' || $rate===null) $rate = $product->get_price(); $payload = array( 'name' => $this->get_normilize_product_title($product), 'rate' => (float)$rate, 'sku' => $product->get_sku(), 'item_type' => 'goods' ); list($res,$err)=$this->request('POST','/items',$payload); return $res && isset($res['item']) ? $res['item'] : null; } public function create_sales_order_from_wc($order,$settings=array()){ // line items $lines=array(); $create_missing=!empty($settings['create_missing']); foreach($order->get_items() as $item){ $product=$item->get_product(); if(!$product) continue; $sku=$product->get_sku(); $zi=$sku?$this->find_item_by_sku($sku):null; if(!$zi && $create_missing){ $zi=$this->create_item_from_wc($product); } $line=array('name'=>$this->get_normilize_product_title($product),'rate'=>(float)$order->get_item_subtotal($item,false),'quantity'=>(float)$item->get_quantity(), 'tax_id' => '5868409000003263039'); if($zi && !empty($zi['item_id'])) $line['item_id']=$zi['item_id']; if($sku) $line['sku']=$sku; $lines[]=$line; } // Diagnostics: compute address lengths (we don't send them in SO) $ship_addr1 = $this->pick($order->get_shipping_address_1(), $order->get_billing_address_1()); $ship_addr2 = $this->pick($order->get_shipping_address_2(), $order->get_billing_address_2()); $ship_city = $this->pick($order->get_shipping_city(), $order->get_billing_city()); $ship_state = $this->pick($order->get_shipping_state(), $order->get_billing_state()); $ship_zip = $this->pick($order->get_shipping_postcode(), $order->get_billing_postcode()); $ship_ctry = $this->pick($order->get_shipping_country(), $order->get_billing_country()); $addr_line = trim($ship_addr1.' '.($ship_addr2?:'')); $addr = array( 'address'=>$this->clamp($addr_line, 90), 'city'=>$this->clamp($ship_city, 40), 'state'=>$this->clamp($ship_state, 30), 'zip'=>$this->clamp($ship_zip, 20), 'country'=>$this->clamp($ship_ctry, 30) ); $len = array( 'address'=>(function_exists('mb_strlen')?mb_strlen($addr['address'],'UTF-8'):strlen($addr['address'])), 'city'=>(function_exists('mb_strlen')?mb_strlen($addr['city'],'UTF-8'):strlen($addr['city'])), 'state'=>(function_exists('mb_strlen')?mb_strlen($addr['state'],'UTF-8'):strlen($addr['state'])), 'zip'=>(function_exists('mb_strlen')?mb_strlen($addr['zip'],'UTF-8'):strlen($addr['zip'])), 'country'=>(function_exists('mb_strlen')?mb_strlen($addr['country'],'UTF-8'):strlen($addr['country'])) ); WZHF_Logger::log('SO create:addr-lengths', $len); WZHF_Logger::log('SO create:payload-no-address'); // Ensure customer (and sync contact addresses as per setting) $email = $order->get_billing_email(); $customer_id = null; if($email){ list($find1,$e1)=$this->request('GET','/contacts',null,array('email'=>$email)); if($find1 && !empty($find1['contacts'])) $customer_id=$find1['contacts'][0]['contact_id']; if(!$customer_id){ list($find2,$e2)=$this->request('GET','/contacts',null,array('search_text'=>$email)); if($find2 && !empty($find2['contacts'])) $customer_id=$find2['contacts'][0]['contact_id']; } } if(!$customer_id){ list($cr,$cerr) = $this->create_contact_from_order($order); if($cr && !empty($cr['contact_id'])) $customer_id = $cr['contact_id']; } else { $mode = isset($this->opt['contact_addr_sync']) ? $this->opt['contact_addr_sync'] : 'always'; $this->update_contact_addresses($customer_id, $order, $mode); } if(!$customer_id){ WZHF_Logger::log('No customer_id resolved for SO',array('order_id'=>$order->get_id())); return array(null, 'no_customer'); } $body=array( 'customer_id'=>$customer_id, 'reference_number'=>(string)$order->get_id(), 'date'=>current_time('Y-m-d'), 'line_items'=>$lines, 'notes'=>'WC Order #'.$order->get_order_number() ); error_log('=====LOG START====='); error_log(json_encode($body)); error_log('=====LOG END====='); list($res,$err)=$this->request('POST','/salesorders',$body); if(!$res || empty($res['salesorder'])) return array(null, ($err ? $err : 'unknown')); return array($res['salesorder'],null); } }