<?php

defined('BASEPATH') or exit('No direct script access allowed');
/**
 * This class describes a sage accounting integration model.
 */
class Sage_accounting_integration_model extends App_Model
{
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * update general setting
     *
     * @param      array   $data   The data
     *
     * @return     boolean
     */
    public function update_setting($data)
    {
        $affectedRows = 0;
        if (!isset($data['settings']['acc_integration_sage_accounting_active'])) {
            $data['settings']['acc_integration_sage_accounting_active'] = 0;
        }

        if (!isset($data['settings']['acc_integration_sage_accounting_sync_from_system'])) {
            $data['settings']['acc_integration_sage_accounting_sync_from_system'] = 0;
        }

        if (!isset($data['settings']['acc_integration_sage_accounting_sync_to_system'])) {
            $data['settings']['acc_integration_sage_accounting_sync_to_system'] = 0;
        }


        if (isset($data['settings']['acc_integration_sage_accounting_client_id'])) {
            $data['settings']['acc_integration_sage_accounting_client_id'] = $this->encryption->encrypt($data['settings']['acc_integration_sage_accounting_client_id']);
        }

        if (isset($data['settings']['acc_integration_sage_accounting_client_secret'])) {
            $data['settings']['acc_integration_sage_accounting_client_secret'] = $this->encryption->encrypt($data['settings']['acc_integration_sage_accounting_client_secret']);
        }

        foreach ($data['settings'] as $key => $value) {
            if (update_option($key, $value)) {
                $affectedRows++;
            }
        }

        if ($affectedRows > 0) {
            return true;
        }
        return false;
    }

    public function init_sage_accounting_config(){
        $configs = get_acc_sage_accounting_config();

        // Prep Data Services
        $params = [
            'refresh_token' => $configs['refresh_token'],
            'client_id' => $configs['client_id'], 
            'client_secret' => $configs['client_secret'], 
            'grant_type' => "refresh_token"
        ];

        $http_header = array(
                 'Accept' => 'application/json',
                 'Content-Type' => 'application/x-www-form-urlencoded'
            );

        if($configs['access_token_expires'] <= (time() + 30)){
            $result = $this->executeRequest($configs['access_token_url'], $params, $http_header,'POST');

            if(isset($result['access_token'])){
                update_option('acc_integration_sage_accounting_access_token', $result['access_token']);
                update_option('acc_integration_sage_accounting_access_token_expires', time() + $result['expires_in']);
                update_option('acc_integration_sage_accounting_refresh_token', $result['refresh_token']);
                update_option('acc_integration_sage_accounting_refresh_token_expires', time() + $result['refresh_token_expires_in']);
                update_option('acc_integration_sage_accounting_requested_by_id', $result['requested_by_id']);
            }
        }
    }


