294 lines
16 KiB
PHP
294 lines
16 KiB
PHP
<?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);
|
|
}
|
|
}
|