This commit is contained in:
14 changed files with 987 additions and 0 deletions

293
inc/class-zoho.php Normal file
View File

@@ -0,0 +1,293 @@
<?php
if (!defined('ABSPATH')) exit;
class WZHF_Zoho {
private $opt;
public function __construct(){ $this->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);
}
}