    public function create_sage_accounting_customer($customer_id = ''){
        $this->db->select('*, '.db_prefix() . 'clients.userid as userid');
        if($customer_id != ''){
            $this->db->where(db_prefix().'clients.userid', $customer_id);
        }
        $this->db->join(db_prefix() . 'contacts', db_prefix() . 'contacts.userid=' . db_prefix() . 'clients.userid AND '.db_prefix() . 'contacts.is_primary = "1"', 'left');
        $this->db->join(db_prefix() . 'acc_integration_logs', db_prefix() . 'acc_integration_logs.rel_id=' . db_prefix() . 'clients.userid AND '.db_prefix() . 'acc_integration_logs.rel_type = "customer" AND software = "sage_accounting"', 'left');

        $clients = $this->db->get(db_prefix().'clients')->result_array();

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $customer_data = [];
        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/contacts?attributes=all&contact_type_id=CUSTOMER&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                foreach ($entities['$items'] as $customer) {
                    $customer_data[$customer['id']] = $customer;
                }
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        foreach ($clients as $client) {
            $customerObj = [
                "contact" => [
                 "name"=>  $client['company'],
                 "contact_type_ids" =>  ["CUSTOMER"],
                 "company_name"=>  $client['company'],
                 "website"=>  $client['website'],

                "main_address" => [
                     "name" => $client['company'].': Main Address',
                     "address_line_1"=>  $client['billing_street'],
                     "address_line_2"=>  '',
                     "city"=>  $client['billing_city'],
                     "region"=> $client['billing_country'],
                     "postal_code"=>  $client['billing_zip']
                 ],

                 "delivery_address" => [
                     "name" => $client['company'].': Delivery Address',
                     "address_line_1" =>  $client['shipping_street'],
                     "address_line_2" =>  '',
                     "city"=>  $client['shipping_city'],
                     "region"=> $client['shipping_country'],
                     "postal_code"=>  $client['shipping_zip']
                 ],

                 "main_contact_person" => [
                    "name" => $client['firstname'].' '.$client['lastname'],
                    "email" => $client['email'],
                    "telephone" => $client['phonenumber'],
                    "is_main_contact" => true
                 ]
             ]
         ];

            if($client['connect_id'] != '' && isset($customer_data[$client['connect_id']])){
                $url = $api_domain.'/contacts/'.$client['connect_id'];
                $result = $this->callAPI($url, $customerObj, $header, 'PUT');
            }else{
                if($client['connect_id'] != ''){
                    $this->delete_integration_log($client['userid'], 'customer', 'sage_accounting');
                }

                $url = $api_domain.'/contacts';
                $result = $this->callAPI($url, $customerObj, $header, 'POST');
            }

            $this->delete_integration_error_log($client['userid'], 'customer', 'sage_accounting');
            if (!isset($result['id'])) {

                $message = '';

                foreach ($result as $key => $value) {
                    $message .= '<br>'.$value['$message'];
                }

                $this->db->insert(db_prefix().'acc_integration_error_logs', [
                    'rel_id' => $client['userid'],
                    'rel_type' => 'customer',
                    'software' => 'sage_accounting',
                    'error_detail' => $message,
                    'date_updated' => date('Y-m-d H:i:s'),
                ]);

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $client['userid'],
                    'rel_type' => 'customer',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 0,
                    'connect_id' => '',
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($customer_id != ''){
                    return $message;
                }
            }else{
                if(!isset($customer_data[$client['connect_id']])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $client['userid'],
                        'rel_type' => 'customer',
                        'software' => 'sage_accounting',
                        'connect_id' => $result['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $client['userid'],
                    'rel_type' => 'customer',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 1,
                    'connect_id' => $result['id'],
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($customer_id != ''){
                    return true;
                }
            }
        }
    }

    public function create_sage_accounting_invoice($invoice_id = ''){
        $this->db->select('*, ' . db_prefix() . 'currencies.id as currencyid, ' . db_prefix() . 'invoices.id as id, ' . db_prefix() . 'currencies.name as currency_name');
        if($invoice_id != ''){
            $this->db->where(db_prefix().'invoices.id', $invoice_id);
        }
        $this->db->join(db_prefix() . 'currencies', '' . db_prefix() . 'currencies.id = ' . db_prefix() . 'invoices.currency', 'left');
        $this->db->join(db_prefix() . 'acc_integration_logs', db_prefix() . 'acc_integration_logs.rel_id=' . db_prefix() . 'invoices.id AND '.db_prefix() . 'acc_integration_logs.rel_type = "invoice" AND software = "sage_accounting"', 'left');

        $invoices = $this->db->get(db_prefix().'invoices')->result_array();

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $income_account_id = $this->get_sage_accounting_income_account();

        $invoice_data = [];
        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/sales_invoices?attributes=all&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                foreach ($entities['$items'] as $invoice) {
                    $invoice_data[$invoice['id']] = $invoice;
                }
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        foreach ($invoices as $invoice) {
            $customer_connect_id = $this->get_connect_id($invoice['clientid'], 'customer', 'sage_accounting');

            $items  = get_items_by_type('invoice', $invoice['id']);
            $item_array = [];

            $tax_amount = 0;
            foreach ($items as $item) {
                $data_tax = $this->get_invoice_item_tax($item, $invoice);

                foreach ($data_tax['tax_amount'] as $k => $amount) {
                    $tax_amount += $amount;
                }

                $tax_connect_id = '';

                $item_array[] = [
                    "item_id" => '',
                    "quantity" => $item['qty'],
                    "unit_price" => $item['rate'],
                    'description' => $item['description'],
                    'total_amount' => $item['rate'] * $item['qty'],
                    'tax_rate_id' => $tax_connect_id,
                    'ledger_account_id' => $income_account_id
                ];
            }

            if($tax_amount > 0){
                $item_array[] = [
                    "item_id" => '',
                    "quantity" => 1,
                    "unit_price" => $tax_amount,
                    'description' => 'Total Tax',
                    'total_amount' => $tax_amount,
                    'tax_rate_id' => $tax_connect_id,
                    'ledger_account_id' => $income_account_id
                ];
            }
            
            if($invoice['discount_total'] > 0){
                $item_array[] = [
                    "item_id" => '',
                    "quantity" => 1,
                    "unit_price" => 0,
                    "discount_amount" => $invoice['discount_total'],
                    'description' => 'Discount',
                    'ledger_account_id' => $income_account_id
                ];
            }

            if($invoice['adjustment'] != 0){
                $item_array[] = [
                    "item_id" => '',
                    "quantity" => 1,
                    "unit_price" => $invoice['adjustment'],
                    'description' => 'Adjustment',
                    'total_amount' => $invoice['adjustment'],
                    'ledger_account_id' => $income_account_id
                ];
            }

            $invoiceObj = [
                "sales_invoice" => [
                    "contact_id"=> $customer_connect_id,
                    "date" => $invoice['date'],
                    "due_date" => $invoice['duedate'],
                    "notes" => $invoice['clientnote'],
                    "reference" => format_invoice_number($invoice['id']),
                    "invoice_lines" => $item_array,
                    "total_amount" => $invoice['total'],
                    "net_amount" => $invoice['subtotal'],
                    "tax_amount" => $invoice['total_tax'],
                ]
            ];

            if($invoice['connect_id'] != '' && isset($invoice_data[$invoice['connect_id']])){
                $url = $api_domain.'/sales_invoices/'.$invoice['connect_id'];
                $result = $this->callAPI($url, $invoiceObj, $header, 'PUT');
            }else{
                if($invoice['connect_id'] != ''){
                    $this->delete_integration_log($invoice['id'], 'invoice', 'sage_accounting');
                }

                $url = $api_domain.'/sales_invoices';
                $result = $this->callAPI($url, $invoiceObj, $header, 'POST');
            }

            $this->delete_integration_error_log($invoice['id'], 'invoice', 'sage_accounting');
            if (!isset($result['id'])) {

                $message = '';

                foreach ($result as $key => $value) {
                    $message .= '<br>'.$value['$message'];
                }

                $this->db->insert(db_prefix().'acc_integration_error_logs', [
                    'rel_id' => $invoice['id'],
                    'rel_type' => 'invoice',
                    'software' => 'sage_accounting',
                    'error_detail' => $message,
                    'date_updated' => date('Y-m-d H:i:s'),
                ]);

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $invoice['id'],
                    'rel_type' => 'invoice',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 0,
                    'connect_id' => '',
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($invoice_id != ''){
                    return $message;
                }
            }else{
                if(!isset($invoice_data[$invoice['connect_id']])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $invoice['id'],
                        'rel_type' => 'invoice',
                        'software' => 'sage_accounting',
                        'connect_id' => $result['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $invoice['id'],
                    'rel_type' => 'invoice',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 1,
                    'connect_id' => $result['id'],
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($invoice_id != ''){
                    return true;
                }
            }
        }
    }

    public function create_sage_accounting_payment($payment_id = ''){
        $this->db->select('*, ' . db_prefix() . 'invoicepaymentrecords.id as id');
        if($payment_id != ''){
            $this->db->where(db_prefix().'invoicepaymentrecords.id', $payment_id);
        }
        $this->db->join(db_prefix() . 'acc_integration_logs', db_prefix() . 'acc_integration_logs.rel_id=' . db_prefix() . 'invoicepaymentrecords.id AND '.db_prefix() . 'acc_integration_logs.rel_type = "payment" AND software = "sage_accounting"', 'left');

        $payments = $this->db->get(db_prefix().'invoicepaymentrecords')->result_array();

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();
        $bank_account_id = $this->get_sage_accounting_bank_account();

        $payment_data = [];
        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/contact_payments?attributes=all&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                foreach ($entities['$items'] as $payment) {
                    $payment_data[$payment['id']] = $payment;
                }
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        foreach ($payments as $payment) {
            $this->db->where('id', $payment['invoiceid']);
            $invoice = $this->db->get(db_prefix().'invoices')->row();

            $invoice_connect_id = $this->get_connect_id($payment['invoiceid'], 'invoice', 'sage_accounting');
            $customer_connect_id = $this->get_connect_id($invoice->clientid, 'customer', 'sage_accounting');

            $paymentObj = [
                "contact_payment" => [
                    "contact_id" => $customer_connect_id,
                    "date" => $payment['date'],
                    "total_amount" => $payment['amount'],
                    "payment_method_id" => "CASH",
                    "bank_account_id" => $bank_account_id,
                    "transaction_type_id" => 'CUSTOMER_RECEIPT',
                    "allocated_artefacts" => [
                        [
                            "artefact_id" => $invoice_connect_id,
                            "amount" => $payment['amount']
                        ]
                    ]
                ]
            ];

            if($payment['connect_id'] != '' && isset($payment_data[$payment['connect_id']])){
                $url = $api_domain.'/contact_payments/'.$payment['connect_id'];
                $result = $this->callAPI($url, $paymentObj, $header, 'PUT');
            }else{
                if($payment['connect_id'] != ''){
                    $this->delete_integration_log($payment['id'], 'payment', 'sage_accounting');
                }

                $url = $api_domain.'/contact_payments';
                $result = $this->callAPI($url, $paymentObj, $header, 'POST');
            }

            $this->delete_integration_error_log($payment['id'], 'payment', 'sage_accounting');
            if (!isset($result['id'])) {

                $message = '';

                foreach ($result as $key => $value) {
                    $message .= '<br>'.$value['$message'];
                }

                $this->db->insert(db_prefix().'acc_integration_error_logs', [
                    'rel_id' => $payment['id'],
                    'rel_type' => 'payment',
                    'software' => 'sage_accounting',
                    'error_detail' => $message,
                    'date_updated' => date('Y-m-d H:i:s'),
                ]);  

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $payment['id'],
                    'rel_type' => 'payment',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 0,
                    'connect_id' => '',
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($payment_id != ''){
                    return $message;
                }
            }else{
                if(!isset($payment_data[$payment['connect_id']])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $payment['id'],
                        'rel_type' => 'payment',
                        'software' => 'sage_accounting',
                        'connect_id' => $result['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $payment['id'],
                    'rel_type' => 'payment',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 1,
                    'connect_id' => $result['id'],
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($payment_id != ''){
                    return true;
                }
            }
        }
    }

    public function create_sage_accounting_expense($expense_id = ''){
        $this->db->select('*, ' . db_prefix() . 'expenses.id as id');
        if($expense_id != ''){
            $this->db->where(db_prefix().'expenses.id', $expense_id);
        }
        $this->db->join(db_prefix() . 'acc_integration_logs', db_prefix() . 'acc_integration_logs.rel_id=' . db_prefix() . 'expenses.id AND '.db_prefix() . 'acc_integration_logs.rel_type = "expense" AND software = "sage_accounting"', 'left');

        $expenses = $this->db->get(db_prefix().'expenses')->result_array();

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $expense_account_id = $this->get_sage_accounting_expense_account();
        $vendor_connect_id = $this->get_sage_accounting_vendor_default();

        $expense_data = [];
        $continue = true;
        $page = 1;

        while ($continue) {
            $continue = false;
            $url = $api_domain.'/purchase_quick_entries?attributes=all&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                foreach ($entities['$items'] as $expense) {
                    $expense_data[$expense['id']] = $expense;
                }
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }
        
        foreach ($expenses as $expense) {
            $total_tax = 0;

            if($expense['tax'] > 0){
                $this->db->where('id', $expense['tax']);
                $tax = $this->db->get(db_prefix().'taxes')->row();
                if($tax){
                    $total_tax += ($tax->taxrate/100) * $expense['amount'];
                }
            }

            if($expense['tax2'] > 0){
                $this->db->where('id', $expense['tax2']);
                $tax = $this->db->get(db_prefix().'taxes')->row();
                if($tax){
                    $total_tax += ($tax->taxrate/100) * $expense['amount'];
                }
            }

            $customer_connect_id = '';

            if($expense['clientid'] != 0){
                $customer_connect_id = $this->get_connect_id($expense['clientid'], 'customer', 'sage_accounting');
            }

            $tax_connect_id = '';

            $expenseObj = [
                "purchase_quick_entry" => [
                    "quick_entry_type_id" => 'INVOICE',
                    "contact_id" => $vendor_connect_id,
                    "ledger_account_id" => $expense_account_id,
                    "date" => $expense['date'],
                    "net_amount" => $expense['amount'] + $total_tax,
                    "total_amount" => $expense['amount'] + $total_tax,
                    "reference" => "Expenses#".$expense['id']
                ]
            ];

            if($expense['connect_id'] != '' && isset($expense_data[$expense['connect_id']])){
                $url = $api_domain.'/purchase_quick_entries/'.$expense['connect_id'];
                $result = $this->callAPI($url, $expenseObj, $header, 'PUT');
            }else{
                if($expense['connect_id'] != ''){
                    $this->delete_integration_log($expense['id'], 'expense', 'sage_accounting');
                }

                $url = $api_domain.'/purchase_quick_entries';
                $result = $this->callAPI($url, $expenseObj, $header, 'POST');
            }

            $this->delete_integration_error_log($expense['id'], 'expense', 'sage_accounting');
            if (!isset($result['id'])) {

                $message = '';

                foreach ($result as $key => $value) {
                    $message .= '<br>'.$value['$message'];
                }

                $this->db->insert(db_prefix().'acc_integration_error_logs', [
                    'rel_id' => $expense['id'],
                    'rel_type' => 'expense',
                    'software' => 'sage_accounting',
                    'error_detail' => $message,
                    'date_updated' => date('Y-m-d H:i:s'),
                ]);

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $expense['id'],
                    'rel_type' => 'expense',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 0,
                    'connect_id' => '',
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($expense_id != ''){
                    return $message;
                }
            }else{
                if(!isset($expense_data[$expense['connect_id']])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $expense['id'],
                        'rel_type' => 'expense',
                        'software' => 'sage_accounting',
                        'connect_id' => $result['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }

                $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                    'rel_id' => $expense['id'],
                    'rel_type' => 'expense',
                    'software' => 'sage_accounting',
                    'type' => 'sync_up',
                    'status' => 1,
                    'connect_id' => $result['id'],
                    'datecreated' => date('Y-m-d H:i:s'),
                ]);

                if($expense_id != ''){
                    return true;
                } 
            }
        }
    }

    public function get_sage_accounting_bank_account(){
        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $url = $api_domain.'/bank_accounts?items_per_page=200';

        $result = $this->callAPI($url, [], $header, 'GET');

        if (!isset($result['$items'])) {
            return 0;   
        }else{
            foreach ($result['$items'] as $key => $value) {
                if($value['displayed_as'] == 'Cash (1210)'){
                    return $value['id'];
                }
            }

            return 0;
        }
    }

    public function get_sage_accounting_income_account(){
        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $url = $api_domain.'/ledger_accounts?visible_in=sales&items_per_page=200';

        $result = $this->callAPI($url, [], $header, 'GET');

        if (!isset($result['$items'])) {
            return 0;   
        }else{
            foreach ($result['$items'] as $key => $value) {
                if($value['displayed_as'] == 'Other income (4900)'){
                    return $value['id'];
                }
            }

            return 0;
        }
    }

    public function get_sage_accounting_expense_account(){
        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $url = $api_domain.'/ledger_accounts?visible_in=expenses&items_per_page=200';
        $result = $this->callAPI($url, [], $header, 'GET');

        if (!isset($result['$items'])) {
            return 0;   
        }else{
            foreach ($result['$items'] as $key => $value) {
                if($value['displayed_as'] == 'Cost of Sales - Goods (5000)'){
                    return $value['id'];
                }
            }

            return 0;
        }
    }

    public function get_sage_accounting_vendor_default(){
        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $url = $api_domain.'/contacts?search=FromSync';
        $result = $this->callAPI($url, [], $header, 'GET');
        if (!isset($result['$items']) || (isset($result['$items']) && count($result['$items']) == 0)) {
            $customerObj = [
                "contact" => [
                     "name"=>  "Expenses Vendor",
                     "contact_type_ids" =>  ["VENDOR"],
                     "company_name"=>  "Expenses Vendor",
                     "reference" => "FromSync"
                ]
            ];

            $url = $api_domain.'/contacts';

            $result = $this->callAPI($url, $customerObj, $header, 'POST');

            if (!isset($result['id'])) {
                return '';
            }else{
                return $result['id'];
            }
        }else{
            foreach ($result['$items'] as $key => $value) {
                return $value['id'];
            }

            return '';
        }
    }

    public function get_sage_accounting_customer(){
        $customer_list = $this->clients_model->get();
     
        $customer_arr = [];
        foreach ($customer_list as $customer) {
            $customer_arr[$customer['userid']] = $customer;
        }

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/contacts?attributes=all&contact_type_id=CUSTOMER&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                $list_results = array_merge($list_results, $entities['$items']);
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        foreach ($list_results as $customer) {

            $check_connect_id = $this->check_connect_id($customer['id'], 'customer', 'sage_accounting');
            

            $url = $api_domain.'/addresses/c10effa3125c4d69ab169044884d917c';
            $customer_detail = $this->callAPI($url, [], $header, 'GET');

            $customer_data = [];
            $customer_data['company'] = $customer['name'];
            $customer_data['phonenumber'] = '';

            $customer_data['balance'] = '';
            $customer_data['balance_as_of'] = '';

            $url = $api_domain.$customer['main_address']['$path'];
            $address_data = $this->callAPI($url, [], $header, 'GET');

            $customer_data['billing_street'] = $address_data['address_line_1'];
            $customer_data['billing_city'] = $address_data['city'];
            $customer_data['billing_state'] = '';
            $customer_data['billing_zip'] = $address_data['postal_code'];
            $customer_data['billing_country'] = $address_data['region'];

            $shipping_street = '';
            $shipping_city = '';
            $shipping_zip = '';

            $customer_data['shipping_street'] = $shipping_street;
            $customer_data['shipping_city'] = $shipping_city;
            $customer_data['shipping_state'] = '';
            $customer_data['shipping_zip'] = $shipping_zip;
            $customer_data['shipping_country'] = '';

            $customer_data['default_currency'] = '';
            if($check_connect_id != 0 && isset($customer_arr[$check_connect_id])){
                $this->clients_model->update($customer_data, $check_connect_id);
                $client_id = $check_connect_id;
            }else{
                if($check_connect_id != 0){
                    $this->delete_integration_log($check_connect_id, 'customer', 'sage_accounting');
                }

                $client_id = $this->clients_model->add($customer_data);
            }

            $sync_status = 0;
            if($client_id){
            $sync_status = 1;
                if(!isset($customer_arr[$check_connect_id])){
                    if(count($customer['main_contact_person']) > 0){

                        $url = $api_domain.$customer['main_contact_person']['$path'];
                        $contract_detail = $this->callAPI($url, [], $header, 'GET');
                        if(isset($contract_detail['id'])){
                            $contact_data = [];
                            $contact_data['firstname'] = $contract_detail['name'];
                            $contact_data['lastname'] = $contract_detail['name'];
                            $contact_data['phonenumber'] = $contract_detail['telephone'];
                            $contact_data['email'] = $contract_detail['email'];
                            $contact_data['title'] = '';
                            $contact_data['direction'] = '';
                            $contact_data['fakeusernameremembered'] = '';
                            $contact_data['fakepasswordremembered'] = '';
                            $contact_data['password'] = '123456@';
                            $contact_data['is_primary'] = 'on';
                            $contact_data['donotsendwelcomeemail'] = 'on';
                            $contact_data['permissions'] = ['1','2','3','4','5','6'];
                            $contact_data['invoice_emails'] = 'invoice_emails';
                            $contact_data['estimate_emails'] = 'estimate_emails';
                            $contact_data['credit_note_emails'] = 'credit_note_emails';
                            $contact_data['project_emails'] = 'project_emails';
                            $contact_data['ticket_emails'] = 'ticket_emails';
                            $contact_data['task_emails'] = 'task_emails';
                            $contact_data['contract_emails'] = 'contract_emails';
                            $this->clients_model->add_contact($contact_data, $client_id);
                        }
                    }

                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $client_id,
                        'rel_type' => 'customer',
                        'software' => 'sage_accounting',
                        'connect_id' => $customer['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }
            }

            $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                        'rel_id' => $client_id,
                        'rel_type' => 'customer',
                        'software' => 'sage_accounting',
                        'type' => 'sync_down',
                        'status' => $sync_status,
                        'connect_id' => $customer['id'],
                        'datecreated' => date('Y-m-d H:i:s'),
                    ]);
        }
    }


    public function get_sage_accounting_invoice(){
        $this->load->model('invoices_model');
        $invoice_list = $this->invoices_model->get();
     
        $invoice_arr = [];
        foreach ($invoice_list as $invoice) {
            $invoice_arr[$invoice['id']] = $invoice;
        }

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/sales_invoices?attributes=all&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                $list_results = array_merge($list_results, $entities['$items']);
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        $this->load->model('payment_modes_model');
        $payment_modes = $this->payment_modes_model->get();

        $payment_model_list = [];
        if ($payment_modes) {
            foreach($payment_modes as $payment_mode){
                $payment_model_list[] = $payment_mode['id'];
            }
        }

        foreach ($list_results as $invoice) {
            $check_connect_id = $this->check_connect_id($invoice['id'], 'invoice', 'sage_accounting');
            

            $customer_connect_id = $this->check_connect_id($invoice['contact']['id'], 'customer', 'sage_accounting');
            if($customer_connect_id == 0){
                $this->get_quickbook_customer();
                $customer_connect_id = $this->check_connect_id($invoice['contact']['id'], 'customer', 'sage_accounting');
            }

            $invoice_data = [];

            $currency = get_currency($invoice['currency']['id']);
            $invoice_data['currency'] = '1';

            $invoice_data['date'] = $invoice['date'];
            $invoice_data['duedate'] = $invoice['due_date'];

            $invoice_data['clientid']         = $customer_connect_id;

            $invoice_data['include_shipping']         = 1;
            $invoice_data['show_shipping_on_invoice'] = 1;

            $invoice_data["allowed_payment_modes"] = $payment_model_list;

            $billing_street = '';
            $billing_city = '';
            $billing_state = '';
            $billing_zip = '';
            $billing_country = '';
            if($invoice['main_address']){
                $billing_street = $invoice['main_address']['address_line_1'];
                $billing_city = $invoice['main_address']['city'];
                $billing_country = $invoice['main_address']['region'];
                $billing_zip = $invoice['main_address']['postal_code'];
            }

            $invoice_data['billing_street'] = $billing_street;
            $invoice_data['billing_city'] = $billing_city;
            $invoice_data['billing_state'] = '';
            $invoice_data['billing_zip'] = $billing_zip;
            $invoice_data['billing_country'] = $billing_country;

            $shipping_street = '';
            $shipping_city = '';
            $shipping_state = '';
            $shipping_zip = '';
            $shipping_country = '';
            if($invoice['delivery_address']){
                $shipping_street = $invoice['delivery_address']['address_line_1'];
                $shipping_city = $invoice['delivery_address']['city'];
                $shipping_country = $invoice['delivery_address']['region'];
                $shipping_zip = $invoice['delivery_address']['postal_code'];
            }

            $invoice_data['shipping_street'] = $shipping_street;
            $invoice_data['shipping_city'] = $shipping_city;
            $invoice_data['shipping_state'] = $shipping_state;
            $invoice_data['shipping_zip'] = $shipping_zip;
            $invoice_data['shipping_country'] = $shipping_country;
            $invoice_data['total']               = $invoice['total_amount'];

            $newitems = [];

            $discount_total = 0;
            $subtotal = 0;
            foreach ($invoice['invoice_lines'] as $key => $value) {
                $tax_arr = '';

                array_push($newitems, array(
                    'order' => $key, 
                    'description' => $value['displayed_as'] ?? $value['description'], 
                    'long_description' => '', 
                    'qty' => $value['quantity'] ?? 1, 
                    'unit' => '', 
                    'rate' => $value['unit_price'], 
                    'taxname' => $tax_arr));
                $subtotal += ($value['quantity'] * $value['unit_price']);

                if($value['discount_amount'] > 0){
                    $discount_total += $value['discount_amount'];
                }
            }

            
            if($invoice['shipping_total_amount'] > 0){
                $invoice_data['adjustment']  = $invoice['shipping_total_amount'];
            }

            if($discount_total > 0){
                $invoice_data['discount_type']               = 'before_tax';
                $invoice_data['discount_total']               = $discount_total;
            }

            $invoice_data['subtotal']            = $subtotal;

            $invoice_data['newitems'] = $newitems;
            
            if($check_connect_id != 0 && isset($invoice_arr[$check_connect_id])){
                $this->delete_invoice_item($check_connect_id);
                $this->invoices_model->update($invoice_data, $check_connect_id);
                $invoice_id = $check_connect_id;
            }else{
                if($check_connect_id != 0){
                    $this->delete_integration_log($check_connect_id, 'invoice', 'sage_accounting');
                }

                $__number        = get_option('next_invoice_number');
                $_invoice_number = str_pad($__number, get_option('number_padding_prefixes'), '0', STR_PAD_LEFT);
                $invoice_data['number']              = $_invoice_number;

                $invoice_id = $this->invoices_model->add($invoice_data);
            }

            $sync_status = 0;
            if($invoice_id){
            $sync_status = 1;
                if(!isset($invoice_arr[$check_connect_id])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $invoice_id,
                        'rel_type' => 'invoice',
                        'software' => 'sage_accounting',
                        'connect_id' => $invoice['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }
            }

            $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                        'rel_id' => $invoice_id,
                        'rel_type' => 'invoice',
                        'software' => 'sage_accounting',
                        'type' => 'sync_down',
                        'status' => $sync_status,
                        'connect_id' => $invoice['id'],
                        'datecreated' => date('Y-m-d H:i:s'),
                    ]);
        }
    }

    public function get_sage_accounting_payment(){
        $this->load->model('payments_model');
        $payment_list = $this->db->get(db_prefix() . 'invoicepaymentrecords')->result_array();
     
        $payment_arr = [];
        foreach ($payment_list as $payment) {
            $payment_arr[$payment['id']] = $payment;
        }

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/contact_payments?attributes=all&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                $list_results = array_merge($list_results, $entities['$items']);
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        $this->load->model('payment_modes_model');

        $payment_modes = $this->payment_modes_model->get();

        $fisrt_payment_mode = 0;
        if (isset($payment_modes[0])) {
            $fisrt_payment_mode = $payment_modes[0]['id'];
        }

        foreach ($list_results as $payment) {
            $check_connect_id = $this->check_connect_id($payment['id'], 'payment', 'sage_accounting');

            if($payment['transaction_type']['id'] != 'CUSTOMER_RECEIPT'){
                continue;
            }
           
            $invoice_id = 0;
            $amount = round($payment['total_amount'], 2);
            foreach ($payment['allocated_artefacts'] as $key => $value) {
                $invoice_id = $this->check_connect_id($value['artefact']['id'], 'invoice', 'sage_accounting');
                $amount = round($value['amount'], 2);
            }

            if($invoice_id == 0){
                continue;
            }

            $payment_data = [];

            $payment_data['invoiceid'] = $invoice_id;
            $payment_data['amount'] = $amount;
            $payment_data['date'] = $payment['date'];
            $payment_data['transactionid'] = '';
            $payment_data['note'] = '';
            
            if($check_connect_id != 0 && isset($payment_arr[$check_connect_id])){
                $this->payments_model->update($payment_data, $check_connect_id);
                $payment_id = $check_connect_id;
            }else{
                if($check_connect_id != 0){
                    $this->delete_integration_log($check_connect_id, 'payment', 'sage_accounting');
                }

                $payment_data['do_not_send_email_template'] = 'on';
                $payment_data['do_not_redirect'] = 'on';
                $payment_data["paymentmode"] = $fisrt_payment_mode;
                $payment_id = $this->payments_model->add($payment_data);
            }

            $sync_status = 0;
            if($payment_id){
            $sync_status = 1;
                if(!isset($payment_arr[$check_connect_id])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $payment_id,
                        'rel_type' => 'payment',
                        'software' => 'sage_accounting',
                        'connect_id' => $payment['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }
            }

            $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                        'rel_id' => $payment_id,
                        'rel_type' => 'payment',
                        'software' => 'sage_accounting',
                        'type' => 'sync_down',
                        'status' => $sync_status,
                        'connect_id' => $payment['id'],
                        'datecreated' => date('Y-m-d H:i:s'),
                    ]);
        }
    }

    public function get_sage_accounting_expense(){
        $this->load->model('expenses_model');
        $expense_list = $this->expenses_model->get();
     
        $expense_arr = [];
        foreach ($expense_list as $expense) {
            $expense_arr[$expense['id']] = $expense;
        }

        $header = acc_get_sage_accounting_header();
        $api_domain = acc_get_sage_accounting_api_domain();

        $continue = true;
        $page = 1;

        $list_results = [];
        while ($continue) {
            $continue = false;
            $url = $api_domain.'/purchase_quick_entries?attributes=all&items_per_page=200&page='.$page;
            $entities = $this->callAPI($url, [], $header, 'GET');
            $page++;
            if (isset($entities['$items'])) {
                $list_results = array_merge($list_results, $entities['$items']);
            }

            if(isset($entities['$next']) && $entities['next'] != NULL){
                $continue = true;
            }
        }

        $this->load->model('payment_modes_model');
        $payment_modes = $this->payment_modes_model->get();

        $fisrt_payment_mode = 0;
        if (isset($payment_modes[0])) {
            $fisrt_payment_mode = $payment_modes[0]['id'];
        }

        foreach ($list_results as $expense) {
            $check_connect_id = $this->check_connect_id($expense['id'], 'expense', 'sage_accounting');
            

            $url = $api_domain.'/books/v3/expenses/'.$expense['id'];

            $expense_data = [];

            $expense_data['vendor'] = '';
            $expense_data['expense_name'] = '';
            $expense_data['note'] = '';
            $expense_data['category'] = $this->init_expense_category('Sage Accounting Expenses');
            $expense_data['date'] = $expense['date'];
            $expense_data['amount'] = $expense['total_amount'];
            $expense_data['clientid'] = '';
            $expense_data['project_id'] = '';

            $currency = get_currency($expense['currency']['id']);
            $expense_data['currency'] = '1';

            $expense_data['tax'] = '';
            $expense_data['tax2'] = '';
            $expense_data['paymentmode'] = '';
            $expense_data['reference_no'] = '';
            $expense_data['repeat_every'] = '';
            $expense_data['repeat_every_custom'] = '1';
            $expense_data['repeat_type_custom'] = 'day';

            if($check_connect_id != 0 && isset($expense_arr[$check_connect_id])){
                $this->expenses_model->update($expense_data, $check_connect_id);
                $expense_id = $check_connect_id;
            }else{
                if($check_connect_id != 0){
                    $this->delete_integration_log($check_connect_id, 'expense', 'sage_accounting');
                }

                $expense_id = $this->expenses_model->add($expense_data);
            }

            $sync_status = 0;
            if($expense_id){
            $sync_status = 1;
                if(!isset($expense_arr[$check_connect_id])){
                    $this->db->insert(db_prefix().'acc_integration_logs', [
                        'rel_id' => $expense_id,
                        'rel_type' => 'expense',
                        'software' => 'sage_accounting',
                        'connect_id' => $expense['id'],
                        'date_updated' => date('Y-m-d H:i:s'),
                    ]);
                }
            }

            $this->db->insert(db_prefix().'acc_integration_sync_logs', [
                        'rel_id' => $expense_id,
                        'rel_type' => 'expense',
                        'software' => 'sage_accounting',
                        'type' => 'sync_down',
                        'status' => $sync_status,
                        'connect_id' => $expense['id'],
                        'datecreated' => date('Y-m-d H:i:s'),
                    ]);
        }
    }

    public function init_expense_category($name)
    {
        $this->db->where('name', $name);

        $expenses_categorie = $this->db->get(db_prefix() . 'expenses_categories')->row();

        if($expenses_categorie){
            return $expenses_categorie->id;
        }
        
        $this->load->model('expenses_model');
        $id = $this->expenses_model->add_category(['name' => $name, 'description' => '']);

        return $id;
    }

    public function executeRequest($url, $parameters = array(), $http_header = '', $http_method = '')
    {

      $curl_options = array();

      switch($http_method){
            case 'GET':
              $curl_options[CURLOPT_HTTPGET] = 'true';
              if (is_array($parameters) && count($parameters) > 0) {
                $url .= '?' . http_build_query($parameters);
              } elseif ($parameters) {
                $url .= '?' . $parameters;
              }
              break;
            case 'POST':
              $curl_options[CURLOPT_POST] = '1';
              if(is_array($parameters) && count($parameters) > 0){
                $body = http_build_query($parameters);
                $curl_options[CURLOPT_POSTFIELDS] = $body;
              }
              break;
            default:
              break;
      }
      /**
      * An array of HTTP header fields to set, in the format array('Content-type: text/plain', 'Content-length: 100')
      */
      if(is_array($http_header)){
            $header = array();
            foreach($http_header as $key => $value) {
                $header[] = "$key: $value";
            }
            $curl_options[CURLOPT_HTTPHEADER] = $header;
      }

      $curl_options[CURLOPT_URL] = $url;
      $ch = curl_init();

      //debug_backtrace
      //curl_setopt($ch, CURLOPT_VERBOSE, true);
      //$verbose = fopen('php://temp', 'w+');
      //curl_setopt($ch, CURLOPT_STDERR, $verbose);


      curl_setopt_array($ch, $curl_options);

      //curl_setopt($ch, CURLINFO_HEADER_OUT, true);
      //Don't display, save it on result
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

      //Execute the Curl Request
      $result = curl_exec($ch);

      $headerSent = curl_getinfo($ch, CURLINFO_HEADER_OUT );

      $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);


      $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
       if ($curl_error = curl_error($ch)) {
           throw new Exception($curl_error);
       } else {
           $json_decode = json_decode($result, true);
       }
       curl_close($ch);

       return $json_decode;
    }

    public function delete_integration_error_log($rel_id, $rel_type, $software){
        $this->db->where('rel_id', $rel_id);
        $this->db->where('rel_type', $rel_type);
        $this->db->delete(db_prefix().'acc_integration_error_logs');

        if ($this->db->affected_rows() > 0) {
            return true;
        }
        return false;
    }

    public function check_connect_id($connect_id, $rel_type, $software = 'quickbook'){
        $this->db->where('connect_id', $connect_id);
        $this->db->where('rel_type', $rel_type);
        $this->db->where('software', $software);
        $log = $this->db->get(db_prefix().'acc_integration_logs')->row();

        if($log){
            return $log->rel_id;
        }

        return 0;
    }


    public function callAPI($url, $params, $header, $method = 'POST'){
            $data_string = json_encode($params);

            $curl = curl_init($url);
            
            curl_setopt($curl, CURLOPT_URL, $url);
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $header);

            if($method == 'POST' || $method == 'PUT'){
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data_string);
            }

            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
            curl_setopt($curl, CURLOPT_AUTOREFERER, 1);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
            curl_setopt($curl, CURLOPT_TIMEOUT, 120);
            curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 120);
            curl_setopt($curl, CURLOPT_MAXREDIRS, 10);
            
            $result = curl_exec($curl);

            $result = json_decode($result, true);

            return $result;
    }

    /**
     * [get_invoice_item_tax description]
     * @param  [type] $item
     * @return [type]      
     */
    public function get_invoice_item_tax($item, $invoice){
        $data_return = [];
        $data_return['tax_id'] = [];
        $data_return['tax_amount'] = [];
        $data_return['tax_rate'] = [];
        $item_total = $item['rate'] * $item['qty'];
        $this->db->where('itemid', $item['id']);
        $item_tax = $this->db->get(db_prefix().'item_tax')->result_array();

        foreach($item_tax as $tax){
            $this->db->where('taxrate', $tax['taxrate']);
            $this->db->where('name', $tax['taxname']);
            $_tax = $this->db->get(db_prefix().'taxes')->row();
            if($_tax){
                $data_return['tax_rate'][] = $tax['taxrate'];

                $data_return['tax_id'][] = $_tax->id;
                if($invoice['discount_type'] == 'before_tax'){
                    $total_tax = $item_total * $tax['taxrate'] / 100;
                    $t = ($invoice['discount_total'] / $invoice['subtotal']) * 100;

                    $data_return['tax_amount'][] = ($total_tax - $total_tax*$t/100);
                }else{
                    $data_return['tax_amount'][] = $item_total * $tax['taxrate'] / 100;
                }

            }
        }

        return $data_return;
    }

    public function delete_integration_log($rel_id, $rel_type, $software){
        $this->db->where('rel_id', $rel_id);
        $this->db->where('rel_type', $rel_type);
        $this->db->where('software', $software);
        $this->db->delete(db_prefix().'acc_integration_logs');

        if ($this->db->affected_rows() > 0) {
            return true;
        }
        return false;
    }

    public function get_connect_id_old($rel_id, $rel_type, $software = 'quickbook'){
        $this->db->where('rel_id', $rel_id);
        $this->db->where('rel_type', $rel_type);
        $this->db->where('software', $software);
        $log = $this->db->get(db_prefix().'acc_integration_logs')->row();

        if($log){
            return $log->connect_id;
        }

        return '';
    }

    public function delete_invoice_item($id){
        $items = get_items_by_type('invoice', $id);

        $this->db->where('rel_id', $id);
        $this->db->where('rel_type', 'invoice');
        $this->db->delete(db_prefix() . 'itemable');

        foreach ($items as $item) {
            $this->db->where('item_id', $item['id']);
            $this->db->delete(db_prefix() . 'related_items');
        }
        $this->db->where('rel_id', $id);
        $this->db->where('rel_type', 'invoice');
        $this->db->delete(db_prefix() . 'item_tax');

        return true;
    }








    /* ======================= PERFEX → PERFEX SECTION ======================= */

protected $perfex_ctx = []; // will hold ['source_key'=>..., 'base_url'=>..., 'authtoken'=>...]

/** Init for one satellite Perfex by key */
public function init_perfex_config($source_key)
{
    $conn = get_acc_perfex_connection($source_key);
    if (!$conn || empty($conn['base_url']) || empty($conn['authtoken'])) {
        throw new Exception('Perfex connection missing for source_key: ' . $source_key);
    }
    $this->perfex_ctx = [
        'source_key' => $source_key,
        'base_url'   => rtrim($conn['base_url'], '/'),
        'authtoken'  => $conn['authtoken'],
    ];
}

/** Generic REST call to remote Perfex */
// keep your helper acc_get_perfex_header for baseline headers if you like
// but don't force Content-Type for writes.
// Put this in your model (private)
private function curl_flatten_form(array $data, string $prefix = ''): array
{
    $out = [];
    foreach ($data as $key => $val) {
        $k = $prefix !== '' ? "{$prefix}[{$key}]" : $key;

        if (is_array($val)) {
            // recurse for nested arrays (e.g., newitems)
            $out += $this->curl_flatten_form($val, $k);
        } elseif ($val instanceof CURLFile) {
            $out[$k] = $val; // allow files too
        } elseif (is_bool($val)) {
            $out[$k] = $val ? '1' : '0';
        } elseif ($val === null) {
            // skip nulls
        } else {
            // cast scalars to string to be safe
            $out[$k] = (string)$val;
        }
    }
    return $out;
}

protected function perfex_call_api($method, $path, $payload = null, $query = [], $mode = 'json')
{
    $headers = [
        'authtoken: ' . $this->perfex_ctx['authtoken'],
        'Accept: application/json',
    ];

    $url = acc_perfex_api_url($this->perfex_ctx['base_url'], $path);
    if (!empty($query)) {
        $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query);
    }
     // Better logging
// $isWrite = in_array(strtoupper($method), ['POST','PUT','PATCH'], true);
//  log_message(
//         'error',
//         sprintf(
//             'API %s %s | mode=%s | query=%s | payload=%s',
//             strtoupper($method),
//             $url,
//             $mode,
//             $query ? json_encode($query) : '—',
//             $isWrite ? json_encode($payload) : '—'
//         )
//     );
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));

    if (in_array(strtoupper($method), ['POST','PUT','PATCH'], true)) {
        if ($mode === 'json') {
            $headers[] = 'Content-Type: application/json';
            $body = is_array($payload) ? json_encode($payload) : ($payload ?? '');
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        } else { // 'form' => multipart/form-data
            // DO NOT set Content-Type manually; let cURL set boundary.
            $fields = is_array($payload) ? $this->curl_flatten_form($payload) : [];
            curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
        }
    }

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $resp = curl_exec($ch);
    $err  = curl_error($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($resp === false) {
        return ['error'=>true,'status'=>0,'body'=>'cURL error: '.$err];
    }
    if ($code >= 400) {
        return ['error'=>true,'status'=>$code,'body'=>$resp];
    }
    $json = json_decode($resp, true);
    return $json !== null ? $json : $resp;
}


protected function perfex_call_api__OLD($method, $path, $payload = null, $query = [], $mode = 'json')
{
    $headers = [
        'authtoken: ' . $this->perfex_ctx['authtoken'],
        'Accept: application/json',
    ];
    $url = acc_perfex_api_url($this->perfex_ctx['base_url'], $path);
    if (!empty($query)) {
        $url .= (strpos($url,'?')===false ? '?' : '&') . http_build_query($query);
    }

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));

    if (in_array(strtoupper($method), ['POST','PUT','PATCH'])) {
        if ($mode === 'json') {
            $headers[] = 'Content-Type: application/json';
            curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($payload) ? json_encode($payload) : ($payload ?? ''));
        } else { // multipart form-data
            // IMPORTANT: do NOT set Content-Type manually. Let cURL add the boundary.
            // Send a flat array like ['company'=>'Acme', 'city'=>'KL', ...]
            curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($payload) ? $payload : []);
        }
    }

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $resp = curl_exec($ch);
    $err  = curl_error($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($resp === false) {
        return ['error'=>true,'status'=>0,'body'=>'cURL error: '.$err];
    }
    if ($code >= 400) {
        return ['error'=>true,'status'=>$code,'body'=>$resp];
    }
    $json = json_decode($resp, true);
    return $json !== null ? $json : $resp;
}


/** Helper: read connect_id for specific software tag (we will store 'perfex:EVENTA') */
protected function get_connect_id($rel_id, $rel_type, $software_tag)
{
    $row = $this->db->where('rel_id', $rel_id)
                    ->where('rel_type', $rel_type)
                    ->where('software', $software_tag)
                    ->get(db_prefix().'acc_integration_logs')->row();
    return $row ? $row->connect_id : '';
}

/** Helper: upsert log record */
protected function upsert_connect_log($rel_id, $rel_type, $software_tag, $connect_id)
{
    $exists = $this->db->where('rel_id',$rel_id)
                       ->where('rel_type',$rel_type)
                       ->where('software',$software_tag)
                       ->get(db_prefix().'acc_integration_logs')->row();
    $payload = [
        'rel_id' => $rel_id,
        'rel_type' => $rel_type,
        'software' => $software_tag,
        'connect_id' => $connect_id,
        'date_updated' => date('Y-m-d H:i:s'),
    ];
    if ($exists) {
        $this->db->where('id',$exists->id)->update(db_prefix().'acc_integration_logs', $payload);
    } else {
        $this->db->insert(db_prefix().'acc_integration_logs', $payload);
    }
}

/** Common logger (same pattern you used for Sage) */
protected function log_sync($ok, $rel_id, $rel_type, $software_tag, $type, $connect_id = '', $error_detail = '')
{
    if (!$ok && $error_detail) {
        $this->db->insert(db_prefix().'acc_integration_error_logs', [
            'rel_id' => $rel_id,
            'rel_type' => $rel_type,
            'software' => $software_tag,
            'error_detail' => $error_detail,
            'date_updated' => date('Y-m-d H:i:s'),
        ]);
    } else {
        // clean previous error for this rel
        $this->db->where('rel_id',$rel_id)
                 ->where('rel_type',$rel_type)
                 ->where('software',$software_tag)
                 ->delete(db_prefix().'acc_integration_error_logs');
    }

    $this->db->insert(db_prefix().'acc_integration_sync_logs', [
        'rel_id' => $rel_id,
        'rel_type' => $rel_type,
        'software' => $software_tag,
        'type' => $type, // sync_up or sync_down
        'status' => $ok ? 1 : 0,
        'connect_id' => $connect_id,
        'datecreated' => date('Y-m-d H:i:s'),
    ]);
}

/* ======================= PUSH (UP) CENTRAL → SATELLITE ======================= */

/** Customers → remote Perfex */
// ---- create -> upsert (CUSTOMER) ----
public function create_perfex_customer($customer_id = '', $source_key = '')
{
    // Keep old signature; controller passes both $id and $source_key for Perfex
    if ($source_key === '') {
        return 'Missing source_key';
    }

    // Single record (manual_sync button)
    if ($customer_id !== '') {
        return $this->upsert_perfex_customer((int)$customer_id, $source_key);
    }

    // Bulk path (optional – used by cron or “sync all” buttons)
    $software_tag = 'perfex:'.$source_key;

    // Prefer your existing helper if present
    if (method_exists($this, 'get_unsynced_ids')) {
        $ids = $this->get_unsynced_ids('customer', $software_tag, 200);
    } else {
        // Fallback: sync all customers
        $ids = array_map(function($r){ return (int)$r['userid']; },
            $this->db->select('userid')->get(db_prefix().'clients')->result_array()
        );
    }

    $ok = 0; $fail = 0; $lastErr = '';
    foreach ($ids as $cid) {
        $cid = is_array($cid) ? (int)($cid['userid'] ?? $cid['id'] ?? 0) : (int)$cid;
        if (!$cid) { continue; }
        $res = $this->upsert_perfex_customer($cid, $source_key);
        if ($res === true) { $ok++; } else { $fail++; $lastErr = (string)$res; }
    }
    return $fail ? "Synced {$ok}, failed {$fail}. Last error: {$lastErr}" : true;
}

public function create_perfex_customer__OLD($customer_id = '', $source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    // Pull clients + primary contact id (so we can create a contact)
    $this->db->select('*, '.db_prefix().'clients.userid as userid, '.db_prefix().'contacts.id as contact_id');
    if ($customer_id != '') {
        $this->db->where(db_prefix().'clients.userid', $customer_id);
    }
    $this->db->join(db_prefix().'contacts', db_prefix().'contacts.userid=' . db_prefix().'clients.userid AND '.db_prefix().'contacts.is_primary = "1"', 'left');
    $this->db->join(db_prefix().'acc_integration_logs', db_prefix().'acc_integration_logs.rel_id=' . db_prefix().'clients.userid AND '.db_prefix().'acc_integration_logs.rel_type="customer" AND software="'.$software_tag.'"', 'left');
    $clients = $this->db->get(db_prefix().'clients')->result_array();

    foreach ($clients as $client) {
        // 1) guarantee company
        $company = trim((string)($client['company'] ?? ''));
        if ($company === '') {
            $full = trim(((string)($client['firstname'] ?? '')).' '.((string)($client['lastname'] ?? '')));
            $company = $full !== '' ? $full : (!empty($client['email']) ? $client['email'] : 'Client '.$client['userid']);
        }

        // 2) build flat multipart payload (omit empties)
        $payload = [
            'company'          => $company,
            'vat'              => (string)($client['vat'] ?? ''),
            'phonenumber'      => (string)($client['phonenumber'] ?? ''),
            'website'          => (string)($client['website'] ?? ''),
            'address'          => (string)($client['address'] ?? ''),
            'city'             => (string)($client['city'] ?? ''),
            'state'            => (string)($client['state'] ?? ''),
            'zip'              => (string)($client['zip'] ?? ''),
            'billing_street'   => (string)($client['billing_street'] ?? ''),
            'billing_city'     => (string)($client['billing_city'] ?? ''),
            'billing_state'    => (string)($client['billing_state'] ?? ''),
            'billing_zip'      => (string)($client['billing_zip'] ?? ''),
            'shipping_street'  => (string)($client['shipping_street'] ?? ''),
            'shipping_city'    => (string)($client['shipping_city'] ?? ''),
            'shipping_state'   => (string)($client['shipping_state'] ?? ''),
            'shipping_zip'     => (string)($client['shipping_zip'] ?? ''),
            'default_currency' => (string)($client['default_currency'] ?? ''),
            'active'           => (string)((int)($client['active'] ?? 1)),
            'default_language' => (string)($client['default_language'] ?? ''),
            'show_primary_contact' => (string)((int)($client['show_primary_contact'] ?? 0)),
        ];
        foreach ($payload as $k=>$v) { if ($v === '' || $v === null) unset($payload[$k]); }

        // 3) If mapping exists → PUT, else POST
        $connect_id = $client['connect_id'] ?? $this->get_connect_id($client['userid'], 'customer', $software_tag);

        if ($connect_id) {
            $res = $this->perfex_call_api('PUT', 'customers/'.$connect_id, $payload, [], 'form');
        } else {
            // small de-dupe guard: if you want, prefetch and match by company
            $res = $this->perfex_call_api('POST','customers', $payload, [], 'form');
        }

        // 4) Extract remote id robustly
        $remoteId = $this->perfex_extract_id($res);
        if (!$remoteId && is_array($res) && isset($res['status']) && $res['status']===true && isset($res['message'])) {
            // some builds return only status/message; try a light lookup by company
            $all = $this->perfex_call_api('GET', 'customers', null, [], 'json');
            if (is_array($all)) {
                foreach ($all as $r) {
                    if (isset($r['company']) && $r['company'] === $company) {
                        $remoteId = (string)($r['userid'] ?? $r['id'] ?? null);
                        if ($remoteId) break;
                    }
                }
            }
        }

        if (!$remoteId) {
            $body = is_array($res) ? json_encode($res) : (string)$res;
            $this->log_sync(false, $client['userid'], 'customer', $software_tag, 'sync_up', '', 'Missing remote id: '.$body);
            if ($customer_id != '') { return 'Missing remote id'; }
            continue;
        }

        // 5) Save mapping & success log
        $this->upsert_connect_log($client['userid'], 'customer', $software_tag, $remoteId);
        $this->log_sync(true, $client['userid'], 'customer', $software_tag, 'sync_up', $remoteId);

        // 6) Ensure PRIMARY CONTACT exists remotely
        $this->ensure_perfex_primary_contact($client, $remoteId, $software_tag, $source_key);
        if ($customer_id != '') { return true; }
    }
}


private function ensure_perfex_primary_contact(array $clientRow, $remoteCustomerId, $software_tag, $source_key)
{
    // ✅ Correct: central contact id (from your join) not the customer id
    $centralContactId = $clientRow['contact_id'] ?? null;

    $first = trim((string)($clientRow['firstname'] ?? ''));
    $last  = trim((string)($clientRow['lastname'] ?? ''));
    $email = trim((string)($clientRow['email'] ?? ''));
    $phone = trim((string)($clientRow['phonenumber'] ?? ''));
    $password = trim((string)($clientRow['password'] ?? ''));

    if ($email === '') {
        $this->log_sync(false, $centralContactId ?: $clientRow['userid'], 'contact', $software_tag, 'sync_up', '', 'Missing primary contact email');
        return;
    }

    // 1) See if this contact already exists remotely for this customer (by email)
    $list = $this->perfex_search_contact_by_email($email);
    $existingId = $this->pick_contact_id_for_customer($list, $remoteCustomerId);

    $payload = [
        'customer_id' => (string)$remoteCustomerId,
        'firstname'   => $first !== '' ? $first : ($clientRow['company'] ?? 'Contact'),
        'lastname'    => $last  !== '' ? $last  : '-',
        'email'       => $email,
        'phonenumber' => $phone,
        'password'    => $password,            // optional; remove if not desired
        'is_primary'  => 'on',
        'donotsendwelcomeemail' => 'on',
        // 'permissions' => ['1','2','5','6'],
        // 'invoice_emails' => 'invoice_emails',
    ];
    foreach ($payload as $k=>$v) { if ($v === '' || $v === null) unset($payload[$k]); }

    // 2) PUT if we found an existing contact, else POST (multipart form-data)
    if ($existingId) {
        $res = $this->perfex_call_api('PUT', 'contacts/'.$existingId, $payload, [], 'form');
        $rid = $existingId; // we already know it
    } else {
        $res = $this->perfex_call_api('POST','contacts', $payload, [], 'form');

        // Try to read id directly
        $rid = $this->perfex_extract_id($res);

        // Some builds return only {status:true,message:"Contact added successfully."}
        if (!$rid && is_array($res) && !empty($res['status'])) {
            $list2 = $this->perfex_search_contact_by_email($email);
            $rid   = $this->pick_contact_id_for_customer($list2, $remoteCustomerId);
        }
    }

    if ($rid) {
        if ($centralContactId) {
            $this->upsert_connect_log($centralContactId, 'contact', $software_tag, $rid);
        }
        $this->log_sync(true, $centralContactId ?: $clientRow['userid'], 'contact', $software_tag, 'sync_up', $rid);
    } else {
        $body = is_array($res) ? json_encode($res) : (string)$res;
        $this->log_sync(false, $centralContactId ?: $clientRow['userid'], 'contact', $software_tag, 'sync_up', '', 'Contact create/update failed: '.$body);
    }
}


private function push_perfex_customer_custom_fields(int $localCustomerId, int $remoteCustomerId, string $source_key, string $software_tag)
{
    // Load all custom fields values for this local customer
    $rows = $this->db->where([
                'relid'  => $localCustomerId,
                'fieldto'=> 'customers',            // Perfex core target for customer CFs
            ])->get(db_prefix().'customfieldsvalues')->result_array();

    foreach ($rows as $r) {
        $payload = [
            'relid'   => $remoteCustomerId,        // IMPORTANT: remote id here
            'fieldid' => (int)$r['fieldid'],
            'fieldto' => (string)$r['fieldto'],    // "customers"
            'value'   => (string)$r['value'],
        ];

        // Optional per-field fingerprint to avoid redundant calls
        $fpKeyRelType = 'cfv_customer_'.$r['fieldid'];
        $fp = $this->acc_make_fp($payload);
        if ($this->acc_get_fp($software_tag, $fpKeyRelType, $localCustomerId) === $fp) {
            // Already in-sync
            continue;
        }

        // POST to remote: /api/custom_fields_values
        $res = $this->perfex_call_api('POST', 'custom_fields_values', $payload, [], 'form', $source_key);

        // Treat any {status:true,...} as success
        if (is_array($res) && !empty($res['status'])) {
            $this->acc_set_fp($software_tag, $fpKeyRelType, $localCustomerId, $fp);
            // (Optional) log success
            $this->log_sync(true, $localCustomerId, 'customer_custom_field', $software_tag, 'sync_up', (string)($res['id'] ?? ''));
        } else {
            // (Optional) log error for diagnostics
            $body = is_array($res) ? json_encode($res) : (string)$res;
            $this->log_sync(false, $localCustomerId, 'customer_custom_field', $software_tag, 'sync_up', '', 'CF upsert failed: '.$body);
        }
    }
}



private function perfex_search_contact_by_email($email) {
    // Build path with URL-encoded email
    $path = 'contacts/search/' . rawurlencode(trim($email));

    $resp = $this->perfex_call_api('GET', $path, null, [], 'json');

    // perfex_call_api may return string, array, or error array
    if (is_string($resp)) {
        $tmp = json_decode($resp, true);
        if (json_last_error() === JSON_ERROR_NONE) $resp = $tmp;
    }
    if (is_array($resp) && isset($resp['error']) && $resp['error'] === true) {
        // Treat "No data were found" as empty list
        if (!empty($resp['body'])) {
            $tmp = json_decode($resp['body'], true);
            if (is_array($tmp) && isset($tmp['status']) && $tmp['status'] === false) {
                return [];
            }
        }
        return [];
    }

    // Expect an array of contacts (possibly 0/1/many)
    return is_array($resp) ? $resp : [];
}

private function pick_contact_id_for_customer(array $list, $remoteCustomerId) {
    foreach ($list as $rc) {
        // From your sample payload: "id", "userid" (remote customer id)
        $rcCust = (string)($rc['userid'] ?? $rc['customer_id'] ?? $rc['clientid'] ?? '');
        if ((string)$remoteCustomerId === $rcCust) {
            return (string)($rc['id'] ?? $rc['contact_id'] ?? $rc['contactid'] ?? null);
        }
    }
    return null;
}


private function perfex_extract_id($res) {
    if (is_string($res)) {
        $tmp = json_decode($res, true);
        if (json_last_error() === JSON_ERROR_NONE) $res = $tmp;
    }
    if (!is_array($res)) return null;

    $candidates = [
        'id','userid','customer_id','clientid','contact_id','contactid','insert_id',
        'data.id','data.userid','data.customer_id','data.clientid','data.contact_id','data.contactid','data.insert_id',
        'customer.id','contact.id'
    ];

    foreach ($candidates as $key) {
        if (strpos($key,'.') !== false) {
            [$a,$b] = explode('.',$key,2);
            if (isset($res[$a]) && is_array($res[$a]) && !empty($res[$a][$b])) {
                return (string)$res[$a][$b];
            }
        } else {
            if (!empty($res[$key])) return (string)$res[$key];
        }
    }
    return null;
}


/** Invoices → remote Perfex (simplified: header only; add items if your REST exposes them) */
// Resolve remote currency id by name/code, fallback to first
private function perfex_resolve_currency_id($source_key, $localCurrencyNameOrCode)
{
    $res = $this->perfex_call_api('GET', 'currencies', null, [], 'json', $source_key);
    if (is_string($res)) { $tmp = json_decode($res, true); if (json_last_error()===JSON_ERROR_NONE) $res=$tmp; }
    if (!is_array($res)) return 1;

    $name = strtolower(trim((string)$localCurrencyNameOrCode));
    foreach ($res as $c) {
        $n = strtolower(trim((string)($c['name'] ?? $c['currency'] ?? '')));
        $code = strtolower(trim((string)($c['symbol'] ?? $c['code'] ?? '')));
        if ($name !== '' && ($n === $name || $code === $name)) {
            return (int)($c['id'] ?? 1);
        }
    }
    // fallback: first active
    return (int)($res[0]['id'] ?? 1);
}

// Return a non-empty array of remote payment mode IDs
private function perfex_fetch_payment_mode_ids($source_key)
{
    $res = $this->perfex_call_api('GET', 'payment_modes', null, [], 'json', $source_key);
    if (is_string($res)) { $tmp = json_decode($res, true); if (json_last_error()===JSON_ERROR_NONE) $res=$tmp; }
    $ids = [];
    if (is_array($res)) {
        foreach ($res as $pm) {
            // accept all active modes
            if (!isset($pm['active']) || (int)$pm['active'] === 1) {
                $ids[] = (int)($pm['id'] ?? 0);
            }
        }
    }
    if (empty($ids)) $ids = [1]; // safe fallback
    return $ids;
}

// Build a single remote item payload; include taxes if you can resolve them
private function perfex_build_item_payload(array $item, array $invoice): array
{
    $payload = [
        'description'       => (string)($item['description'] ?? ''),
        'long_description'  => (string)($item['long_description'] ?? $item['long_description'] ?? ''),
        'qty'               => (float)$item['qty'],
        'rate'              => (float)$item['rate'],
        'unit'              => (string)($item['unit'] ?? ''),
    ];

    // If you have a helper that returns taxes for this line, map to taxname array: "NAME|RATE"
    // Example using your existing tax helper:
    if (method_exists($this, 'get_invoice_item_tax')) {
        $taxData = $this->get_invoice_item_tax($item, $invoice);
        // Expect $taxData['taxes'] like [['name'=>'SST','rate'=>6], ...] – adapt if your helper differs
        if (isset($taxData['taxes']) && is_array($taxData['taxes']) && count($taxData['taxes']) > 0) {
            $taxnames = [];
            foreach ($taxData['taxes'] as $t) {
                $tname = (string)($t['name'] ?? 'TAX');
                $trate = (float)($t['rate'] ?? 0);
                $taxnames[] = $tname.'|'.$trate;  // Perfex expects "Name|Rate"
            }
            if ($taxnames) $payload['taxname'] = $taxnames;
        }
    }

    return $payload;
}

// Safe number to string
private function perfex_safe_date($sqlDate)
{
    // $sqlDate is usually Y-m-d in Perfex already; normalize just in case
    return date('Y-m-d', strtotime($sqlDate));
}

// ---- create -> upsert (INVOICE) ----
public function create_perfex_invoice($invoice_id = '', $source_key = '')
{
    if ($source_key === '') {
        return 'Missing source_key';
    }

    // Single record (manual_sync button)
    if ($invoice_id !== '') {
        //log_message('error', "UpdateInvoice:".json_encode($invoice_id));
        return $this->upsert_perfex_invoice((int)$invoice_id, $source_key);
    }

    // Bulk path (optional – used by cron or “sync all” buttons)
    $software_tag = 'perfex:'.$source_key;

    if (method_exists($this, 'get_unsynced_ids')) {
        $ids = $this->get_unsynced_ids('invoice', $software_tag, 200);
    } else {
        // Fallback: sync all invoices (you can filter by date/status if needed)
        $ids = array_map(function($r){ return (int)$r['id']; },
            $this->db->select('id')->get(db_prefix().'invoices')->result_array()
        );
    }

    $ok = 0; $fail = 0; $lastErr = '';
    foreach ($ids as $iid) {
        $iid = is_array($iid) ? (int)($iid['id'] ?? 0) : (int)$iid;
        if (!$iid) { continue; }
        $res = $this->upsert_perfex_invoice($iid, $source_key);
        if ($res === true) { $ok++; } else { $fail++; $lastErr = (string)$res; }
    }
    return $fail ? "Synced {$ok}, failed {$fail}. Last error: {$lastErr}" : true;
}

public function create_perfex_invoice__OLD($invoice_id = '', $source_key = '')
{
    $software_tag = 'perfex:'.$source_key;

    // load invoice + client billing details
    $this->db->select(
        db_prefix().'invoices.*, '.
        db_prefix().'invoices.id as id, '.
        db_prefix().'invoices.number as number, '.
        db_prefix().'currencies.name as currency_name, '.
        db_prefix().'clients.company, '.
        db_prefix().'clients.billing_street, '.
        db_prefix().'clients.billing_city, '.
        db_prefix().'clients.billing_state, '.
        db_prefix().'clients.billing_zip, '.
        db_prefix().'clients.billing_country, '.
        db_prefix().'clients.shipping_street, '.
        db_prefix().'clients.shipping_city, '.
        db_prefix().'clients.shipping_state, '.
        db_prefix().'clients.shipping_zip, '.
        db_prefix().'clients.shipping_country'
    );
    if ($invoice_id !== '') {
        $this->db->where(db_prefix().'invoices.id', $invoice_id);
    }
    $this->db->join(db_prefix().'currencies', db_prefix().'currencies.id = '.db_prefix().'invoices.currency', 'left');
    $this->db->join(db_prefix().'clients', db_prefix().'clients.userid = '.db_prefix().'invoices.clientid', 'left');
    $this->db->join(db_prefix().'acc_integration_logs', 
        db_prefix().'acc_integration_logs.rel_id = '.db_prefix().'invoices.id'.
        ' AND '.db_prefix().'acc_integration_logs.rel_type = "invoice"'.
        ' AND '.db_prefix().'acc_integration_logs.software = '.$this->db->escape($software_tag),
    'left');

    $invoices = $this->db->get(db_prefix().'invoices')->result_array();

    foreach ($invoices as $inv) {
        $centralInvoiceId = (int)$inv['id'];

        // remote client id (must exist)
        $remoteClientId = $this->get_connect_id((int)$inv['clientid'], 'customer', $software_tag);
        if (!$remoteClientId) {
            $this->log_sync(false, $centralInvoiceId, 'invoice', $software_tag, 'sync_up', '', 'Missing remote customer id');
            if ($invoice_id !== '') return 'Missing remote customer id';
            continue;
        }

        // items
        $items = get_items_by_type('invoice', $centralInvoiceId);
        $newitems = [];
        foreach ($items as $it) {
            $newitems[] = $this->perfex_build_item_payload($it, $inv);
        }

        if (empty($newitems)) {
            // Perfex requires newitems
            $newitems[] = [
                'description' => 'Item',
                'long_description' => '',
                'qty' => 1,
                'rate' => (float)$inv['subtotal'], // fallback single line
            ];
        }

        // required fields
        //$remoteCurrencyId = $this->perfex_resolve_currency_id($source_key, $inv['currency_name'] ?? '');
        //$paymentModes     = $this->perfex_fetch_payment_mode_ids($source_key);

        // required billing street
        $billing_street = (string)($inv['billing_street'] ?? '');
        if ($billing_street === '') {
            // fallback to company address or "N/A" to satisfy API
            $billing_street = 'N/A';
        }

        // payload
        $payload = [
            'clientid'               => (int)$remoteClientId,
            'number'                 => (int)$inv['number'], // raw number, not formatted string
            'date'                   => $this->perfex_safe_date($inv['date']),
            'currency'               => (int)$inv['currency'],
            'newitems'               => $newitems,
            'subtotal'               => $inv['subtotal'],
            'total'                  => $inv['total'],
            'formatted_number'       => $inv['formatted_number'],
            'prefix'                 => $inv['prefix'],
            'billing_street'         => $billing_street,
            'allowed_payment_modes'  => $inv['allowed_payment_modes'],
            // Optional (pass when available)
            'billing_city'           => (string)($inv['billing_city'] ?? ''),
            'billing_state'          => (string)($inv['billing_state'] ?? ''),
            'billing_zip'            => (string)($inv['billing_zip'] ?? ''),
            'billing_country'        => (string)($inv['billing_country'] ?? ''),
            'include_shipping'       => (!empty($inv['shipping_street']) ? 'yes' : 'no'),
            'show_shipping_on_invoice'=> (!empty($inv['shipping_street']) ? 'yes' : 'no'),
            'shipping_street'        => (string)($inv['shipping_street'] ?? ''),
            'shipping_city'          => (string)($inv['shipping_city'] ?? ''),
            'shipping_state'         => (string)($inv['shipping_state'] ?? ''),
            'shipping_zip'           => (string)($inv['shipping_zip'] ?? ''),
            'shipping_country'       => (string)($inv['shipping_country'] ?? ''),
            'duedate'                => (!empty($inv['duedate']) ? $this->perfex_safe_date($inv['duedate']) : ''),
            'discount_type'          => (string)($inv['discount_type'] ?? ''), // 'before_tax'/'after_tax'
            'clientnote'             => (string)($inv['clientnote'] ?? ''),
            'terms'                  => (string)($inv['terms'] ?? ''),
            // You can add tags, sale_agent, etc. if you need them
        ];

        // remove empties
        foreach ($payload as $k=>$v) { if ($v === '' || $v === null || (is_array($v) && empty($v))) unset($payload[$k]); }

        // POST (create) — if you also want PUT (update) you can first probe by number
        $res = $this->perfex_call_api('POST','invoices', $payload, [], 'form', $source_key);

        // success shape: {"status":true,"message":"Invoice Added Successfully"}
        // try to get id; many builds don’t return it, so we search by number
        $rid = $this->perfex_extract_id($res);
        if (!$rid && is_array($res) && !empty($res['status'])) {
            // fetch by number (if your build supports query param)
            $search = $this->perfex_call_api('GET', 'invoices', null, ['number' => (int)$inv['number']], 'json', $source_key);
            if (is_array($search)) {
                foreach ($search as $ri) {
                    if ((int)($ri['number'] ?? 0) === (int)$inv['number'] && (int)($ri['clientid'] ?? 0) === (int)$remoteClientId) {
                        $rid = (string)($ri['id'] ?? '');
                        break;
                    }
                }
            }
        }

        if ($rid) {
            // save mapping if not exists
            if (!$this->get_connect_id($centralInvoiceId, 'invoice', $software_tag)) {
                $this->db->insert(db_prefix().'acc_integration_logs', [
                    'rel_id'      => $centralInvoiceId,
                    'rel_type'    => 'invoice',
                    'software'    => $software_tag,
                    'connect_id'  => $rid,
                    'date_updated'=> date('Y-m-d H:i:s'),
                ]);
            }

            $this->log_sync(true, $centralInvoiceId, 'invoice', $software_tag, 'sync_up', $rid);

            if ($invoice_id !== '') return true;
        } else {
            $body = is_array($res) ? json_encode($res) : (string)$res;
            $this->log_sync(false, $centralInvoiceId, 'invoice', $software_tag, 'sync_up', '', 'Invoice create failed: '.$body);
            if ($invoice_id !== '') return 'Invoice create failed: '.$body;
        }
    }
}

/** Invoices → remote Perfex (simplified: header only; add items if your REST exposes them) */


/** Payments → remote Perfex */
public function create_perfex_payment($payment_id = '', $source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    $this->db->select(db_prefix().'invoicepaymentrecords.*, '.db_prefix().'invoices.currency as inv_currency');
    if ($payment_id != '') {
        $this->db->where(db_prefix().'invoicepaymentrecords.id', $payment_id);
    }
    $this->db->join(db_prefix().'invoices', db_prefix().'invoices.id='.db_prefix().'invoicepaymentrecords.invoiceid', 'left');
    $this->db->join(db_prefix().'acc_integration_logs', db_prefix().'acc_integration_logs.rel_id=' . db_prefix().'invoicepaymentrecords.id AND '.db_prefix().'acc_integration_logs.rel_type="payment" AND software="'.$software_tag.'"', 'left');
    $rows = $this->db->get(db_prefix().'invoicepaymentrecords')->result_array();

    foreach ($rows as $row) {
        $remoteInv = $this->get_connect_id($row['invoiceid'], 'invoice', $software_tag);
        if (!$remoteInv) {
            // try send invoice first
            $r = $this->create_perfex_invoice($row['invoiceid'], $source_key);
            $remoteInv = $this->get_connect_id($row['invoiceid'], 'invoice', $software_tag);
            if (!$remoteInv) {
                $this->log_sync(false, $row['id'], 'payment', $software_tag, 'sync_up', '', 'Invoice not mapped remotely');
                if ($payment_id != '') { return 'Invoice not mapped remotely'; }
                continue;
            }
        }

        $payload = [
            'invoiceid'  => (int)$remoteInv,
            'amount'     => (float)$row['amount'],
            'date'       => $row['date'],
            'paymentmode'=> $row['paymentmode'],
            'note'       => $row['note'],
        ];

        $connect_id = $row['connect_id'] ?? $this->get_connect_id($row['id'], 'payment', $software_tag);
        $res = $connect_id
            ? $this->perfex_call_api('PUT', 'payments/'.$connect_id, $payload)
            : $this->perfex_call_api('POST', 'payments', $payload);

        if (isset($res['error'])) {
            $msg = is_array($res['body']) ? json_encode($res['body']) : (string)$res['body'];
            $this->log_sync(false, $row['id'], 'payment', $software_tag, 'sync_up', '', $msg);
            if ($payment_id != '') { return $msg; }
            continue;
        }

        $remoteId = $connect_id ?: ($res['id'] ?? null);
        if ($remoteId) {
            $this->upsert_connect_log($row['id'], 'payment', $software_tag, $remoteId);
            $this->log_sync(true, $row['id'], 'payment', $software_tag, 'sync_up', $remoteId);
            if ($payment_id != '') { return true; }
        } else {
            $this->log_sync(false, $row['id'], 'payment', $software_tag, 'sync_up', '', 'Missing remote id');
            if ($payment_id != '') { return 'Missing remote id'; }
        }
    }
}

/** Expenses → remote Perfex */
public function create_perfex_expense($expense_id = '', $source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    $this->db->select('*');
    if ($expense_id != '') {
        $this->db->where('id', $expense_id);
    }
    $this->db->join(db_prefix().'acc_integration_logs', db_prefix().'acc_integration_logs.rel_id=' . db_prefix().'expenses.id AND '.db_prefix().'acc_integration_logs.rel_type="expense" AND software="'.$software_tag.'"', 'left');
    $rows = $this->db->get(db_prefix().'expenses')->result_array();

    foreach ($rows as $row) {
        // Optional: link to customer if mapped
        $remoteClient = '';
        if (!empty($row['clientid'])) {
            $remoteClient = $this->get_connect_id($row['clientid'], 'customer', $software_tag);
        }

        $payload = [
            'category'    => (int)$row['category'],
            'amount'      => (float)$row['amount'],
            'date'        => $row['date'],
            'paymentmode' => $row['paymentmode'],
            'note'        => $row['note'],
            'clientid'    => $remoteClient ? (int)$remoteClient : null,
            'currency'    => (int)$row['currency'],
            'invoiceid'   => $row['invoiceid'] ?: null,
            'expense_name'=> $row['expense_name'],
        ];

        $connect_id = $row['connect_id'] ?? $this->get_connect_id($row['id'], 'expense', $software_tag);
        $res = $connect_id
            ? $this->perfex_call_api('PUT', 'expenses/'.$connect_id, $payload)
            : $this->perfex_call_api('POST', 'expenses', $payload);

        if (isset($res['error'])) {
            $msg = is_array($res['body']) ? json_encode($res['body']) : (string)$res['body'];
            $this->log_sync(false, $row['id'], 'expense', $software_tag, 'sync_up', '', $msg);
            if ($expense_id != '') { return $msg; }
            continue;
        }

        $remoteId = $connect_id ?: ($res['id'] ?? null);
        if ($remoteId) {
            $this->upsert_connect_log($row['id'], 'expense', $software_tag, $remoteId);
            $this->log_sync(true, $row['id'], 'expense', $software_tag, 'sync_up', $remoteId);
            if ($expense_id != '') { return true; }
        } else {
            $this->log_sync(false, $row['id'], 'expense', $software_tag, 'sync_up', '', 'Missing remote id');
            if ($expense_id != '') { return 'Missing remote id'; }
        }
    }
}

/* ======================= PULL (DOWN) SATELLITE → CENTRAL ======================= */
/* Note: these assume satellite exposes GET endpoints like /customers, /invoices, etc. */

public function get_perfex_customer($source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    // Basic pagination loop (adjust per your REST add-on)
    $page = 1; $per = 200; $count = 0;
    do {
        $rows = $this->perfex_call_api('GET', 'customers', null, ['page'=>$page,'per_page'=>$per]);
        if (!is_array($rows) || empty($rows)) break;

        foreach ($rows as $r) {
            // Upsert into central clients
            $data = [
                'company'          => $r['company'] ?? '',
                'vat'              => $r['vat'] ?? '',
                'phonenumber'      => $r['phonenumber'] ?? '',
                'website'          => $r['website'] ?? '',
                'address'          => $r['address'] ?? '',
                'city'             => $r['city'] ?? '',
                'state'            => $r['state'] ?? '',
                'zip'              => $r['zip'] ?? '',
                'country'          => isset($r['country']) ? (int)$r['country'] : 0,
                'billing_street'   => $r['billing_street'] ?? '',
                'billing_city'     => $r['billing_city'] ?? '',
                'billing_state'    => $r['billing_state'] ?? '',
                'billing_zip'      => $r['billing_zip'] ?? '',
                'billing_country'  => isset($r['billing_country']) ? (int)$r['billing_country'] : 0,
                'shipping_street'  => $r['shipping_street'] ?? '',
                'shipping_city'    => $r['shipping_city'] ?? '',
                'shipping_state'   => $r['shipping_state'] ?? '',
                'shipping_zip'     => $r['shipping_zip'] ?? '',
                'shipping_country' => isset($r['shipping_country']) ? (int)$r['shipping_country'] : 0,
                'default_currency' => isset($r['default_currency']) ? (int)$r['default_currency'] : 0,
                'active'           => isset($r['active']) ? (int)$r['active'] : 1,
            ];

            $remoteId = (string)($r['userid'] ?? $r['id']);
            // does central already have a mapping to this remote?
            $map = $this->db->where('rel_type','customer')
                            ->where('software',$software_tag)
                            ->where('connect_id',$remoteId)
                            ->get(db_prefix().'acc_integration_logs')->row();

            if ($map) {
                // update client
                $this->load->model('clients_model');
                $this->clients_model->update($data, $map->rel_id);
                $this->log_sync(true, $map->rel_id, 'customer', $software_tag, 'sync_down', $remoteId);
            } else {
                // create new client
                $this->load->model('clients_model');
                $cid = $this->clients_model->add($data);
                $this->upsert_connect_log($cid, 'customer', $software_tag, $remoteId);
                $this->log_sync(true, $cid, 'customer', $software_tag, 'sync_down', $remoteId);
            }
            $count++;
        }

        $page++;
    } while (is_array($rows) && count($rows) === $per);

    return $count;
}

public function get_perfex_invoice($source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    $page = 1; $per = 200;
    do {
        $rows = $this->perfex_call_api('GET', 'invoices', null, ['page'=>$page,'per_page'=>$per]);
        if (!is_array($rows) || empty($rows)) break;

        foreach ($rows as $r) {
            $remoteClient = (string)($r['clientid'] ?? $r['customer_id']);
            // find central client mapping
            $cmap = $this->db->where('rel_type','customer')
                             ->where('software',$software_tag)
                             ->where('connect_id',$remoteClient)
                             ->get(db_prefix().'acc_integration_logs')->row();
            if (!$cmap) { continue; }

            $payload = [
                'clientid'        => (int)$cmap->rel_id,
                'date'            => $r['date'] ?? date('Y-m-d'),
                'duedate'         => $r['duedate'] ?? null,
                'currency'        => isset($r['currency']) ? (int)$r['currency'] : get_base_currency()->id,
                'subtotal'        => (float)($r['subtotal'] ?? 0),
                'total'           => (float)($r['total'] ?? 0),
                'discount_type'   => $r['discount_type'] ?? '',
                'discount_percent'=> (float)($r['discount_percent'] ?? 0),
                'discount_total'  => (float)($r['discount_total'] ?? 0),
                'status'          => (int)($r['status'] ?? 1),
                'number'          => $r['number'] ?? null,
                'clientnote'      => $r['clientnote'] ?? '',
                'adminnote'       => $r['adminnote'] ?? '',
            ];

            $remoteId = (string)($r['id']);
            $imap = $this->db->where('rel_type','invoice')
                             ->where('software',$software_tag)
                             ->where('connect_id',$remoteId)
                             ->get(db_prefix().'acc_integration_logs')->row();

            $this->load->model('invoices_model');
            if ($imap) {
                $this->invoices_model->update($payload, $imap->rel_id);
                $this->log_sync(true, $imap->rel_id, 'invoice', $software_tag, 'sync_down', $remoteId);
            } else {
                $invId = $this->invoices_model->add($payload);
                $this->upsert_connect_log($invId, 'invoice', $software_tag, $remoteId);
                $this->log_sync(true, $invId, 'invoice', $software_tag, 'sync_down', $remoteId);
            }
        }

        $page++;
    } while (is_array($rows) && count($rows) === $per);

    return true;
}

public function get_perfex_payment($source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    $page = 1; $per = 200;
    do {
        $rows = $this->perfex_call_api('GET', 'payments', null, ['page'=>$page,'per_page'=>$per]);
        if (!is_array($rows) || empty($rows)) break;

        foreach ($rows as $r) {
            $remoteInv = (string)($r['invoiceid'] ?? $r['invoice_id']);
            $imap = $this->db->where('rel_type','invoice')
                             ->where('software',$software_tag)
                             ->where('connect_id',$remoteInv)
                             ->get(db_prefix().'acc_integration_logs')->row();
            if (!$imap) { continue; }

            $payload = [
                'invoiceid'  => (int)$imap->rel_id,
                'amount'     => (float)($r['amount'] ?? 0),
                'date'       => $r['date'] ?? date('Y-m-d'),
                'paymentmode'=> $r['paymentmode'] ?? null,
                'note'       => $r['note'] ?? '',
            ];

            $remoteId = (string)($r['id']);
            $pmap = $this->db->where('rel_type','payment')
                             ->where('software',$software_tag)
                             ->where('connect_id',$remoteId)
                             ->get(db_prefix().'acc_integration_logs')->row();

            $this->load->model('payments_model');
            if ($pmap) {
                $this->payments_model->update($payload, $pmap->rel_id);
                $this->log_sync(true, $pmap->rel_id, 'payment', $software_tag, 'sync_down', $remoteId);
            } else {
                $pid = $this->payments_model->add($payload);
                $this->upsert_connect_log($pid, 'payment', $software_tag, $remoteId);
                $this->log_sync(true, $pid, 'payment', $software_tag, 'sync_down', $remoteId);
            }
        }

        $page++;
    } while (is_array($rows) && count($rows) === $per);

    return true;
}

public function get_perfex_expense($source_key = '')
{
    $software_tag = 'perfex:' . $source_key;

    $page = 1; $per = 200;
    do {
        $rows = $this->perfex_call_api('GET', 'expenses', null, ['page'=>$page,'per_page'=>$per]);
        if (!is_array($rows) || empty($rows)) break;

        foreach ($rows as $r) {
            $payload = [
                'category'    => (int)($r['category'] ?? 0),
                'amount'      => (float)($r['amount'] ?? 0),
                'date'        => $r['date'] ?? date('Y-m-d'),
                'paymentmode' => $r['paymentmode'] ?? null,
                'note'        => $r['note'] ?? '',
                'currency'    => isset($r['currency']) ? (int)$r['currency'] : get_base_currency()->id,
                'invoiceid'   => $r['invoiceid'] ?? null,
                'expense_name'=> $r['expense_name'] ?? '',
                // Optional: map client by remote connect id if present
            ];

            $remoteId = (string)($r['id']);
            $emap = $this->db->where('rel_type','expense')
                             ->where('software',$software_tag)
                             ->where('connect_id',$remoteId)
                             ->get(db_prefix().'acc_integration_logs')->row();

            $this->load->model('expenses_model');
            if ($emap) {
                $this->expenses_model->update($payload, $emap->rel_id);
                $this->log_sync(true, $emap->rel_id, 'expense', $software_tag, 'sync_down', $remoteId);
            } else {
                $eid = $this->expenses_model->add($payload);
                $this->upsert_connect_log($eid, 'expense', $software_tag, $remoteId);
                $this->log_sync(true, $eid, 'expense', $software_tag, 'sync_down', $remoteId);
            }
        }

        $page++;
    } while (is_array($rows) && count($rows) === $per);

    return true;
}







// Returns arrays of local IDs for the given type with NO mapping in acc_integration_logs for this software tag
public function get_unsynced_ids($rel_type, $software_tag, $limit = 100)
{
    switch ($rel_type) {
        case 'customer':
            $this->db->select(db_prefix().'clients.userid as id');
            $this->db->from(db_prefix().'clients');
            $this->db->join(db_prefix().'acc_integration_logs l',
                'l.rel_id = '.db_prefix().'clients.userid AND l.rel_type="customer" AND l.software='.$this->db->escape($software_tag),
                'left');
            $this->db->where('l.connect_id IS NULL');
            $this->db->limit($limit);
            $rows = $this->db->get()->result_array();
            break;

        case 'invoice':
            $this->db->select(db_prefix().'invoices.id as id');
            $this->db->from(db_prefix().'invoices');
            $this->db->join(db_prefix().'acc_integration_logs l',
                'l.rel_id = '.db_prefix().'invoices.id AND l.rel_type="invoice" AND l.software='.$this->db->escape($software_tag),
                'left');
            $this->db->where('l.connect_id IS NULL');
            $this->db->limit($limit);
            $rows = $this->db->get()->result_array();
            break;

        case 'payment':
            $this->db->select(db_prefix().'invoicepaymentrecords.id as id');
            $this->db->from(db_prefix().'invoicepaymentrecords');
            $this->db->join(db_prefix().'acc_integration_logs l',
                'l.rel_id = '.db_prefix().'invoicepaymentrecords.id AND l.rel_type="payment" AND l.software='.$this->db->escape($software_tag),
                'left');
            $this->db->where('l.connect_id IS NULL');
            $this->db->limit($limit);
            $rows = $this->db->get()->result_array();
            break;

        case 'expense':
            $this->db->select(db_prefix().'expenses.id as id');
            $this->db->from(db_prefix().'expenses');
            $this->db->join(db_prefix().'acc_integration_logs l',
                'l.rel_id = '.db_prefix().'expenses.id AND l.rel_type="expense" AND l.software='.$this->db->escape($software_tag),
                'left');
            $this->db->where('l.connect_id IS NULL');
            $this->db->limit($limit);
            $rows = $this->db->get()->result_array();
            break;

        default:
            $rows = [];
    }

    return array_map(function($r){ return (int)$r['id']; }, $rows);
}

/**
 * Batch “sync up” for Perfex. Only pushes records that have no mapping yet.
 * Reuses your existing create_perfex_* methods (which already handle logging/mapping).
 */
public function cron_sync_up_perfex($source_key, $limitPerType = 100)
{
    $software_tag = 'perfex:'.$source_key;

    $types = ['customer','invoice','payment','expense']; // extend/trim as you like
    $summary = ['source'=>$source_key, 'software'=>$software_tag, 'synced'=>[]];

    foreach ($types as $t) {
        $ids = $this->get_unsynced_ids($t, $software_tag, $limitPerType);
        $ok = 0; $fail = 0;

        foreach ($ids as $id) {
            $fn = 'create_perfex_'.$t;
            try {
                $res = $this->$fn($id, $source_key);
                if ($res === true) $ok++; else $fail++;
            } catch (\Throwable $e) {
                $fail++;
                // optional: log to PHP error log
                log_message('error', "cron_sync_up_perfex {$t} {$id} failed: ".$e->getMessage());
            }
        }
        $summary['synced'][] = ['type'=>$t, 'ok'=>$ok, 'fail'=>$fail, 'attempted'=>count($ids)];
    }

    return $summary;
}






public function upsert_perfex_customer($customer_id, $source_key)
{
    $software_tag = 'perfex:'.$source_key;

    // Load local customer + primary contact (similar to your existing query)
    //$this->db->select(db_prefix().'clients.*, '.db_prefix().'clients.userid as userid, '.
                      //db_prefix().'contacts.firstname,'.db_prefix().'contacts.lastname,'.
                     // db_prefix().'contacts.email,'.db_prefix().'contacts.phonenumber');

    $this->db->select('*, '.db_prefix().'clients.userid as userid, '.db_prefix().'contacts.id as contact_id');

    $this->db->where(db_prefix().'clients.userid', $customer_id);
    $this->db->join(db_prefix().'contacts', db_prefix().'contacts.userid='.db_prefix().'clients.userid AND '.db_prefix().'contacts.is_primary=1','left');
    $row = $this->db->get(db_prefix().'clients')->row_array();
    if (!$row) return 'Customer not found';

    // Build remote payload (Perfex REST expects multipart-form)
  $payload = [
        'company'          => (string)$row['company'],
        'vat'              => (string)$row['vat'],
        'website'          => (string)$row['website'],
        'phonenumber'      => (string)$row['phonenumber'],
        'billing_street'   => (string)$row['billing_street'],
        'billing_city'     => (string)$row['billing_city'],
        'billing_state'    => (string)$row['billing_state'],
        'billing_zip'      => (string)$row['billing_zip'],
        'billing_country'  => (string)$row['billing_country'],
        'shipping_street'  => (string)$row['shipping_street'],
        'shipping_city'    => (string)$row['shipping_city'],
        'shipping_state'   => (string)$row['shipping_state'],
        'shipping_zip'     => (string)$row['shipping_zip'],
        'shipping_zip'     => (string)($row['shipping_zip'] ?? ''),
        'default_currency' => (string)($row['default_currency'] ?? ''),
        'active'           => (string)((int)($row['active'] ?? 1)),
        'default_language' => (string)($row['default_language'] ?? ''),
        'show_primary_contact' => (string)((int)($row['show_primary_contact'] ?? 0)),
    ];
    // drop empties
    foreach ($payload as $k=>$v) if ($v==='' || $v===null) unset($payload[$k]);

    $rid = $this->get_connect_id($customer_id, 'customer', $software_tag);
    $fp  = $this->acc_make_fp($payload);

    // Skip if unchanged and already mapped
    if ($rid && $this->acc_get_fp($software_tag,'customer',$customer_id) === $fp) {
        $this->log_sync(true, $customer_id, 'customer', $software_tag, 'noop', $rid);
        return true;
    }

    // Upsert
    // $res = $this->perfex_call_api($rid ? 'PUT' : 'POST',
    //     'customers'.($rid?'/'.$rid:''), $payload, [], 'form', $source_key);
    $res = $this->perfex_call_api(
    $rid ? 'PUT' : 'POST',
    'customers' . ($rid ? '/' . $rid : ''),
    $payload,
    [],
    $rid ? 'json' : 'form',   // <-- JSON for PUT, form for POST
    $source_key
);

    // Extract id or look it up (as you already do)
    $newId = $rid ?: $this->perfex_extract_id($res);
    if (!$newId && is_array($res) && !empty($res['status'])) {
        // fallback: try search by company if your build supports it
        $list = $this->perfex_call_api('GET','customers', null, ['company'=>$row['company']], 'json', $source_key);
        if (is_array($list)) foreach ($list as $c) {
            if (strcasecmp($c['company'] ?? '', $row['company'])===0) { $newId = (string)($c['userid'] ?? $c['id'] ?? ''); break; }
        }
    }

    if ($newId) {
        $this->upsert_connect_log($customer_id, 'customer', $software_tag, $newId);
        $this->acc_set_fp($software_tag,'customer',$customer_id,$fp);
        $this->log_sync(true, $customer_id, 'customer', $software_tag, $rid?'sync_up':'sync_up', $newId);

        // Also upsert primary contact to reflect changed email/phone
        $this->ensure_perfex_primary_contact($row, $newId, $software_tag, $source_key);
        // NEW: push custom fields
        $this->push_perfex_customer_custom_fields((int)$customer_id, (int)$newId, $source_key, $software_tag);

        return true;
    }

    $this->log_sync(false, $customer_id, 'customer', $software_tag, 'sync_up', '', 'Customer upsert failed: '.(is_array($res)?json_encode($res):$res));
    return 'Customer upsert failed';
}



public function upsert_perfex_invoice($invoice_id, $source_key)
{
    $software_tag = 'perfex:'.$source_key;

    // load invoice + client billing details
    $this->db->select(
        db_prefix().'invoices.*, '.
        db_prefix().'invoices.id as id, '.
        db_prefix().'invoices.number as number, '.
        db_prefix().'currencies.name as currency_name, '.
        db_prefix().'clients.company, '.
        db_prefix().'clients.billing_street, '.
        db_prefix().'clients.billing_city, '.
        db_prefix().'clients.billing_state, '.
        db_prefix().'clients.billing_zip, '.
        db_prefix().'clients.billing_country, '.
        db_prefix().'clients.shipping_street, '.
        db_prefix().'clients.shipping_city, '.
        db_prefix().'clients.shipping_state, '.
        db_prefix().'clients.shipping_zip, '.
        db_prefix().'clients.shipping_country'
    );
    if ($invoice_id !== '') {
        $this->db->where(db_prefix().'invoices.id', $invoice_id);
    }
    $this->db->join(db_prefix().'currencies', db_prefix().'currencies.id = '.db_prefix().'invoices.currency', 'left');
    $this->db->join(db_prefix().'clients', db_prefix().'clients.userid = '.db_prefix().'invoices.clientid', 'left');
    $this->db->join(db_prefix().'acc_integration_logs', 
        db_prefix().'acc_integration_logs.rel_id = '.db_prefix().'invoices.id'.
        ' AND '.db_prefix().'acc_integration_logs.rel_type = "invoice"'.
        ' AND '.db_prefix().'acc_integration_logs.software = '.$this->db->escape($software_tag),
    'left');

    $invoices = $this->db->get(db_prefix().'invoices')->result_array();

    foreach ($invoices as $inv) {
        $centralInvoiceId = (int)$inv['id'];

        // remote client id (must exist)
        $remoteClientId = $this->get_connect_id((int)$inv['clientid'], 'customer', $software_tag);
        //log_message('error', "UpdateInvoice:".json_encode($remoteClientId));
        if (!$remoteClientId) {
            //log_message('error', "UpdateInvoice:".json_encode($remoteClientId));
            $this->log_sync(false, $centralInvoiceId, 'invoice', $software_tag, 'sync_up', '', 'Missing remote customer id');
            if ($invoice_id !== '') return 'Missing remote customer id';
            continue;
        }

        // items
        $items = get_items_by_type('invoice', $centralInvoiceId);
        $newitems = [];
        foreach ($items as $it) {
            $newitems[] = $this->perfex_build_item_payload($it, $inv);
        }

        if (empty($newitems)) {
            // Perfex requires newitems
            $newitems[] = [
                'description' => 'Item',
                'long_description' => '',
                'qty' => 1,
                'rate' => (float)$inv['subtotal'], // fallback single line
            ];
        }

        // required fields
        //$remoteCurrencyId = $this->perfex_resolve_currency_id($source_key, $inv['currency_name'] ?? '');
        //$paymentModes     = $this->perfex_fetch_payment_mode_ids($source_key);

        // required billing street
        $billing_street = (string)($inv['billing_street'] ?? '');
        if ($billing_street === '') {
            // fallback to company address or "N/A" to satisfy API
            $billing_street = 'N/A';
        }

        // payload
        $core = [
            'clientid'               => (int)$remoteClientId,
            'number'                 => (int)$inv['number'], // raw number, not formatted string
            'date'                   => $this->perfex_safe_date($inv['date']),
            'currency'               => $inv['currency'],
            'newitems'               => $newitems,
            'subtotal'               => $inv['subtotal'],
            'total'                  => $inv['total'],
            'formatted_number'       => $inv['formatted_number'],
            'prefix'                 => $inv['prefix'],
            'billing_street'         => $billing_street,
            'allowed_payment_modes'  => $inv['allowed_payment_modes'],
          
            'billing_city'           => (string)($inv['billing_city'] ?? ''),
            'billing_state'          => (string)($inv['billing_state'] ?? ''),
            'billing_zip'            => (string)($inv['billing_zip'] ?? ''),
            'billing_country'        => (string)($inv['billing_country'] ?? ''),
            'include_shipping'       => (!empty($inv['shipping_street']) ? 'yes' : 'no'),
            'show_shipping_on_invoice'=> (!empty($inv['shipping_street']) ? 'yes' : 'no'),
            'shipping_street'        => (string)($inv['shipping_street'] ?? ''),
            'shipping_city'          => (string)($inv['shipping_city'] ?? ''),
            'shipping_state'         => (string)($inv['shipping_state'] ?? ''),
            'shipping_zip'           => (string)($inv['shipping_zip'] ?? ''),
            'shipping_country'       => (string)($inv['shipping_country'] ?? ''),
            'duedate'                => (!empty($inv['duedate']) ? $this->perfex_safe_date($inv['duedate']) : ''),
            'discount_type'          => (string)($inv['discount_type'] ?? ''), // 'before_tax'/'after_tax'
            'clientnote'             => (string)($inv['clientnote'] ?? ''),
            'terms'                  => (string)($inv['terms'] ?? ''),
            // You can add tags, sale_agent, etc. if you need them
        ];

        // remove empties
        foreach ($core as $k=>$v) { if ($v === '' || $v === null || (is_array($v) && empty($v))) unset($core[$k]); }

    $rid = $this->get_connect_id((int)$inv['id'],'invoice',$software_tag);
    $fp  = $this->acc_make_fp([$core, $newitems]);

    // No-op if unchanged
    if ($rid && $this->acc_get_fp($software_tag,'invoice',(int)$inv['id']) === $fp) {
        $this->log_sync(true, (int)$inv['id'], 'invoice', $software_tag, 'noop', $rid);
        //return true;
    }

    if ($rid) {
        // fetch remote to get current item ids so we can rewrite items in one PUT
        $remote = $this->perfex_call_api('GET','invoices/'.$rid, null, [], 'json', $source_key);
        //$remote = $this->perfex_call_api('GET', 'invoices/'.$rid, null, [], 'json', $source_key);
//log_message('error', "UpdateInvoice_remote:".json_encode($remote));
        $removed = [];
        // Remote may come back as array (assoc) or stdClass; normalize
    if (is_string($remote)) {
        $tmp = json_decode($remote, true);
        if (json_last_error() === JSON_ERROR_NONE) { $remote = $tmp; }
    }
       if (is_array($remote) && !empty($remote['items']) && is_array($remote['items'])) {
        foreach ($remote['items'] as $ri) {
            // remote returns: {"id":"57", ...}
            if (!empty($ri['id'])) {
                $removed[] = (string)$ri['id'];
            }
        }
    }

   // 2) Build update payload
    //    – We “replace” all items: remove existing ones, then add our newitems
    $payload = $core + ['newitems' => $newitems];
    if (!empty($removed)) {
        $payload['removed_items'] = $removed;
    }
        $res = $this->perfex_call_api('PUT', 'invoices/'.$rid, $payload, [],  'json', $source_key);
        $ok  = (is_array($res) && !empty($res['status'])) || $this->perfex_extract_id($res);

        if ($ok) {
            $this->acc_set_fp($software_tag,'invoice',(int)$inv['id'],$fp);
            $this->log_sync(true, (int)$inv['id'], 'invoice', $software_tag, 'sync_up', $rid);
            // After successful invoice upsert
$this->upsert_perfex_credit_notes_for_invoice((int)$inv['id'], $source_key, $software_tag);
$this->upsert_perfex_credit_links_for_invoice((int)$inv['id'], $source_key, $software_tag);

$this->upsert_perfex_debit_notes_for_invoice((int)$inv['id'], $source_key, $software_tag);
$this->upsert_perfex_debit_links_for_invoice((int)$inv['id'], $source_key, $software_tag);

            return true;
        }
    } else {
        $payload = $core + ['newitems'=>$newitems];
        $res = $this->perfex_call_api('POST','invoices', $payload, [], 'form', $source_key);
        $newId = $this->perfex_extract_id($res);
        if (!$newId && is_array($res) && !empty($res['status'])) {
            // fallback search by number
            $search = $this->perfex_call_api('GET','invoices', null, ['number'=>(int)$inv['number']], 'json', $source_key);
            if (is_array($search)) {
                foreach ($search as $ri) {
                    if ((int)($ri['number'] ?? 0) === (int)$inv['number'] && (int)($ri['clientid'] ?? 0) === (int)$remoteClientId) {
                        $newId = (string)($ri['id'] ?? ''); break;
                    }
                }
            }
        }
        if ($newId) {
            $this->upsert_connect_log((int)$inv['id'], 'invoice', $software_tag, $newId);
            $this->acc_set_fp($software_tag,'invoice',(int)$inv['id'],$fp);
            $this->log_sync(true, (int)$inv['id'], 'invoice', $software_tag, 'sync_up', $newId);
            // After successful invoice upsert
$this->upsert_perfex_credit_notes_for_invoice((int)$inv['id'], $source_key, $software_tag);
$this->upsert_perfex_credit_links_for_invoice((int)$inv['id'], $source_key, $software_tag);

$this->upsert_perfex_debit_notes_for_invoice((int)$inv['id'], $source_key, $software_tag);
$this->upsert_perfex_debit_links_for_invoice((int)$inv['id'], $source_key, $software_tag);

            return true;
        }
    }

    $this->log_sync(false, (int)$inv['id'], 'invoice', $software_tag, 'sync_up', '', 'Invoice upsert failed: '.(is_array($res)?json_encode($res):$res));
    return 'Invoice upsert failed';
}
}


// models/Sage_accounting_integration_model.php

public function cron_sync_updates_perfex(string $source_key, int $limit = 200)
{
    $software_tag = 'perfex:'.$source_key;

    // a) pick candidates since last run (24h fallback if never run)
    $last = (int) get_option('acc_integration_perfex_invoices_last_run');
    if (!$last) { $last = time() - 24*3600; }

    // If you have a reliable "updated" timestamp on invoices, filter by it here.
    // Perfex’ core invoices don’t have an updated_at by default, so we pick:
    //   - invoices that already have a mapping (so they can be updated)
    //   - order by id DESC as a decent proxy; cap with a limit
    $since = date('Y-m-d H:i:s', $last);
$mapped = $this->db->select('l.rel_id')
  ->from(db_prefix().'acc_integration_logs as l')
  ->join(db_prefix().'invoices as i', 'i.id = l.rel_id', 'inner')
  ->where('l.rel_type','invoice')
  ->where('l.software', $software_tag)
  ->where('i.updated_at >=', $since)      // your custom timestamp
  ->order_by('i.updated_at','DESC')
  ->limit($limit)->get()->result_array();

    $ok=0; $fail=0; $lastErr='';

    foreach ($mapped as $row) {
        $iid = (int)$row['rel_id'];
        // Reuse your upsert; it will compare fingerprints and do:
        // - PUT if changed
        // - NOOP if nothing changed
        //$res = $this->upsert_perfex_invoice($iid, $source_key);
        $res = $this->upsert_perfex_invoice($iid, $source_key);
$this->upsert_perfex_credit_notes_for_invoice($iid, $source_key, $software_tag);
$this->upsert_perfex_credit_links_for_invoice($iid, $source_key, $software_tag);

        if ($res === true) { $ok++; } else { $fail++; $lastErr = (string)$res; }
    }

    update_option('acc_integration_perfex_invoices_last_run', time());

    return [
        'source'    => $source_key,
        'software'  => $software_tag,
        'synced'    => [['type'=>'invoice','ok'=>$ok,'fail'=>$fail,'attempted'=>($ok+$fail)]],
    ];
}









/**
 * Sync all CREDIT NOTES that are linked (applied) to a central invoice.
 * - Reads tblcredits (link -> amount, dates) + tblcreditnotes (header).
 * - Upserts remote credit note via REST: POST /api/credit_notes or PUT /api/credit_notes/{id}
 * - Stores mapping under rel_type = 'credit_note'.
 * - Does NOT “apply” the credit to the remote invoice (only the note itself) because the REST spec you shared
 *   only covers creating the credit note. If you later expose an “apply” endpoint, we can add it.
 */
public function upsert_perfex_credit_notes_for_invoice(int $invoice_id, string $source_key, string $software_tag)
{
    // Pull credits applied to this invoice + their credit note headers
    $this->db->select('c.id as link_id, c.credit_id, c.amount, c.date as link_date, c.date_applied, '.
                      'cn.*, cn.id as cn_id, cn.number as cn_number, cn.currency as cn_currency, cn.clientid as cn_clientid');
    $this->db->from(db_prefix().'credits as c');
    $this->db->join(db_prefix().'creditnotes as cn', 'cn.id = c.credit_id', 'inner');
    $this->db->where('c.invoice_id', $invoice_id);
    $rows = $this->db->get()->result_array();

    if (empty($rows)) { return true; }

    foreach ($rows as $row) {
        $creditId  = (int)$row['cn_id'];
        $clientId  = (int)$row['cn_clientid'];

        // Must have remote customer mapping
        $remoteClientId = $this->get_connect_id($clientId, 'customer', $software_tag);
        if (!$remoteClientId) {
            $this->log_sync(false, $creditId, 'credit_note', $software_tag, 'sync_up', '', 'Missing remote customer id for credit note');
            continue;
        }

        // Build newitems from local credit note items
        $items = function_exists('get_items_by_type')
            ? get_items_by_type('credit_note', $creditId)
            : [];
        $newitems = [];
        foreach ($items as $it) {
            // Re-use your existing item payload builder you use for invoices
            $newitems[] = $this->perfex_build_item_payload($it, $row);
        }
        if (empty($newitems)) {
            // The API requires newitems; fall back to one line if there are no items
            $newitems[] = [
                'description'       => 'Credit',
                'long_description'  => '',
                'qty'               => 1,
                'rate'              => (float)$row['subtotal'],
            ];
        }

        // Required fields per your doc
        $payload = [
            'clientid'   => (int)$remoteClientId,
            'date'       => $this->perfex_safe_date($row['date']),
            'number'     => (int)$row['cn_number'],
            'currency'   => (int)$row['cn_currency'],
            'newitems'   => $newitems,
            'subtotal'   => $row['subtotal'],
            'total'      => $row['total'],

            // Optional – pass when non-empty
            'billing_street'   => (string)($row['billing_street'] ?? ''),
            'billing_city'     => (string)($row['billing_city'] ?? ''),
            'billing_state'    => (string)($row['billing_state'] ?? ''),
            'billing_zip'      => (string)($row['billing_zip'] ?? ''),
            'billing_country'  => (string)($row['billing_country'] ?? ''),
            'shipping_street'  => (string)($row['shipping_street'] ?? ''),
            'shipping_city'    => (string)($row['shipping_city'] ?? ''),
            'shipping_state'   => (string)($row['shipping_state'] ?? ''),
            'shipping_zip'     => (string)($row['shipping_zip'] ?? ''),
            'shipping_country' => (string)($row['shipping_country'] ?? ''),
            'discount_type'    => (string)($row['discount_type'] ?? ''),
            'adminnote'        => (string)($row['adminnote'] ?? ''),
            'clientnote'       => (string)($row['clientnote'] ?? ''),
            'terms'            => (string)($row['terms'] ?? ''),
        ];
        foreach ($payload as $k=>$v) { if ($v === '' || $v === null || (is_array($v) && empty($v))) unset($payload[$k]); }

        // Upsert by mapping
        $rid = $this->get_connect_id($creditId, 'credit_note', $software_tag);

        // Optional: a simple fingerprint to skip unchanged updates
        $fp = $this->acc_make_fp([$payload]);
        if ($rid && $this->acc_get_fp($software_tag, 'credit_note', $creditId) === $fp) {
            $this->log_sync(true, $creditId, 'credit_note', $software_tag, 'noop', $rid);
            continue;
        }

        if ($rid) {
            $res = $this->perfex_call_api('PUT', 'credit_notes/'.$rid, $payload, [], 'form', $source_key);
            $ok  = (is_array($res) && !empty($res['status'])) || $this->perfex_extract_id($res);

            if ($ok) {
                $this->acc_set_fp($software_tag, 'credit_note', $creditId, $fp);
                $this->log_sync(true, $creditId, 'credit_note', $software_tag, 'sync_up', $rid);
            } else {
                $this->log_sync(false, $creditId, 'credit_note', $software_tag, 'sync_up', '', 'Credit note update failed: '.(is_array($res)?json_encode($res):$res));
            }
        } else {
            $res   = $this->perfex_call_api('POST', 'credit_notes', $payload, [], 'form', $source_key);
            $newId = $this->perfex_extract_id($res);

            // If your REST returns only {status:true,message:"..."} we can optionally search by number:
            if (!$newId && is_array($res) && !empty($res['status'])) {
                $search = $this->perfex_call_api('GET', 'credit_notes', null, ['number'=>(int)$row['cn_number']], 'json', $source_key);
                if (is_array($search)) {
                    foreach ($search as $ri) {
                        if ((int)($ri['number'] ?? 0) === (int)$row['cn_number']
                            && (int)($ri['clientid'] ?? 0) === (int)$remoteClientId) {
                            $newId = (string)($ri['id'] ?? ''); break;
                        }
                    }
                }
            }

            if ($newId) {
                $this->upsert_connect_log($creditId, 'credit_note', $software_tag, $newId);
                $this->acc_set_fp($software_tag, 'credit_note', $creditId, $fp);
                $this->log_sync(true, $creditId, 'credit_note', $software_tag, 'sync_up', $newId);
            } else {
                $this->log_sync(false, $creditId, 'credit_note', $software_tag, 'sync_up', '', 'Credit note create failed: '.(is_array($res)?json_encode($res):$res));
            }
        }
    }

    return true;
}


public function upsert_perfex_credit_links_for_invoice(int $invoice_id, string $source_key, string $software_tag)
{
    // Pull local credits entries linked to this invoice
    $this->db->select('c.*, c.id as link_id, c.invoice_id, c.credit_id, c.amount, c.date, c.date_applied, '.
                      'cn.id as cn_id, cn.number as cn_number, cn.clientid as cn_clientid');
    $this->db->from(db_prefix().'credits as c');
    $this->db->join(db_prefix().'creditnotes as cn', 'cn.id = c.credit_id', 'inner');
    $this->db->where('c.invoice_id', $invoice_id);
    $rows = $this->db->get()->result_array();

    if (empty($rows)) { return true; }

    foreach ($rows as $row) {
        $localLinkId    = (int)$row['link_id'];     // tblcredits.id (local link)
        $localInvId     = (int)$row['invoice_id'];  // local invoice id
        $localCredNote  = (int)$row['credit_id'];   // local creditnote id

        // Resolve remote ids
        $remoteInvoiceId    = $this->get_connect_id($localInvId,    'invoice',     $software_tag);
        $remoteCreditNoteId = $this->get_connect_id($localCredNote, 'credit_note', $software_tag);

        if (!$remoteInvoiceId || !$remoteCreditNoteId) {
            $this->log_sync(false, $localLinkId, 'credit_link', $software_tag, 'sync_up', '',
                'Missing remote mapping (invoice or credit_note)');
            continue;
        }

        // Prepare payload for /api/credits (multipart form)
        $payload = [
            'invoice_id'   => (int)$remoteInvoiceId,
            'credit_id'    => (int)$remoteCreditNoteId,
            'amount'       => $row['amount'],
            'date'         => $row['date'],                 // Y-m-d
            'date_applied' => $row['date_applied'] ?? ''// Y-m-d H:i:s or blank
        ];
        foreach ($payload as $k=>$v) { if ($v === '' || $v === null) unset($payload[$k]); }

        // Fingerprint to avoid re-applying the same link
        $fp = $this->acc_make_fp([$payload]);
        if ($this->acc_get_fp($software_tag, 'credit_link', $localLinkId) === $fp) {
            $this->log_sync(true, $localLinkId, 'credit_link', $software_tag, 'noop', '');
            continue;
        }

        // POST /api/credits (Option A)
        $res   = $this->perfex_call_api('POST', 'credits', $payload, [], 'form', $source_key);

        //log_message('error', "CreditNote:".json_encode($res));

        // Success patterns to accept:
        //  - {"status":true,"message":"Credit applied successfully","id":123}
        //  - {"status":true,"message":"Credit link already exists"}
        //  - or any 200 with top-level id
        $ok    = false;
        $newId = $this->perfex_extract_id($res);

        if (is_array($res) && !empty($res['status'])) {
            $ok = true;
        } elseif ($newId) {
            $ok = true;
        }

        if ($ok) {
            if ($newId) {
                // Save mapping for the *link* too (optional but nice to have)
                $this->upsert_connect_log($localLinkId, 'credit_link', $software_tag, (string)$newId);
            }
            $this->acc_set_fp($software_tag, 'credit_link', $localLinkId, $fp);
            $this->log_sync(true, $localLinkId, 'credit_link', $software_tag, 'sync_up', (string)($newId ?? ''));
        } else {
            $body = is_array($res) ? json_encode($res) : (string)$res;
            $this->log_sync(false, $localLinkId, 'credit_link', $software_tag, 'sync_up', '', 'Credit link create failed: '.$body);
        }
    }

    return true;
}


/**
 * Sync all DEBIT NOTES that are linked (applied) to a central invoice.
 * - Reads tbldebits (link) + tbldebitnotes (header).
 * - Upserts remote debit note via REST: POST /api/debit_notes or PUT /api/debit_notes/{id}
 * - Stores mapping under rel_type = 'debit_note'.
 */
public function upsert_perfex_debit_notes_for_invoice(int $invoice_id, string $source_key, string $software_tag)
{
    // Pull debit applications linked to this invoice + their debit note headers
    $this->db->select('d.id as link_id, d.debit_id, d.amount, d.date as link_date, d.date_applied, '.
                      'dn.*, dn.id as dn_id, dn.number as dn_number, dn.currency as dn_currency, dn.clientid as dn_clientid');
    $this->db->from(db_prefix().'debits as d');
    $this->db->join(db_prefix().'debitnotes as dn', 'dn.id = d.debit_id', 'inner');
    $this->db->where('d.invoice_id', $invoice_id);
    $rows = $this->db->get()->result_array();

    if (empty($rows)) { return true; }

    foreach ($rows as $row) {
        $debitId = (int)$row['dn_id'];
        $clientId = (int)$row['dn_clientid'];

        // Must have remote customer mapping
        $remoteClientId = $this->get_connect_id($clientId, 'customer', $software_tag);
        if (!$remoteClientId) {
            $this->log_sync(false, $debitId, 'debit_note', $software_tag, 'sync_up', '', 'Missing remote customer id for debit note');
            continue;
        }

        // Build newitems from local debit note items
        $items = function_exists('get_items_by_type') ? get_items_by_type('debit_note', $debitId) : [];
        $newitems = [];
        foreach ($items as $it) {
            $newitems[] = $this->perfex_build_item_payload($it, $row);
        }
        if (empty($newitems)) {
            $newitems[] = [
                'description'       => 'Debit',
                'long_description'  => '',
                'qty'               => 1,
                'rate'              => (float)$row['subtotal'],
            ];
        }

        // Payload (same pattern as credit notes)
        $payload = [
            'clientid'  => (int)$remoteClientId,
            'date'      => $row['date'],
            'number'    => (int)$row['dn_number'],
            'currency'  => (int)$row['dn_currency'],
            'newitems'  => $newitems,
            'subtotal'  => $row['subtotal'],
            'total'     => $row['total'],

            'billing_street'   => (string)($row['billing_street'] ?? ''),
            'billing_city'     => (string)($row['billing_city'] ?? ''),
            'billing_state'    => (string)($row['billing_state'] ?? ''),
            'billing_zip'      => (string)($row['billing_zip'] ?? ''),
            'billing_country'  => (string)($row['billing_country'] ?? ''),
            'shipping_street'  => (string)($row['shipping_street'] ?? ''),
            'shipping_city'    => (string)($row['shipping_city'] ?? ''),
            'shipping_state'   => (string)($row['shipping_state'] ?? ''),
            'shipping_zip'     => (string)($row['shipping_zip'] ?? ''),
            'shipping_country' => (string)($row['shipping_country'] ?? ''),
            'discount_type'    => (string)($row['discount_type'] ?? ''),
            'adminnote'        => (string)($row['adminnote'] ?? ''),
            'clientnote'       => (string)($row['clientnote'] ?? ''),
            'terms'            => (string)($row['terms'] ?? ''),
        ];
        foreach ($payload as $k=>$v) { if ($v==='' || $v===null || (is_array($v)&&empty($v))) unset($payload[$k]); }

        $rid = $this->get_connect_id($debitId, 'debit_note', $software_tag);
        $fp  = $this->acc_make_fp([$payload]);

        if ($rid && $this->acc_get_fp($software_tag,'debit_note',$debitId)===$fp) {
            $this->log_sync(true, $debitId, 'debit_note', $software_tag, 'noop', $rid);
            continue;
        }

        if ($rid) {
            $res = $this->perfex_call_api('PUT', 'debit_notes/'.$rid, $payload, [], 'form', $source_key);
            $ok  = (is_array($res) && !empty($res['status'])) || $this->perfex_extract_id($res);
            if ($ok) {
                $this->acc_set_fp($software_tag,'debit_note',$debitId,$fp);
                $this->log_sync(true, $debitId, 'debit_note', $software_tag, 'sync_up', $rid);
            } else {
                $this->log_sync(false, $debitId, 'debit_note', $software_tag, 'sync_up', '', 'Debit note update failed: '.(is_array($res)?json_encode($res):$res));
            }
        } else {
            $res   = $this->perfex_call_api('POST','debit_notes', $payload, [], 'form', $source_key);
            $newId = $this->perfex_extract_id($res);

            if (!$newId && is_array($res) && !empty($res['status'])) {
                $search = $this->perfex_call_api('GET','debit_notes', null, ['number'=>(int)$row['dn_number']], 'json', $source_key);
                if (is_array($search)) {
                    foreach ($search as $ri) {
                        if ((int)($ri['number'] ?? 0)===(int)$row['dn_number']
                            && (int)($ri['clientid'] ?? 0)===(int)$remoteClientId) {
                            $newId = (string)($ri['id'] ?? ''); break;
                        }
                    }
                }
            }

            if ($newId) {
                $this->upsert_connect_log($debitId, 'debit_note', $software_tag, $newId);
                $this->acc_set_fp($software_tag,'debit_note',$debitId,$fp);
                $this->log_sync(true, $debitId, 'debit_note', $software_tag, 'sync_up', $newId);
            } else {
                $this->log_sync(false, $debitId, 'debit_note', $software_tag, 'sync_up', '', 'Debit note create failed: '.(is_array($res)?json_encode($res):$res));
            }
        }
    }
    return true;
}
public function upsert_perfex_debit_links_for_invoice(int $invoice_id, string $source_key, string $software_tag)
{
    // Pull local debit entries linked to this invoice
    $this->db->select('d.*, d.id as link_id, d.invoice_id, d.debit_id, d.amount, d.date, d.date_applied, '.
                      'dn.id as dn_id, dn.number as dn_number, dn.clientid as dn_clientid');
    $this->db->from(db_prefix().'debits as d');
    $this->db->join(db_prefix().'debitnotes as dn', 'dn.id = d.debit_id', 'inner');
    $this->db->where('d.invoice_id', $invoice_id);
    $rows = $this->db->get()->result_array();

    if (empty($rows)) { return true; }

    foreach ($rows as $row) {
        $localLinkId  = (int)$row['link_id'];      // tbldebits.id (local)
        $localInvId   = (int)$row['invoice_id'];   // local invoice id
        $localDebitId = (int)$row['debit_id'];     // local debit note id

        // Resolve remote ids
        $remoteInvoiceId  = $this->get_connect_id($localInvId,   'invoice',    $software_tag);
        $remoteDebitNoteId= $this->get_connect_id($localDebitId, 'debit_note', $software_tag);

        if (!$remoteInvoiceId || !$remoteDebitNoteId) {
            $this->log_sync(false, $localLinkId, 'debit_link', $software_tag, 'sync_up', '',
                'Missing remote mapping (invoice or debit_note)');
            continue;
        }

        // Build payload for /api/debits
        $payload = [
            'invoice_id'   => (int)$remoteInvoiceId,
            'debit_id'     => (int)$remoteDebitNoteId,
            'amount'       => $row['amount'],
            'date'         => $row['date'],
            'date_applied' => $row['date_applied'] ?? '',
        ];
        foreach ($payload as $k=>$v) { if ($v==='' || $v===null) unset($payload[$k]); }

        // Fingerprint to avoid re-applying same link
        $fp = $this->acc_make_fp([$payload]);
        if ($this->acc_get_fp($software_tag, 'debit_link', $localLinkId) === $fp) {
            $this->log_sync(true, $localLinkId, 'debit_link', $software_tag, 'noop', '');
            continue;
        }

        $res   = $this->perfex_call_api('POST', 'debits', $payload, [], 'form', $source_key);
        $ok    = false;
        $newId = $this->perfex_extract_id($res);

        if (is_array($res) && !empty($res['status'])) {
            $ok = true;
        } elseif ($newId) {
            $ok = true;
        }

        if ($ok) {
            if ($newId) {
                $this->upsert_connect_log($localLinkId, 'debit_link', $software_tag, (string)$newId);
            }
            $this->acc_set_fp($software_tag, 'debit_link', $localLinkId, $fp);
            $this->log_sync(true, $localLinkId, 'debit_link', $software_tag, 'sync_up', (string)($newId ?? ''));
        } else {
            $body = is_array($res) ? json_encode($res) : (string)$res;
            $this->log_sync(false, $localLinkId, 'debit_link', $software_tag, 'sync_up', '', 'Debit link create failed: '.$body);
        }
    }

    return true;
}




//tblacc_integration_fingerprints

private function fp_table() { return db_prefix().'acc_integration_fingerprints'; }

private function acc_fp_key($software, $rel_type, $rel_id) {
    // kept for backward-compat only (old tbloptions key format)
    return 'acc_fp_'.$software.'_'.$rel_type.'_'.$rel_id;
}

private function acc_get_fp($software, $rel_type, $rel_id) {
    // 1) try new table
    $row = $this->db
        ->select('fp')
        ->where('software', $software)
        ->where('rel_type', $rel_type)
        ->where('rel_id', (int)$rel_id)
        ->get($this->fp_table())
        ->row_array();
    if (!empty($row['fp'])) {
        return (string)$row['fp'];
    }
    // 2) fallback to old tbloptions once (for installations upgrading)
    $legacy = (string) get_option($this->acc_fp_key($software,$rel_type,$rel_id)) ?: '';
    if ($legacy !== '') {
        // up-migrate silently
        $this->acc_set_fp($software, $rel_type, $rel_id, $legacy);
    }
    return $legacy;
}

private function acc_set_fp($software, $rel_type, $rel_id, $fp) {
    $data = [
        'software'   => (string)$software,
        'rel_type'   => (string)$rel_type,
        'rel_id'     => (int)$rel_id,
        'fp'         => (string)$fp,
        'updated_at' => date('Y-m-d H:i:s'),
    ];
    // upsert
    $exists = $this->db->select('id')
        ->where('software',$data['software'])
        ->where('rel_type',$data['rel_type'])
        ->where('rel_id',$data['rel_id'])
        ->get($this->fp_table())
        ->row_array();

    if ($exists) {
        $this->db->where('id', (int)$exists['id'])->update($this->fp_table(), $data);
    } else {
        $this->db->insert($this->fp_table(), $data);
    }
}

private function acc_make_fp($payload) {
    $norm = function (&$v) use (&$norm) {
        if (is_array($v)) { ksort($v); foreach ($v as &$vv) $norm($vv); }
    };
    $copy = $payload; $norm($copy);
    return sha1(json_encode($copy));
}

// private function perfex_decimal_2($val) {
//     return number_format((float)$val, 2, '.', '');
// }



}
