Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 2555
0.00% covered (danger)
0.00%
0 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
zbsDAL_contacts
0.00% covered (danger)
0.00%
0 / 2555
0.00% covered (danger)
0.00%
0 / 53
453602
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 292
0.00% covered (danger)
0.00%
0 / 1
56
 add_listview_filters
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 getSingle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIDList
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getAll
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getFullCount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalExtSourceCount
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getContact
0.00% covered (danger)
0.00%
0 / 232
0.00% covered (danger)
0.00%
0 / 1
3540
 getContacts
0.00% covered (danger)
0.00%
0 / 611
0.00% covered (danger)
0.00%
0 / 1
35532
 addUpdateContact
0.00% covered (danger)
0.00%
0 / 483
0.00% covered (danger)
0.00%
0 / 1
20306
 addUpdateContactTags
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 addUpdateContactCompanies
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
110
 addUpdateContactWPID
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
 deleteContact
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 1
272
 tidy_contact
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
600
 db_ready_contact
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
20
 getContactMeta
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getExternalSourcesForContact
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
240
 getTrackingForContact
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
272
 getContactOwner
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getContactStatus
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 setContactStatus
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 setContactOwner
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getContactEmail
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 update_contact_email
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getContactEmails
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 getContactMobile
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getContactFullName
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 getContactFullNameEtc
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 getContactNameWithFallback
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 getContactAddress
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 getContact2ndAddress
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 getContactTags
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getContactLastContactUTS
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 setContactLastContactUTS
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getContactSocials
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
6
 getContactWPID
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getContactDoNotMail
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setContactDoNotMail
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getContactAvatarURL
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getContactAvatar
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 getContactAvatarHTML
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 getContactCount
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 getContactCompanies
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 contactHasQuote
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 contactHasInvoice
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 contactHasTransaction
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 contactHasObjLink
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getContactPrevNext
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 listViewObj
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
506
 format_fullname
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 format_name_etc
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
132
 format_name_with_fallback
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3/*
4 * Jetpack CRM
5 * https://jetpackcrm.com
6 * V3.0+
7 *
8 * Copyright 2020 Automattic
9 *
10 * Date: 14/01/19
11 */
12
13defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
14
15use Automattic\Jetpack\CRM\Event_Manager\Events_Manager;
16
17/**
18 * ZBS DAL >> Contacts
19 *
20 * @author   Woody Hayday <hello@jetpackcrm.com>
21 * @version  2.0
22 * @access   public
23 * @see      https://jetpackcrm.com/kb
24 */
25class zbsDAL_contacts extends zbsDAL_ObjectLayer {
26
27    protected $objectType              = ZBS_TYPE_CONTACT;
28    protected $objectDBPrefix          = 'zbsc_';
29    protected $objectIncludesAddresses = true;
30    protected $include_in_templating   = true;
31    // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase, Squiz.Commenting.VariableComment.Missing -- to be refactored.
32    protected $objectModel = array();
33
34    /** @var Events_Manager To manage the CRM events */
35    private $events_manager;
36
37    // hardtyped list of types this object type is commonly linked to
38    protected $linkedToObjectTypes = array(
39
40        ZBS_TYPE_COMPANY,
41
42    );
43
44    // phpcs:ignore Squiz.Commenting.FunctionComment.Missing, Squiz.Scope.MethodScope.Missing -- to be refactored.
45    function __construct( $args = array() ) {
46        // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- to be refactored.
47        $this->objectModel = array(
48            // ID
49            'ID'            => array(
50                'fieldname' => 'ID',
51                'format'    => 'int',
52            ),
53            // site + team generics
54            'zbs_site'      => array(
55                'fieldname' => 'zbs_site',
56                'format'    => 'int',
57            ),
58            'zbs_team'      => array(
59                'fieldname' => 'zbs_team',
60                'format'    => 'int',
61            ),
62            'zbs_owner'     => array(
63                'fieldname' => 'zbs_owner',
64                'format'    => 'int',
65            ),
66            // other fields
67            'status'        => array(
68                // db model:
69                'fieldname'             => 'zbsc_status',
70                'format'                => 'str',
71                // output model
72                'input_type'            => 'select',
73                'label'                 => __( 'Status', 'zero-bs-crm' ),
74                'placeholder'           => '',
75                'options'               => array( 'Lead', 'Customer', 'Refused' ),
76                'essential'             => true,
77                'max_len'               => 100,
78                'do_not_show_on_portal' => true,
79            ),
80            'email'         => array(
81                // db model:
82                'fieldname'             => 'zbsc_email',
83                'format'                => 'str',
84                // output model
85                'input_type'            => 'email',
86                'label'                 => __( 'Email', 'zero-bs-crm' ),
87                'placeholder'           => 'e.g. john@gmail.com',
88                'essential'             => true,
89                'force_unique'          => true, // must be unique. This is required and breaking if true
90                'can_be_blank'          => true,
91                'max_len'               => 200,
92                // removed due to some users using mobile/other as unique field? see #gh-153
93                // 'not_empty' => true,
94                'do_not_show_on_portal' => true,
95            ),
96            'prefix'        => array(
97                // db model:
98                'fieldname'   => 'zbsc_prefix',
99                'format'      => 'str',
100                // output model
101                'input_type'  => 'select',
102                'label'       => __( 'Prefix', 'zero-bs-crm' ),
103                'placeholder' => '',
104                'options'     => array( 'Mr', 'Mrs', 'Ms', 'Miss', 'Mx', 'Dr', 'Prof', 'Mr & Mrs' ),
105                'essential'   => true,
106                'max_len'     => 30,
107            ),
108            'fname'         => array(
109                // db model:
110                'fieldname'   => 'zbsc_fname',
111                'format'      => 'str',
112                // output model
113                'input_type'  => 'text',
114                'label'       => __( 'First Name', 'zero-bs-crm' ),
115                'placeholder' => 'e.g. John',
116                'essential'   => true,
117                'max_len'     => 100,
118            ),
119            'lname'         => array(
120                // db model:
121                'fieldname'   => 'zbsc_lname',
122                'format'      => 'str',
123                // output model
124                'input_type'  => 'text',
125                'label'       => __( 'Last Name', 'zero-bs-crm' ),
126                'placeholder' => 'e.g. Doe',
127                'essential'   => true,
128                'max_len'     => 100,
129            ),
130
131            'addr1'         => array(
132                // db model:
133                'fieldname'   => 'zbsc_addr1',
134                'format'      => 'str',
135                // output model
136                'input_type'  => 'text',
137                'label'       => __( 'Address Line 1', 'zero-bs-crm' ),
138                'placeholder' => '',
139                'area'        => 'Main Address',
140                'migrate'     => 'addresses',
141                'max_len'     => 200,
142            ),
143            'addr2'         => array(
144                // db model:
145                'fieldname'   => 'zbsc_addr2',
146                'format'      => 'str',
147                // output model
148                'input_type'  => 'text',
149                'label'       => __( 'Address Line 2', 'zero-bs-crm' ),
150                'placeholder' => '',
151                'area'        => 'Main Address',
152                'migrate'     => 'addresses',
153                'max_len'     => 200,
154            ),
155            'city'          => array(
156                // db model:
157                'fieldname'   => 'zbsc_city',
158                'format'      => 'str',
159                // output model
160                'input_type'  => 'text',
161                'label'       => __( 'City', 'zero-bs-crm' ),
162                'placeholder' => 'e.g. New York',
163                'area'        => 'Main Address',
164                'migrate'     => 'addresses',
165                'max_len'     => 200,
166            ),
167            'county'        => array(
168                // db model:
169                'fieldname'   => 'zbsc_county',
170                'format'      => 'str',
171                // output model
172                'input_type'  => 'text',
173                'label'       => __( 'County', 'zero-bs-crm' ),
174                'placeholder' => 'e.g. Kings County',
175                'area'        => 'Main Address',
176                'migrate'     => 'addresses',
177                'max_len'     => 200,
178            ),
179            'postcode'      => array(
180                // db model:
181                'fieldname'   => 'zbsc_postcode',
182                'format'      => 'str',
183                // output model
184                'input_type'  => 'text',
185                'label'       => __( 'Post Code', 'zero-bs-crm' ),
186                'placeholder' => 'e.g. 10019',
187                'area'        => 'Main Address',
188                'migrate'     => 'addresses',
189                'max_len'     => 50,
190            ),
191            'country'       => array(
192                // db model:
193                'fieldname'   => 'zbsc_country',
194                'format'      => 'str',
195                // output model
196                'input_type'  => 'selectcountry',
197                'label'       => __( 'Country', 'zero-bs-crm' ),
198                'placeholder' => '',
199                'area'        => 'Main Address',
200                'migrate'     => 'addresses',
201                'max_len'     => 200,
202            ),
203            'secaddr1'      => array(
204                // db model:
205                'fieldname'   => 'zbsc_addr1',
206                'format'      => 'str',
207                // output model
208                'input_type'  => 'text',
209                'label'       => __( 'Address Line 1', 'zero-bs-crm' ),
210                'placeholder' => '',
211                'area'        => 'Second Address',
212                'migrate'     => 'addresses',
213                'opt'         => 'secondaddress',
214                'max_len'     => 200,
215                'dal1key'     => 'secaddr_addr1', // previous field name
216            ),
217            'secaddr2'      => array(
218                // db model:
219                'fieldname'   => 'zbsc_addr2',
220                'format'      => 'str',
221                // output model
222                'input_type'  => 'text',
223                'label'       => __( 'Address Line 2', 'zero-bs-crm' ),
224                'placeholder' => '',
225                'area'        => 'Second Address',
226                'migrate'     => 'addresses',
227                'opt'         => 'secondaddress',
228                'max_len'     => 200,
229                'dal1key'     => 'secaddr_addr2', // previous field name
230            ),
231            'seccity'       => array(
232                // db model:
233                'fieldname'   => 'zbsc_city',
234                'format'      => 'str',
235                // output model
236                'input_type'  => 'text',
237                'label'       => __( 'City', 'zero-bs-crm' ),
238                'placeholder' => 'e.g. Los Angeles',
239                'area'        => 'Second Address',
240                'migrate'     => 'addresses',
241                'opt'         => 'secondaddress',
242                'max_len'     => 200,
243                'dal1key'     => 'secaddr_city', // previous field name
244            ),
245            'seccounty'     => array(
246                // db model:
247                'fieldname'   => 'zbsc_county',
248                'format'      => 'str',
249                // output model
250                'input_type'  => 'text',
251                'label'       => __( 'County', 'zero-bs-crm' ),
252                'placeholder' => 'e.g. Los Angeles',
253                'area'        => 'Second Address',
254                'migrate'     => 'addresses',
255                'opt'         => 'secondaddress',
256                'max_len'     => 200,
257                'dal1key'     => 'secaddr_county', // previous field name
258            ),
259            'secpostcode'   => array(
260                // db model:
261                'fieldname'   => 'zbsc_postcode',
262                'format'      => 'str',
263                // output model
264                'input_type'  => 'text',
265                'label'       => __( 'Post Code', 'zero-bs-crm' ),
266                'placeholder' => 'e.g. 90001',
267                'area'        => 'Second Address',
268                'migrate'     => 'addresses',
269                'opt'         => 'secondaddress',
270                'max_len'     => 50,
271                'dal1key'     => 'secaddr_postcode', // previous field name
272            ),
273            'seccountry'    => array(
274                // db model:
275                'fieldname'   => 'zbsc_country',
276                'format'      => 'str',
277                // output model
278                'input_type'  => 'selectcountry',
279                'label'       => __( 'Country', 'zero-bs-crm' ),
280                'placeholder' => '',
281                'area'        => 'Second Address',
282                'migrate'     => 'addresses',
283                'opt'         => 'secondaddress',
284                'max_len'     => 200,
285                'dal1key'     => 'secaddr_country', // previous field name
286            ),
287            'hometel'       => array(
288                // db model:
289                'fieldname'   => 'zbsc_hometel',
290                'format'      => 'str',
291                // output model
292                'input_type'  => 'tel',
293                'label'       => __( 'Home Telephone', 'zero-bs-crm' ),
294                'placeholder' => 'e.g. 877 2733049',
295                'max_len'     => 40,
296            ),
297            'worktel'       => array(
298                // db model:
299                'fieldname'   => 'zbsc_worktel',
300                'format'      => 'str',
301                // output model
302                'input_type'  => 'tel',
303                'label'       => __( 'Work Telephone', 'zero-bs-crm' ),
304                'placeholder' => 'e.g. 877 2733049',
305                'max_len'     => 40,
306            ),
307            'mobtel'        => array(
308                // db model:
309                'fieldname'   => 'zbsc_mobtel',
310                'format'      => 'str',
311                // output model
312                'input_type'  => 'tel',
313                'label'       => __( 'Mobile Telephone', 'zero-bs-crm' ),
314                'placeholder' => 'e.g. 877 2733050',
315                'max_len'     => 40,
316            ),
317
318            // ... just removed for DAL3 :) should be custom field anyway by this point
319
320            'wpid'          => array(
321                // db model:
322                'fieldname' => 'zbsc_wpid',
323                'format'    => 'int',
324                // output model
325                // NONE, not exposed via standard input
326            ),
327            'avatar'        => array(
328                // db model:
329                'fieldname' => 'zbsc_avatar',
330                'format'    => 'str',
331                // output model
332                // NONE, not exposed via standard input
333            ),
334            'tw'            => array(
335                // db model:
336                'fieldname' => 'zbsc_tw',
337                'format'    => 'str',
338                'max_len'   => 100,
339                // output model
340                // NONE, not exposed via standard input
341            ),
342            'li'            => array(
343                // db model:
344                'fieldname' => 'zbsc_li',
345                'format'    => 'str',
346                'max_len'   => 300,
347                // output model
348                // NONE, not exposed via standard input
349            ),
350            'fb'            => array(
351                // db model:
352                'fieldname' => 'zbsc_fb',
353                'format'    => 'str',
354                'max_len'   => 200,
355                // output model
356                // NONE, not exposed via standard input
357            ),
358            'created'       => array(
359                // db model:
360                'fieldname' => 'zbsc_created',
361                'format'    => 'uts',
362                // output model
363                // NONE, not exposed via db
364            ),
365            'lastupdated'   => array(
366                // db model:
367                'fieldname' => 'zbsc_lastupdated',
368                'format'    => 'uts',
369                // output model
370                // NONE, not exposed via db
371            ),
372            'lastcontacted' => array(
373                // db model:
374                'fieldname' => 'zbsc_lastcontacted',
375                'format'    => 'uts',
376                // output model
377                // NONE, not exposed via db
378            ),
379        );
380
381        #} =========== LOAD ARGS ==============
382        $defaultArgs = array(
383
384            // 'tag' => false,
385
386        );
387        foreach ( $defaultArgs as $argK => $argV ) {
388            $this->$argK = $argV;
389            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
390                if ( is_array( $args[ $argK ] ) ) {
391                    $newData = $this->$argK;
392                    if ( ! is_array( $newData ) ) {
393                        $newData = array();
394                    }
395                    foreach ( $args[ $argK ] as $subK => $subV ) {
396                        $newData[ $subK ] = $subV;
397                    }
398                    $this->$argK = $newData;
399                } else {
400                    $this->$argK = $args[ $argK ];
401                }
402            }
403        }
404        # =========== / LOAD ARGS =============
405
406        $this->events_manager = new Events_Manager();
407        add_filter( 'jpcrm_listview_filters', array( $this, 'add_listview_filters' ) );
408    }
409
410    /**
411     * Adds items to listview filter using `jpcrm_listview_filters` hook.
412     *
413     * @param array $listview_filters Listview filters.
414     */
415    public function add_listview_filters( $listview_filters ) {
416        global $zbs;
417
418        // Add "assigned"/"not assigned" filters.
419        $listview_filters[ ZBS_TYPE_CONTACT ]['general']['assigned_to_me'] = __( 'Assigned to me', 'zero-bs-crm' );
420        $listview_filters[ ZBS_TYPE_CONTACT ]['general']['not_assigned']   = __( 'Not assigned', 'zero-bs-crm' );
421
422        $quick_filter_settings = $zbs->settings->get( 'quickfiltersettings' );
423
424        // Add 'not-contacted-in-x-days'.
425        if ( ! empty( $quick_filter_settings['notcontactedinx'] ) && $quick_filter_settings['notcontactedinx'] > 0 ) {
426            $days = (int) $quick_filter_settings['notcontactedinx'];
427            $listview_filters[ ZBS_TYPE_CONTACT ]['general'][ 'notcontactedin' . $days ] = sprintf(
428                // translators: %s is the number of days
429                __( 'Not Contacted in %s days', 'zero-bs-crm' ),
430                $days
431            );
432        }
433
434        // Add 'olderthan-x-days'.
435        if ( ! empty( $quick_filter_settings['olderthanx'] ) && $quick_filter_settings['olderthanx'] > 0 ) {
436            $days = (int) $quick_filter_settings['olderthanx'];
437            $listview_filters[ ZBS_TYPE_CONTACT ]['general'][ 'olderthan' . $days ] = sprintf(
438                // translators: %s is the number of days
439                __( 'Older than %s days', 'zero-bs-crm' ),
440                $days
441            );
442        }
443
444        // Add statuses if enabled.
445        if ( $zbs->settings->get( 'filtersfromstatus' ) === 1 ) {
446            $statuses = zeroBSCRM_getCustomerStatuses( true );
447            foreach ( $statuses as $status ) {
448                $listview_filters[ ZBS_TYPE_CONTACT ]['status'][ 'status_' . $status ] = $status;
449            }
450        }
451
452        // Add segments if enabled.
453        if ( $zbs->settings->get( 'filtersfromsegments' ) === 1 ) {
454            $segments = $zbs->DAL->segments->getSegments( -1, 100, 0, false, '', '', 'zbsseg_name', 'ASC' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
455            foreach ( $segments as $segment ) {
456                $listview_filters[ ZBS_TYPE_CONTACT ]['segment'][ 'segment_' . $segment['slug'] ] = $segment['name'];
457            }
458        }
459
460        return $listview_filters;
461    }
462
463    // generic get Company (by ID)
464    // Super simplistic wrapper used by edit page etc. (generically called via dal->contacts->getSingle etc.)
465    public function getSingle( $ID = -1 ) {
466
467        return $this->getContact( $ID );
468    }
469
470    // generic get contact (by ID list)
471    // Super simplistic wrapper used by MVP Export v3.0
472    public function getIDList( $IDs = false ) {
473
474        return $this->getContacts(
475            array(
476                'inArr'            => $IDs,
477                'withCustomFields' => true,
478                'withValues'       => true,
479                'withAssigned'     => true,
480                'page'             => -1,
481                'perPage'          => -1,
482            )
483        );
484    }
485
486    // generic get (EVERYTHING)
487    // expect heavy load!
488    public function getAll( $IDs = false ) {
489
490        return $this->getContacts(
491            array(
492                'withCustomFields' => true,
493                'withValues'       => true,
494                'withAssigned'     => true,
495                'sortByField'      => 'ID',
496                'sortOrder'        => 'ASC',
497                'page'             => -1,
498                'perPage'          => -1,
499            )
500        );
501    }
502
503    // generic get count of (EVERYTHING)
504    public function getFullCount() {
505
506        return $this->getContacts(
507            array(
508                'count'   => true,
509                'page'    => -1,
510                'perPage' => -1,
511            )
512        );
513    }
514
515    /*
516    * Returns an (int) count of all contacts with an external source
517    */
518    public function getTotalExtSourceCount() {
519
520        global $ZBSCRM_t, $wpdb;
521
522        $query = 'SELECT COUNT(contacts.id) FROM ' . $ZBSCRM_t['contacts'] . ' contacts'
523            . ' INNER JOIN ' . $ZBSCRM_t['externalsources'] . ' ext_sources'
524            . ' ON contacts.id = ext_sources.zbss_objid'
525            . ' WHERE ext_sources.zbss_objtype = ' . ZBS_TYPE_CONTACT;
526
527        /*
528        SELECT COUNT(contacts.id) FROM
529        wp_zbs_contacts contacts
530        INNER JOIN wp_zbs_externalsources ext_sources
531        ON contacts.id = ext_sources.zbss_objid
532        WHERE ext_sources.zbss_objtype = 1
533        */
534
535        return $wpdb->get_var( $query );
536    }
537
538    /**
539     * returns full contact line +- details
540     * Replaces many funcs, inc zeroBS_getCustomerIDFromWPID, zeroBS_getCustomerIDWithEmail etc.
541     *
542     * @param int id        contact id
543     * @param array                    $args   Associative array of arguments
544     *                                         withQuotes, withInvoices, withTransactions, withLogs
545     *
546     * @return array result
547     */
548    public function getContact( $id = -1, $args = array() ) {
549
550        global $zbs;
551
552        #} =========== LOAD ARGS ==============
553        $defaultArgs = array(
554
555            'email'                      => false, // if id -1 and email given, will return based on email search
556            'WPID'                       => false, // if id -1 and wpid given, will return based on wpid search
557
558            // if theset wo passed, will search based on these
559            'externalSource'             => false,
560            'externalSourceUID'          => false,
561
562            // with what?
563            'withCustomFields'           => true,
564            'withQuotes'                 => false,
565            'withInvoices'               => false,
566            'withTransactions'           => false,
567            'withTasks'                  => false,
568            'withLogs'                   => false,
569            'withLastLog'                => false,
570            'withTags'                   => false,
571            'withCompanies'              => false,
572            'withOwner'                  => false,
573            'withValues'                 => false, // if passed, returns with 'total' 'invoices_total' 'transactions_total' etc. (requires getting all obj, use sparingly)
574            'withAliases'                => false,
575            'withExternalSources'        => false,
576            'withExternalSourcesGrouped' => false,
577
578            'with_obj_limit'             => false, // if (int) specified, this will limit the count of quotes, invoices, transactions, and tasks returned
579
580            // permissions
581            'ignoreowner'                => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ), // this'll let you not-check the owner of obj
582
583            // returns scalar ID of line
584            'onlyID'                     => false,
585
586            'fields'                     => false, // false = *, array = fieldnames
587
588        );
589        foreach ( $defaultArgs as $argK => $argV ) {
590            $$argK = $argV;
591            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
592                if ( is_array( $args[ $argK ] ) ) {
593                    $newData = $$argK;
594                    if ( ! is_array( $newData ) ) {
595                        $newData = array();
596                    }
597                    foreach ( $args[ $argK ] as $subK => $subV ) {
598                        $newData[ $subK ] = $subV;
599                    }
600                    $$argK = $newData;
601                } else {
602                    $$argK = $args[ $argK ];
603                }
604            }
605        }
606        #} =========== / LOAD ARGS =============
607
608        #} Check ID
609        $id = (int) $id;
610        if ( ( ! empty( $id ) && $id > 0 )
611            ||
612            ( ! empty( $WPID ) && $WPID > 0 )
613            ||
614            ( ! empty( $email ) )
615            ||
616            ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) )
617            ) {
618
619            global $ZBSCRM_t, $wpdb;
620            $wheres          = array( 'direct' => array() );
621            $whereStr        = '';
622            $additionalWhere = '';
623            $params          = array();
624            $res             = array();
625            $extraSelect     = '';
626
627            #} ============= PRE-QUERY ============
628
629                #} Custom Fields
630            if ( $withCustomFields && ! $onlyID ) {
631
632                #} Retrieve any cf
633                $custFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_CONTACT ) );
634
635                #} Cycle through + build into query
636                if ( is_array( $custFields ) ) {
637                    foreach ( $custFields as $cK => $cF ) {
638
639                        // add as subquery
640                        $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . " WHERE zbscf_objid = contact.ID AND zbscf_objkey = %s AND zbscf_objtype = %d LIMIT 1) '" . $cK . "'";
641
642                        // add params
643                        $params[] = $cK;
644                        $params[] = ZBS_TYPE_CONTACT;
645
646                    }
647                }
648            }
649
650                #} Aliases
651            if ( $withAliases ) {
652
653                #} Retrieve these as a CSV :)
654                $extraSelect .= ',(SELECT ' . $this->DAL()->build_group_concat( 'aka_alias', ',' ) . ' FROM ' . $ZBSCRM_t['aka'] . ' WHERE aka_type = ' . ZBS_TYPE_CONTACT . ' AND aka_id = contact.ID) aliases'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
655
656            }
657
658                // Add any addr custom fields for addr1+addr2
659                $addrCustomFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_ADDRESS ) );
660            if ( $withCustomFields && ! $onlyID && is_array( $addrCustomFields ) && count( $addrCustomFields ) > 0 ) {
661
662                foreach ( $addrCustomFields as $cK => $cF ) {
663
664                    // custom field key
665                    $cfKey  = 'addr_' . $cK;
666                    $cfKey2 = 'secaddr_' . $cK;
667
668                    // address custom field (e.g. 'house name') it'll be passed here as 'house-name'
669                    // ... problem is mysql does not like that :) so we have to chage here:
670                    // in this case we prepend address cf's with addr_ and we switch - for _
671                    $cKey  = 'addrcf_' . str_replace( '-', '_', $cK );
672                    $cKey2 = 'secaddrcf_' . str_replace( '-', '_', $cK );
673
674                    // addr 1
675                    // add as subquery
676                    $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = contact.ID AND zbscf_objkey = %s AND zbscf_objtype = %d) ' . $cKey;
677                    // add params
678                    $params[] = $cfKey;
679                    $params[] = ZBS_TYPE_CONTACT;
680                    // addr 2
681                    // add as subquery
682                    $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = contact.ID AND zbscf_objkey = %s AND zbscf_objtype = %d) ' . $cKey2;
683                    // add params
684                    $params[] = $cfKey2;
685                    $params[] = ZBS_TYPE_CONTACT;
686
687                }
688            }
689
690            // ==== TOTAL VALUES
691
692            // Calculate total vals etc. with SQL
693            if ( $withValues && ! $onlyID ) {
694                // only include transactions with statuses which should be included in total value:
695                $transStatusQueryAdd = $this->DAL()->transactions->getTransactionStatusesToIncludeQuery(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
696                // include invoices without deleted status in the total value for invoices_total_inc_deleted:
697                $inv_status_query_add = $this->DAL()->invoices->get_invoice_status_except_deleted_for_query();
698
699                // quotes:
700                $extraSelect .= ',(SELECT SUM(quotestotal.zbsq_value) FROM ' . $ZBSCRM_t['quotes'] . ' as quotestotal WHERE quotestotal.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)) as quotes_total';
701                // invs not including deleted:
702                $extraSelect .= ',(SELECT IFNULL(SUM(invstotal.zbsi_total),0) FROM ' . $ZBSCRM_t['invoices'] . ' as invstotal WHERE invstotal.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $inv_status_query_add . ') as invoices_total'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
703                // invs including deleted:
704                $extraSelect .= ',(SELECT SUM(invstotalincdeleted.zbsi_total) FROM ' . $ZBSCRM_t['invoices'] . ' as invstotalincdeleted WHERE invstotalincdeleted.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)) as invoices_total_inc_deleted'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
705                // invs count:
706                $extraSelect .= ',(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $inv_status_query_add . ') as invoices_count'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
707                // invs count including deleted:
708                $extraSelect .= ',(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)) as invoices_count_inc_deleted'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
709                // trans (with status):
710                $extraSelect .= ',(SELECT SUM(transtotal.zbst_total) FROM ' . $ZBSCRM_t['transactions'] . ' as transtotal WHERE transtotal.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $transStatusQueryAdd . ') as transactions_total';
711                // paid balance against invs  (also in getContacts)
712                // (this allows us to subtract from totals to get a true figure where transactions are part/whole payments for invs)
713                /*
714                    This selects transactions
715                        where there is a link to an invoice
716                            where that invoice has a link to this contact:
717
718                    ==========
719
720                    SELECT * FROM wp_zbs_transactions trans
721                    WHERE trans.ID IN
722
723                        (
724                            SELECT DISTINCT zbsol_objid_from FROM `wp_zbs_object_links`
725                            WHERE zbsol_objtype_from = 5
726                            AND zbsol_objtype_to = 4
727                            AND zbsol_objid_to IN
728
729                                (
730
731                                    SELECT DISTINCT zbsol_objid_from FROM `wp_zbs_object_links`
732                                    WHERE zbsol_objtype_from = 4 AND zbsol_objtype_to = 1 AND zbsol_objid_to = 1
733
734                                )
735
736                        )
737
738                */
739                $extraSelect .= ',(SELECT SUM(assignedtranstotal.zbst_total) FROM ' . $ZBSCRM_t['transactions'] . ' assignedtranstotal WHERE assignedtranstotal.ID IN ';
740                $extraSelect .= '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . ' AND zbsol_objtype_to = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objid_to IN ';
741                $extraSelect .= '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)';
742                $extraSelect .= ')' . $transStatusQueryAdd . ') as transactions_paid_total';
743            }
744
745            // ==== / TOTAL VALUES
746
747            $selector = 'contact.*';
748            if ( is_array( $fields ) ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
749                $selector = '';
750
751                // always needs id, so add if not present
752                if ( ! in_array( 'ID', $fields ) ) {
753                    $selector = 'contact.ID';
754                }
755
756                foreach ( $fields as $f ) {
757                    if ( ! empty( $selector ) ) {
758                        $selector .= ',';
759                    }
760                    $selector .= 'contact.' . $f;
761                }
762            } elseif ( $onlyID ) {
763                $selector = 'contact.ID';
764            }
765
766            #} ============ / PRE-QUERY ===========
767
768            #} Build query
769            $query = 'SELECT ' . $selector . $extraSelect . ' FROM ' . $ZBSCRM_t['contacts'] . ' as contact';
770            #} ============= WHERE ================
771
772            if ( ! empty( $id ) && $id > 0 ) {
773
774                #} Add ID
775                $wheres['ID'] = array( 'ID', '=', '%d', $id );
776
777            }
778
779            if ( ! empty( $email ) ) {
780
781                // where we're seeking the ID from an email we can override the query for performance benefits (#gh-2450):
782                if ( $onlyID ) {
783
784                    $query  = 'SELECT contact.ID FROM ( SELECT contact.ID FROM ' . $ZBSCRM_t['contacts'] . ' as contact WHERE zbsc_email = %s UNION ALL SELECT aka_id AS ID FROM ' . $ZBSCRM_t['aka'] . ' WHERE aka_type = 1 AND aka_alias = %s) contact';
785                    $wheres = array( 'direct' => array() );
786                    $params = array( $email, $email );
787
788                } else {
789
790                    $emailWheres = array();
791
792                    #} Add ID
793                    $emailWheres['emailcheck'] = array( 'zbsc_email', '=', '%s', $email );
794
795                    #} Check AKA
796                    $emailWheres['email_alias'] = array( 'ID', 'IN', '(SELECT aka_id FROM ' . $ZBSCRM_t['aka'] . ' WHERE aka_type = ' . ZBS_TYPE_CONTACT . ' AND aka_alias = %s)', $email );
797
798                    // This generates a query like 'zbsc_email = %s OR zbsc_email2 = %s',
799                    // which we then need to include as direct subquery (below) in main query :)
800                    $emailSearchQueryArr = $this->buildWheres( $emailWheres, '', array(), 'OR', false );
801
802                    if ( is_array( $emailSearchQueryArr ) && isset( $emailSearchQueryArr['where'] ) && ! empty( $emailSearchQueryArr['where'] ) ) {
803
804                        // add it
805                        $wheres['direct'][] = array( '(' . $emailSearchQueryArr['where'] . ')', $emailSearchQueryArr['params'] );
806
807                    }
808                }
809            }
810
811            if ( ! empty( $WPID ) && $WPID > 0 ) {
812
813                #} Add ID
814                $wheres['WPID'] = array( 'zbsc_wpid', '=', '%d', $WPID );
815
816            }
817
818            if ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) ) {
819
820                $wheres['extsourcecheck'] = array( 'ID', 'IN', '(SELECT DISTINCT zbss_objid FROM ' . $ZBSCRM_t['externalsources'] . ' WHERE zbss_objtype = ' . ZBS_TYPE_CONTACT . ' AND zbss_source = %s AND zbss_uid = %s)', array( $externalSource, $externalSourceUID ) );
821
822            }
823
824            #} ============ / WHERE ==============
825
826            #} Build out any WHERE clauses
827            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
828            $whereStr  = $wheresArr['where'];
829            $params    = $params + $wheresArr['params'];
830            #} / Build WHERE
831
832            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
833            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
834            $ownQ   = $this->ownershipSQL( $ignoreowner );
835            if ( ! empty( $ownQ ) ) {
836                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
837            }
838            #} / Ownership
839
840            #} Append to sql (this also automatically deals with sortby and paging)
841            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
842
843            try {
844
845                #} Prep & run query
846                $queryObj     = $this->prepare( $query, $params );
847                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
848
849            } catch ( Exception $e ) {
850
851                #} General SQL Err
852                $this->catchSQLError( $e );
853
854            }
855
856            #} Interpret Results (ROW)
857            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
858
859                #} Has results, tidy + return
860
861                #} Only ID? return it directly
862                if ( $onlyID ) {
863                    return $potentialRes->ID;
864                }
865
866                // tidy
867                if ( is_array( $fields ) ) {
868                    // guesses fields based on table col names
869                    $res = $this->lazyTidyGeneric( $potentialRes );
870                } else {
871                    // proper tidy
872                    $res = $this->tidy_contact( $potentialRes, $withCustomFields );
873                }
874
875                if ( $withTags ) {
876
877                    // add all tags lines
878                    $res['tags'] = $this->DAL()->getTagsForObjID(
879                        array(
880                            'objtypeid' => ZBS_TYPE_CONTACT,
881                            'objid'     => $potentialRes->ID,
882                        )
883                    );
884
885                }
886
887                    // ===================================================
888                    // ========== #} #DB1LEGACY (TOMOVE)
889                    // == Following is all using OLD DB stuff, here until we migrate inv etc.
890                    // ===================================================
891
892                    #} With most recent log? #DB1LEGACY (TOMOVE)
893                if ( $withLastLog ) {
894
895                    $res['lastlog'] = $this->DAL()->logs->getLogsForObj(
896                        array(
897
898                            'objtype'     => ZBS_TYPE_CONTACT,
899                            'objid'       => $potentialRes->ID,
900
901                            'incMeta'     => true,
902
903                            'sortByField' => 'zbsl_created',
904                            'sortOrder'   => 'DESC',
905                            'page'        => 0,
906                            'perPage'     => 1,
907
908                        )
909                    );
910
911                }
912
913                    #} With Assigned?
914                if ( $withOwner ) {
915
916                    $res['owner'] = zeroBS_getOwner( $potentialRes->ID, true, 'zerobs_customer', $potentialRes->zbs_owner );
917
918                }
919
920                        // Objects: return all, unless $with_obj_limit
921                        $objs_page      = -1;
922                        $objs_per_page  = -1;
923                        $with_obj_limit = (int) $with_obj_limit;
924                if ( $with_obj_limit > 0 ) {
925
926                    $objs_page     = 0;
927                    $objs_per_page = $with_obj_limit;
928
929                }
930
931                if ( $withInvoices ) {
932
933                    #} only gets first 100?
934                    // DAL3 ver, more perf, gets all
935                    $res['invoices'] = $zbs->DAL->invoices->getInvoices(
936                        array(
937
938                            'assignedContact' => $potentialRes->ID, // assigned to company id (int)
939                            'page'            => $objs_page,
940                            'perPage'         => $objs_per_page,
941                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_INVOICE ),
942                            'sortByField'     => 'ID',
943                            'sortOrder'       => 'DESC',
944                            'withAssigned'    => false, // no need, it's assigned to this obj already
945
946                        )
947                    );
948
949                }
950
951                if ( $withQuotes ) {
952
953                    // DAL3 ver, more perf, gets all
954                    $res['quotes'] = $zbs->DAL->quotes->getQuotes(
955                        array(
956
957                            'assignedContact' => $potentialRes->ID, // assigned to company id (int)
958                            'page'            => $objs_page,
959                            'perPage'         => $objs_per_page,
960                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_QUOTE ),
961                            'sortByField'     => 'ID',
962                            'sortOrder'       => 'DESC',
963                            'withAssigned'    => false, // no need, it's assigned to this obj already
964
965                        )
966                    );
967
968                }
969
970                        #} ... brutal for mvp #DB1LEGACY (TOMOVE)
971                if ( $withTransactions ) {
972
973                    // DAL3 ver, more perf, gets all
974                    $res['transactions'] = $zbs->DAL->transactions->getTransactions(
975                        array(
976
977                            'assignedContact' => $potentialRes->ID, // assigned to company id (int)
978                            'page'            => $objs_page,
979                            'perPage'         => $objs_per_page,
980                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_TRANSACTION ),
981                            'sortByField'     => 'ID',
982                            'sortOrder'       => 'DESC',
983                            'withAssigned'    => false, // no need, it's assigned to this obj already
984
985                        )
986                    );
987
988                }
989
990                    // }
991
992                    #} With co's?
993                if ( $withCompanies ) {
994
995                    // add all company lines
996                    $res['companies'] = $this->DAL()->getObjsLinkedToObj(
997                        array(
998                            'objtypefrom' => ZBS_TYPE_CONTACT, // contact
999                            'objtypeto'   => ZBS_TYPE_COMPANY, // company
1000                            'objfromid'   => $potentialRes->ID,
1001                        )
1002                    );
1003
1004                }
1005
1006                    #} ... brutal for mvp #DB1LEGACY (TOMOVE)
1007                if ( $withTasks ) {
1008
1009                    // DAL3 ver, more perf, gets all
1010                    $res['tasks'] = $zbs->DAL->events->getEvents(
1011                        array(
1012
1013                            'assignedContact' => $potentialRes->ID, // assigned to company id (int)
1014                            'page'            => $objs_page,
1015                            'perPage'         => $objs_per_page,
1016                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_TASK ),
1017                            'sortByField'     => 'zbse_start',
1018                            'sortOrder'       => 'DESC',
1019                            'withAssigned'    => false, // no need, it's assigned to this obj already
1020
1021                        )
1022                    );
1023
1024                }
1025
1026                    // simplistic, could be optimised (though low use means later.)
1027                if ( $withExternalSources ) {
1028
1029                    $res['external_sources'] = $zbs->DAL->contacts->getExternalSourcesForContact(
1030                        array(
1031
1032                            'contactID'   => $potentialRes->ID,
1033
1034                            'sortByField' => 'ID',
1035                            'sortOrder'   => 'ASC',
1036                            'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
1037
1038                        )
1039                    );
1040
1041                }
1042                if ( $withExternalSourcesGrouped ) {
1043
1044                    $res['external_sources'] = $zbs->DAL->getExternalSources(
1045                        -1,
1046                        array(
1047
1048                            'objectID'          => $potentialRes->ID,
1049                            'objectType'        => ZBS_TYPE_CONTACT,
1050                            'grouped_by_source' => true,
1051                            'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
1052
1053                        )
1054                    );
1055
1056                }
1057
1058                    // ===================================================
1059                    // ========== / #DB1LEGACY (TOMOVE)
1060                    // ===================================================
1061
1062                    return $res;
1063
1064            }
1065        } // / if ID
1066
1067        return false;
1068    }
1069
1070    // TODO $argsOverride=false
1071    /**
1072     * returns contact detail lines
1073     *
1074     * @param array $args Associative array of arguments
1075     *              withQuotes, withInvoices, withTransactions, withLogs, searchPhrase, sortByField, sortOrder, page, perPage
1076     *
1077     * @return array of contact lines
1078     */
1079    public function getContacts( $args = array() ) {
1080
1081        global $zbs;
1082
1083        #} ============ LOAD ARGS =============
1084        $defaultArgs = array(
1085
1086            // Search/Filtering (leave as false to ignore)
1087            'searchPhrase'               => '', // searches which fields?
1088            'inCompany'                  => false, // will be an ID if used
1089            'inArr'                      => false,
1090            'quickFilters'               => false,
1091            'isTagged'                   => false, // 1x INT OR array(1,2,3)
1092            'isNotTagged'                => false, // 1x INT OR array(1,2,3)
1093            'ownedBy'                    => false,
1094            'externalSource'             => false, // e.g. paypal
1095            'olderThan'                  => false, // uts
1096            'newerThan'                  => false, // uts
1097            'hasStatus'                  => false, // Lead (this takes over from the quick filter post 19/6/18)
1098            'otherStatus'                => false, // status other than 'Lead'
1099
1100            // last contacted
1101            'contactedBefore'            => false, // uts
1102            'contactedAfter'             => false, // uts
1103
1104            // email
1105            'hasEmail'                   => false, // 'x@y.com' either in main field or as AKA
1106
1107            // addr
1108            'inCounty'                   => false, // Hertfordshire
1109            'inPostCode'                 => false, // AL1 1AA
1110            'inCountry'                  => false, // United Kingdom
1111            'notInCounty'                => false, // Hertfordshire
1112            'notInPostCode'              => false, // AL1 1AA
1113            'notInCountry'               => false, // United Kingdom
1114
1115            // generic assignments - requires both
1116            // Where the link relationship is OBJECT -> CONTACT
1117            'hasObjIDLinkedTo'           => false, // e.g. quoteid 123
1118            'hasObjTypeLinkedTo'         => false, // e.g. ZBS_TYPE_QUOTE
1119
1120            // generic assignments - requires both
1121            // Where the link relationship is CONTACT -> OBJECT
1122            'isLinkedToObjID'            => false, // e.g. quoteid 123
1123            'isLinkedToObjType'          => false, // e.g. ZBS_TYPE_QUOTE
1124
1125            // returns
1126            'count'                      => false,
1127            'onlyObjTotals'              => false, // if passed, returns for group: 'total' 'invoices_total' 'transactions_total' etc. (requires getting a lot of objs, use sparingly)
1128            'withCustomFields'           => true,
1129            'withQuotes'                 => false,
1130            'withInvoices'               => false,
1131            'withTransactions'           => false,
1132            'withTasks'                  => false,
1133            'withLogs'                   => false,
1134            'withLastLog'                => false,
1135            'withTags'                   => false,
1136            'withOwner'                  => false,
1137            'withAssigned'               => false, // return ['company'] objs if has link
1138            'withDND'                    => false, // if true, returns getContactDoNotMail as well :)
1139            'simplified'                 => false, // returns just id,name,created,email (for typeaheads)
1140            'withValues'                 => false, // if passed, returns with 'total' 'invoices_total' 'transactions_total' etc. (requires getting all obj, use sparingly)
1141            'onlyColumns'                => false, // if passed (array('fname','lname')) will return only those columns (overwrites some other 'return' options). NOTE: only works for base fields (not custom fields)
1142            'withAliases'                => false,
1143            'withExternalSources'        => false,
1144            'withExternalSourcesGrouped' => false,
1145
1146            'sortByField'                => 'ID',
1147            'sortOrder'                  => 'ASC',
1148            'page'                       => 0, // this is what page it is (gets * by for limit)
1149            'perPage'                    => 100,
1150
1151            // permissions
1152            'ignoreowner'                => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ), // this'll let you not-check the owner of obj
1153
1154            // 'argsOverride' => ?? Still req?
1155
1156            // specifics
1157            // NOTE: this is ONLY for use where a sql query is 1 time use, otherwise add as argument
1158            // ... for later use, (above)
1159            // PLEASE do not use the or switch without discussing case with WH
1160            'additionalWhereArr'         => false,
1161            'additional_joins'           => false,
1162            'whereCase'                  => 'AND', // DEFAULT = AND
1163
1164        );
1165        foreach ( $defaultArgs as $argK => $argV ) {
1166            $$argK = $argV;
1167            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1168                if ( is_array( $args[ $argK ] ) ) {
1169                    $newData = $$argK;
1170                    if ( ! is_array( $newData ) ) {
1171                        $newData = array();
1172                    }
1173                    foreach ( $args[ $argK ] as $subK => $subV ) {
1174                        $newData[ $subK ] = $subV;
1175                    }
1176                    $$argK = $newData;
1177                } else {
1178                    $$argK = $args[ $argK ];
1179                }
1180            }
1181        }
1182        #} =========== / LOAD ARGS =============
1183
1184        global $ZBSCRM_t, $wpdb, $zbs;
1185        $wheres          = array( 'direct' => array() );
1186        $whereStr        = '';
1187        $additionalWhere = '';
1188        $params          = array();
1189        $res             = array();
1190        $joinQ           = '';
1191        $extraSelect     = '';
1192
1193        $join_sql = '';
1194
1195        #} ============= PRE-QUERY ============
1196
1197        #} Capitalise this
1198        $sortOrder = strtoupper( $sortOrder );
1199        if ( ! in_array( $sortOrder, array( 'ASC', 'DESC' ) ) ) {
1200            $sortOrder = 'ASC';
1201        }
1202
1203        // If just count or simplified, turn off any extras
1204        if ( $count || $simplified || $onlyObjTotals ) {
1205            $withCustomFields           = false;
1206            $withQuotes                 = false;
1207            $withInvoices               = false;
1208            $withTransactions           = false;
1209            $withTasks                  = false;
1210            $withLogs                   = false;
1211            $withLastLog                = false;
1212            $withTags                   = false;
1213            $withOwner                  = false;
1214            $withAssigned               = false;
1215            $withDND                    = false;
1216            $withAliases                = false;
1217            $withExternalSources        = false;
1218            $withExternalSourcesGrouped = false;
1219        }
1220
1221        #} If onlyColumns, validate
1222        if ( $onlyColumns ) {
1223
1224            #} onlyColumns build out a field arr
1225            if ( is_array( $onlyColumns ) && count( $onlyColumns ) > 0 ) {
1226
1227                $onlyColumnsFieldArr = array();
1228                foreach ( $onlyColumns as $col ) {
1229
1230                    // find db col key from field key (e.g. fname => zbsc_fname)
1231                    $dbCol = '';
1232                    if ( isset( $this->objectModel[ $col ] ) && isset( $this->objectModel[ $col ]['fieldname'] ) ) {
1233                        $dbCol = $this->objectModel[ $col ]['fieldname'];
1234                    }
1235
1236                    if ( ! empty( $dbCol ) ) {
1237
1238                        $onlyColumnsFieldArr[ $dbCol ] = $col;
1239
1240                    }
1241                }
1242            }
1243
1244            // if legit cols:
1245            if ( isset( $onlyColumnsFieldArr ) && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
1246
1247                $onlyColumns = true;
1248
1249                // If onlyColumns, turn off extras
1250                $withCustomFields           = false;
1251                $withQuotes                 = false;
1252                $withInvoices               = false;
1253                $withTransactions           = false;
1254                $withTasks                  = false;
1255                $withLogs                   = false;
1256                $withLastLog                = false;
1257                $withTags                   = false;
1258                $withOwner                  = false;
1259                $withAssigned               = false;
1260                $withDND                    = false;
1261                $withAliases                = false;
1262                $withExternalSources        = false;
1263                $withExternalSourcesGrouped = false;
1264
1265            } else {
1266
1267                // deny
1268                $onlyColumns = false;
1269
1270            }
1271        }
1272
1273        #} Custom Fields
1274        // @phan-suppress-next-line PhanImpossibleCondition -- Phan is confused; this var is initialized at the beginning of the function.
1275        if ( $withCustomFields ) {
1276
1277            #} Retrieve any cf
1278            $custFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_CONTACT ) );
1279
1280            #} Cycle through + build into query
1281            if ( is_array( $custFields ) ) {
1282                foreach ( $custFields as $cK => $cF ) {
1283
1284                    // custom field (e.g. 'third name') it'll be passed here as 'third-name'
1285                    // ... problem is mysql does not like that :) so we have to chage here:
1286                    // in this case we prepend cf's with cf_ and we switch - for _
1287                    $cKey = 'cf_' . str_replace( '-', '_', $cK );
1288
1289                    // we also check the $sortByField in case that's the same cf
1290                    if ( $cK == $sortByField ) {
1291
1292                        // sort by
1293                        $sortByField = $cKey;
1294
1295                        // check if sort needs any CAST (e.g. numeric):
1296                        $sortByField = $this->DAL()->build_custom_field_order_by_str( $sortByField, $cF );
1297
1298                    }
1299
1300                    // add as subquery
1301                    $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = contact.ID AND zbscf_objkey = %s AND zbscf_objtype = %d LIMIT 1) ' . $cKey;
1302
1303                    // add params
1304                    $params[] = $cK;
1305                    $params[] = ZBS_TYPE_CONTACT;
1306
1307                }
1308            }
1309        }
1310
1311        #} Aliases
1312        // @phan-suppress-next-line PhanImpossibleCondition -- Phan is confused; this var is initialized at the beginning of the function.
1313        if ( $withAliases ) {
1314
1315            #} Retrieve these as a CSV :)
1316            $extraSelect .= ',(SELECT ' . $this->DAL()->build_group_concat( 'aka_alias', ',' ) . ' FROM ' . $ZBSCRM_t['aka'] . ' WHERE aka_type = ' . ZBS_TYPE_CONTACT . ' AND aka_id = contact.ID) aliases'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1317
1318        }
1319
1320        // Add any addr custom fields for addr1+addr2
1321        // no need if simpliefied or count parameters passed
1322        if ( ! $simplified && ! $count && ! $onlyColumns && ! $onlyObjTotals ) {
1323            $addrCustomFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_ADDRESS ) );
1324            if ( is_array( $addrCustomFields ) && count( $addrCustomFields ) > 0 ) {
1325
1326                foreach ( $addrCustomFields as $cK => $cF ) {
1327
1328                    // custom field key
1329                    $cfKey  = 'addr_' . $cK;
1330                    $cfKey2 = 'secaddr_' . $cK;
1331
1332                    // address custom field (e.g. 'house name') it'll be passed here as 'house-name'
1333                    // ... problem is mysql does not like that :) so we have to chage here:
1334                    // in this case we prepend address cf's with addr_ and we switch - for _
1335                    $cKey  = 'addrcf_' . str_replace( '-', '_', $cK );
1336                    $cKey2 = 'secaddrcf_' . str_replace( '-', '_', $cK );
1337
1338                    // we also check the $sortByField in case that's the same cf (contacts need the prefix 'zbsc_' :rolls-eyes:)
1339                    if ( 'zbsc_' . $cfKey == $sortByField ) {
1340                        $sortByField = $cKey;
1341                    }
1342                    if ( 'zbsc_' . $cfKey2 == $sortByField ) {
1343                        $sortByField = $cKey2;
1344                    }
1345
1346                    // addr 1
1347                    // add as subquery
1348                    $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = contact.ID AND zbscf_objkey = %s AND zbscf_objtype = %d) ' . $cKey;
1349                    // add params
1350                    $params[] = $cfKey;
1351                    $params[] = ZBS_TYPE_CONTACT;
1352                    // addr 2
1353                    // add as subquery
1354                    $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = contact.ID AND zbscf_objkey = %s AND zbscf_objtype = %d) ' . $cKey2;
1355                    // add params
1356                    $params[] = $cfKey2;
1357                    $params[] = ZBS_TYPE_CONTACT;
1358
1359                }
1360            }
1361        }
1362
1363        // ==== TOTAL VALUES
1364
1365        // If we're sorting by total value, we need the values
1366        if ( $sortByField === 'totalvalue' ) {
1367            $withValues = true;
1368        }
1369
1370        // Calculate total vals etc. with SQL
1371        if ( ! $simplified && ! $count && $withValues && ! $onlyColumns ) {
1372
1373            // arguably, if getting $withInvoices etc. may be more performant to calc this in php in AFTER loop,
1374            // ... for now as a fair guess, this'll be most performant:
1375            // ... we calc total by adding invs + trans below :)
1376
1377            // only include transactions with statuses which should be included in total value:
1378            $transStatusQueryAdd = $this->DAL()->transactions->getTransactionStatusesToIncludeQuery();
1379            // include invoices without deleted status in the total value for invoices_total_inc_deleted:
1380            $inv_status_query_add = $this->DAL()->invoices->get_invoice_status_except_deleted_for_query();
1381
1382            // quotes:
1383            $extraSelect .= ',(SELECT SUM(quotestotal.zbsq_value) FROM ' . $ZBSCRM_t['quotes'] . ' as quotestotal WHERE quotestotal.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)) as quotes_total';
1384            // invs:
1385            $extraSelect .= ',(SELECT IFNULL(SUM(invstotal.zbsi_total),0) FROM ' . $ZBSCRM_t['invoices'] . ' as invstotal WHERE invstotal.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $inv_status_query_add . ') as invoices_total'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1386            // invs including deleted:
1387            $extraSelect .= ',(SELECT SUM(invstotalincdeleted.zbsi_total) FROM ' . $ZBSCRM_t['invoices'] . ' as invstotalincdeleted WHERE invstotalincdeleted.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)) as invoices_total_inc_deleted'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1388            // invs count:
1389            $extraSelect .= ',(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $inv_status_query_add . ') as invoices_count'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1390            // invs count including deleted:
1391            $extraSelect .= ',(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)) as invoices_count_inc_deleted'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1392
1393            // trans (with status):
1394            $extraSelect .= ',(SELECT SUM(transtotal.zbst_total) FROM ' . $ZBSCRM_t['transactions'] . ' as transtotal WHERE transtotal.ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $transStatusQueryAdd . ') as transactions_total';
1395            // paid balance against invs  (also in getContact)
1396            // (this allows us to subtract from totals to get a true figure where transactions are part/whole payments for invs)
1397            /*
1398                This selects transactions
1399                    where there is a link to an invoice
1400                        where that invoice has a link to this contact:
1401
1402                ==========
1403
1404                SELECT * FROM wp_zbs_transactions trans
1405                WHERE trans.ID IN
1406
1407                    (
1408                        SELECT DISTINCT zbsol_objid_from FROM `wp_zbs_object_links`
1409                        WHERE zbsol_objtype_from = 5
1410                        AND zbsol_objtype_to = 4
1411                        AND zbsol_objid_to IN
1412
1413                            (
1414
1415                                SELECT DISTINCT zbsol_objid_from FROM `wp_zbs_object_links`
1416                                WHERE zbsol_objtype_from = 4 AND zbsol_objtype_to = 1 AND zbsol_objid_to = 1
1417
1418                            )
1419
1420                    )
1421
1422            */
1423            $extraSelect .= ',(SELECT SUM(assignedtranstotal.zbst_total) FROM ' . $ZBSCRM_t['transactions'] . ' assignedtranstotal WHERE assignedtranstotal.ID IN ';
1424            $extraSelect .= '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . ' AND zbsol_objtype_to = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objid_to IN ';
1425            $extraSelect .= '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)';
1426            $extraSelect .= ')' . $transStatusQueryAdd . ') as transactions_paid_total';
1427
1428        }
1429
1430        // ==== / TOTAL VALUES
1431
1432        // @phan-suppress-next-line PhanImpossibleCondition -- Phan is confused; this var is initialized at the beginning of the function.
1433        if ( $withDND ) {
1434
1435            // add as subquery
1436            $extraSelect .= ',(SELECT zbsm_val FROM ' . $ZBSCRM_t['meta'] . ' WHERE zbsm_objid = contact.ID AND zbsm_key = %s AND zbsm_objtype = ' . ZBS_TYPE_CONTACT . ' LIMIT 1) dnd';
1437
1438            // add params
1439            $params[] = 'do-not-email';
1440
1441        }
1442
1443        #} ============ / PRE-QUERY ===========
1444
1445        if ( ! empty( $additional_joins ) ) {
1446            list( $join_sql, $join_params ) = $this->DAL()->build_joins( $additional_joins, $whereCase === 'AND' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1447        }
1448
1449        #} Build query
1450        $query = 'SELECT contact.*' . $extraSelect . ' FROM ' . $ZBSCRM_t['contacts'] . ' as contact' . $joinQ;
1451
1452        #} Count override
1453        if ( $count ) {
1454            $query = 'SELECT COUNT(contact.ID) FROM ' . $ZBSCRM_t['contacts'] . ' as contact' . $joinQ;
1455        }
1456
1457        #} simplified override
1458        if ( $simplified ) {
1459            $query = 'SELECT contact.ID as id,CONCAT(contact.zbsc_fname," ",contact.zbsc_lname) as name,contact.zbsc_created as created, contact.zbsc_email as email FROM ' . $ZBSCRM_t['contacts'] . ' as contact' . $joinQ;
1460        }
1461
1462        #} onlyColumns override
1463        if ( $onlyColumns && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
1464
1465            $columnStr = '';
1466            foreach ( $onlyColumnsFieldArr as $colDBKey => $colStr ) {
1467
1468                if ( ! empty( $columnStr ) ) {
1469                    $columnStr .= ',';
1470                }
1471                // this presumes str is db-safe? could do with sanitation?
1472                $columnStr .= $colDBKey;
1473
1474            }
1475
1476            $query = 'SELECT ' . $columnStr . ' FROM ' . $ZBSCRM_t['contacts'] . ' as contact' . $joinQ;
1477
1478        }
1479
1480        #} ============= WHERE ================
1481
1482        #} Add Search phrase
1483        if ( ! empty( $searchPhrase ) ) {
1484
1485            // inefficient searching all fields. Maybe get settings from user "which fields to search"
1486            // ... and auto compile for each contact ahead of time
1487            $searchWheres                    = array();
1488            $searchWheres['search_fullname'] = array( 'CONCAT(zbsc_prefix, " ", zbsc_fname, " ", zbsc_lname)', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1489            $searchWheres['search_fname']    = array( 'zbsc_fname', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1490            $searchWheres['search_lname']    = array( 'zbsc_lname', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1491            $searchWheres['search_email']    = array( 'zbsc_email', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1492
1493            // address elements
1494            $searchWheres['search_addr1']       = array( 'zbsc_addr1', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1495            $searchWheres['search_addr2']       = array( 'zbsc_addr2', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1496            $searchWheres['search_city']        = array( 'zbsc_city', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1497            $searchWheres['search_county']      = array( 'zbsc_county', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1498            $searchWheres['search_country']     = array( 'zbsc_country', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1499            $searchWheres['search_postcode']    = array( 'zbsc_postcode', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1500            $searchWheres['search_secaddr1']    = array( 'zbsc_secaddr1', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1501            $searchWheres['search_secaddr2']    = array( 'zbsc_secaddr2', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1502            $searchWheres['search_seccity']     = array( 'zbsc_seccity', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1503            $searchWheres['search_seccounty']   = array( 'zbsc_seccounty', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1504            $searchWheres['search_seccountry']  = array( 'zbsc_seccountry', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1505            $searchWheres['search_secpostcode'] = array( 'zbsc_secpostcode', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1506
1507            // social
1508            $searchWheres['search_tw'] = array( 'zbsc_tw', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1509            $searchWheres['search_li'] = array( 'zbsc_li', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1510            $searchWheres['search_fb'] = array( 'zbsc_fb', 'LIKE', '%s', '%' . $searchPhrase . '%' );
1511
1512            // phones
1513            // ultimately when search is refactored, we should probably store the "clean" version of the phone numbers in the database
1514            $searchWheres['search_hometel'] = array( 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(zbsc_hometel," ",""),".",""),"-",""),"(",""),")","")', 'LIKE', '%s', '%' . ( str_replace( array( ' ', '.', '-', '(', ')' ), '', $searchPhrase ) ) . '%' );
1515            $searchWheres['search_worktel'] = array( 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(zbsc_worktel," ",""),".",""),"-",""),"(",""),")","")', 'LIKE', '%s', '%' . ( str_replace( array( ' ', '.', '-', '(', ')' ), '', $searchPhrase ) ) . '%' );
1516            $searchWheres['search_mobtel']  = array( 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(zbsc_mobtel," ",""),".",""),"-",""),"(",""),")","")', 'LIKE', '%s', '%' . ( str_replace( array( ' ', '.', '-', '(', ')' ), '', $searchPhrase ) ) . '%' );
1517
1518            // We also add this, which finds AKA emails if using email
1519            $searchWheres['search_alias'] = array( 'ID', 'IN', '(SELECT aka_id FROM ' . $ZBSCRM_t['aka'] . ' WHERE aka_type = ' . ZBS_TYPE_CONTACT . ' AND aka_alias = %s)', $searchPhrase );
1520
1521            // 2.99.9.11 - Added ability to search custom fields (optionally)
1522            $customFieldSearch = zeroBSCRM_getSetting( 'customfieldsearch' );
1523            if ( $customFieldSearch == 1 ) {
1524
1525                // simplistic add
1526                // NOTE: This IGNORES ownership of custom field lines.
1527                // use FULLTEXT index if available (MySQL 5.6+), otherwise use fallback
1528                if ( jpcrm_migration_table_has_index( $ZBSCRM_t['customfields'], 'search' ) ) {
1529                    $searchWheres['search_customfields'] = array( 'ID', 'IN', '(SELECT zbscf_objid FROM ' . $ZBSCRM_t['customfields'] . ' WHERE MATCH(zbscf_objval) AGAINST(%s) AND zbscf_objtype = ' . ZBS_TYPE_CONTACT . ')', $searchPhrase );
1530                } else {
1531                    $searchWheres['search_customfields'] = array( 'ID', 'IN', '(SELECT zbscf_objid FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objval LIKE %s AND zbscf_objtype = ' . ZBS_TYPE_CONTACT . ')', '%' . $searchPhrase . '%' );
1532                }
1533            }
1534
1535            // also search "company name" where assigned
1536            $b2bMode = zeroBSCRM_getSetting( 'companylevelcustomers' );
1537            // OWNERSHIP TODO - next query doesn't USE OWNERSHIP!!!!:
1538            if ( $b2bMode == 1 ) {
1539                $searchWheres['incompanywithname'] = array( 'ID', 'IN', '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objtype_to = ' . ZBS_TYPE_COMPANY . ' AND zbsol_objid_to IN (SELECT ID FROM ' . $ZBSCRM_t['companies'] . ' WHERE zbsco_name LIKE %s))', '%' . $searchPhrase . '%' );
1540            }
1541
1542            // This generates a query like 'zbsc_fname LIKE %s OR zbsc_lname LIKE %s',
1543            // which we then need to include as direct subquery (below) in main query :)
1544            $searchQueryArr = $this->buildWheres( $searchWheres, '', array(), 'OR', false );
1545
1546            if ( is_array( $searchQueryArr ) && isset( $searchQueryArr['where'] ) && ! empty( $searchQueryArr['where'] ) ) {
1547
1548                // add it
1549                $wheres['direct'][] = array( '(' . $searchQueryArr['where'] . ')', $searchQueryArr['params'] );
1550
1551            }
1552        }
1553
1554        #} In company? #DB1LEGACY (TOMOVE -> where)
1555        if ( ! empty( $inCompany ) && $inCompany > 0 ) {
1556
1557            // would never hard-type this in (would make generic as in buildWPMetaQueryWhere)
1558            // but this is only here until MIGRATED to db2 globally
1559            // $wheres['incompany'] = array('ID','IN','(SELECT DISTINCT post_id FROM '.$wpdb->prefix."postmeta WHERE meta_key = 'zbs_company' AND meta_value = %d)",$inCompany);
1560            // Use obj links now
1561            $wheres['incompany'] = array( 'ID', 'IN', '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objtype_to = ' . ZBS_TYPE_COMPANY . ' AND zbsol_objid_to = %d)', $inCompany );
1562
1563        }
1564
1565        #} In array (if inCompany passed, this'll currently overwrite that?! (todo2.5))
1566        if ( is_array( $inArr ) && count( $inArr ) > 0 ) {
1567
1568            // clean for ints
1569            $inArrChecked = array();
1570            foreach ( $inArr as $x ) {
1571                $inArrChecked[] = (int) $x;
1572            }
1573
1574            // add where
1575            $wheres['inarray'] = array( 'ID', 'IN', '(' . implode( ',', $inArrChecked ) . ')' );
1576
1577        }
1578
1579        #} Owned by
1580        if ( ! empty( $ownedBy ) && $ownedBy > 0 ) {
1581
1582            // would never hard-type this in (would make generic as in buildWPMetaQueryWhere)
1583            // but this is only here until MIGRATED to db2 globally
1584            // $wheres['incompany'] = array('ID','IN','(SELECT DISTINCT post_id FROM '.$wpdb->prefix."postmeta WHERE meta_key = 'zbs_company' AND meta_value = %d)",$inCompany);
1585            // Use obj links now
1586            $wheres['ownedBy'] = array( 'zbs_owner', '=', '%s', $ownedBy );
1587
1588        }
1589
1590        // External sources
1591        if ( ! empty( $externalSource ) ) {
1592
1593            // NO owernship built into this, check when roll out multi-layered ownsership
1594            $wheres['externalsource'] = array( 'ID', 'IN', '(SELECT DISTINCT zbss_objid FROM ' . $ZBSCRM_t['externalsources'] . ' WHERE zbss_objtype = ' . ZBS_TYPE_CONTACT . ' AND zbss_source = %s)', $externalSource );
1595
1596        }
1597
1598        // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1599
1600        // quick addition for mike
1601        #} olderThan
1602        if ( ! empty( $olderThan ) && $olderThan > 0 ) {
1603            $wheres['olderThan'] = array( 'zbsc_created', '<=', '%d', $olderThan );
1604        }
1605        #} newerThan
1606        if ( ! empty( $newerThan ) && $newerThan > 0 ) {
1607            $wheres['newerThan'] = array( 'zbsc_created', '>=', '%d', $newerThan );
1608        }
1609
1610        // status
1611        if ( ! empty( $hasStatus ) ) {
1612            $wheres['hasStatus'] = array( 'zbsc_status', '=', '%s', $hasStatus );
1613        }
1614        if ( ! empty( $otherStatus ) ) {
1615            $wheres['otherStatus'] = array( 'zbsc_status', '<>', '%s', $otherStatus );
1616        }
1617
1618        #} contactedBefore
1619        if ( ! empty( $contactedBefore ) && $contactedBefore > 0 ) {
1620            $wheres['contactedBefore'] = array( 'zbsc_lastcontacted', '<=', '%d', $contactedBefore );
1621        }
1622        #} contactedAfter
1623        if ( ! empty( $contactedAfter ) && $contactedAfter > 0 ) {
1624            $wheres['contactedAfter'] = array( 'zbsc_lastcontacted', '>=', '%d', $contactedAfter );
1625        }
1626
1627        #} hasEmail
1628        if ( ! empty( $hasEmail ) ) {
1629            $wheres['hasEmail']      = array( 'zbsc_email', '=', '%s', $hasEmail );
1630            $wheres['hasEmailAlias'] = array( 'ID', 'IN', '(SELECT aka_id FROM ' . $ZBSCRM_t['aka'] . ' WHERE aka_type = ' . ZBS_TYPE_CONTACT . ' AND aka_alias = %s)', $hasEmail );
1631        }
1632
1633        #} inCounty
1634        if ( ! empty( $inCounty ) ) {
1635            $wheres['inCounty']      = array( 'zbsc_county', '=', '%s', $inCounty );
1636            $wheres['inCountyAddr2'] = array( 'zbsc_secaddrcounty', '=', '%s', $inCounty );
1637        }
1638        #} inPostCode
1639        if ( ! empty( $inPostCode ) ) {
1640            $wheres['inPostCode']      = array( 'zbsc_postcode', '=', '%s', $inPostCode );
1641            $wheres['inPostCodeAddr2'] = array( 'zbsc_secaddrpostcode', '=', '%s', $inPostCode );
1642        }
1643        #} inCountry
1644        if ( ! empty( $inCountry ) ) {
1645            $wheres['inCountry']      = array( 'zbsc_country', '=', '%s', $inCountry );
1646            $wheres['inCountryAddr2'] = array( 'zbsc_secaddrcountry', '=', '%s', $inCountry );
1647        }
1648        #} notInCounty
1649        if ( ! empty( $notInCounty ) ) {
1650            $wheres['notInCounty']      = array( 'zbsc_county', '<>', '%s', $notInCounty );
1651            $wheres['notInCountyAddr2'] = array( 'zbsc_secaddrcounty', '<>', '%s', $notInCounty );
1652        }
1653        #} notInPostCode
1654        if ( ! empty( $notInPostCode ) ) {
1655            $wheres['notInPostCode']      = array( 'zbsc_postcode', '<>', '%s', $notInPostCode );
1656            $wheres['notInPostCodeAddr2'] = array( 'zbsc_secaddrpostcode', '<>', '%s', $notInPostCode );
1657        }
1658        #} notInCountry
1659        if ( ! empty( $notInCountry ) ) {
1660            $wheres['notInCountry']      = array( 'zbsc_country', '<>', '%s', $notInCountry );
1661            $wheres['notInCountryAddr2'] = array( 'zbsc_secaddrcountry', '<>', '%s', $notInCountry );
1662        }
1663
1664        // generic obj links, e.g. quotes, invs, trans
1665        // e.g. contact(s) assigned to inv 123
1666        // Where the link relationship is OBJECT -> CONTACT
1667        if ( ! empty( $hasObjIDLinkedTo ) && $hasObjIDLinkedTo > 0 &&
1668                    ! empty( $hasObjTypeLinkedTo ) && $hasObjTypeLinkedTo > 0 ) {
1669            $wheres['hasObjIDLinkedTo'] = array( 'ID', 'IN', '(SELECT zbsol_objid_to FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = %d AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_from = %d AND zbsol_objid_to = contact.ID)', array( $hasObjTypeLinkedTo, $hasObjIDLinkedTo ) );
1670
1671        }
1672
1673        // generic obj links, e.g. companies
1674        // Where the link relationship is CONTACT -> OBJECT
1675        if ( ! empty( $isLinkedToObjID ) && $isLinkedToObjID > 0 &&
1676                    ! empty( $isLinkedToObjType ) && $isLinkedToObjType > 0 ) {
1677            $wheres['isLinkedToObjID'] = array( 'ID', 'IN', '(SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objtype_to = %d AND zbsol_objid_from = contact.ID AND zbsol_objid_to = %d)', array( $isLinkedToObjType, $isLinkedToObjID ) );
1678        }
1679        // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1680
1681        // Any additionalWhereArr?
1682        if ( is_array( $additionalWhereArr ) && count( $additionalWhereArr ) > 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1683
1684            // add em onto wheres (note these will OVERRIDE if using a key used above)
1685            // Needs to be multi-dimensional $wheres = array_merge($wheres,$additionalWhereArr);
1686            $wheres = array_merge_recursive( $wheres, $additionalWhereArr );
1687
1688        }
1689
1690        #} Quick filters - adapted from DAL1 (probs can be slicker)
1691        if ( is_array( $quickFilters ) && count( $quickFilters ) > 0 ) {
1692
1693            // cycle through
1694            foreach ( $quickFilters as $qFilter ) {
1695
1696                // where status = x
1697                // USE hasStatus above now...
1698                if ( str_starts_with( $qFilter, 'status_' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1699
1700                    $quick_filter_status         = substr( $qFilter, 7 ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1701                    $wheres['quickfilterstatus'] = array( 'zbsc_status', '=', 'convert(%s using utf8mb4) collate utf8mb4_bin', $quick_filter_status );
1702
1703                } elseif ( $qFilter === 'assigned_to_me' ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1704                    $wheres['assigned_to_me'] = array( 'zbs_owner', '=', zeroBSCRM_user() );
1705
1706                } elseif ( $qFilter === 'not_assigned' ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1707                    $wheres['not_assigned'] = array( 'zbs_owner', '<=', '0' );
1708
1709                } elseif ( str_starts_with( $qFilter, 'notcontactedin' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1710
1711                    // check
1712                    $notcontactedinDays        = (int) substr( $qFilter, 14 );
1713                    $notcontactedinDaysSeconds = $notcontactedinDays * 86400;
1714                    $wheres['notcontactedinx'] = array( 'zbsc_lastcontacted', '<', '%d', time() - $notcontactedinDaysSeconds );
1715
1716                } elseif ( str_starts_with( $qFilter, 'olderthan' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1717
1718                    // check
1719                    $olderThanDays        = (int) substr( $qFilter, 9 );
1720                    $olderThanDaysSeconds = $olderThanDays * 86400;
1721                    $wheres['olderthanx'] = array( 'zbsc_created', '<', '%d', time() - $olderThanDaysSeconds );
1722
1723                } elseif ( str_starts_with( $qFilter, 'segment_' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1724
1725                    // a SEGMENT
1726                    $qFilterSegmentSlug = substr( $qFilter, 8 );
1727
1728                    #} Retrieve segment + conditions
1729                    $segment    = $this->DAL()->segments->getSegmentBySlug( $qFilterSegmentSlug, true, false );
1730                    $conditions = array();
1731                    if ( isset( $segment['conditions'] ) ) {
1732                        $conditions = $segment['conditions'];
1733                    }
1734                    $matchType = 'all';
1735                    if ( isset( $segment['matchtype'] ) ) {
1736                        $matchType = $segment['matchtype'];
1737                    }
1738
1739                    // retrieve getContacts arguments from a list of segment conditions
1740                    // as at launch of segments (26/6/18) - these are all $additionalWhere args
1741                    // ... if it stays that way, this is nice and simple, so going to proceed with that.
1742                    // be aware if $this->segmentConditionArgs() changes, will affect this.
1743                    $contactGetArgs = $this->DAL()->segments->segmentConditionsToArgs( $conditions, $matchType );
1744
1745                    // as at above, contactGetArgs should have this:
1746                    if ( isset( $contactGetArgs['additionalWhereArr'] ) && is_array( $contactGetArgs['additionalWhereArr'] ) ) {
1747
1748                        // This was required to work with OR and AND situs, along with the usual getContacts vars as well
1749                        // -----------------------
1750                        // match type ALL is default, this switches to ANY
1751                        $segmentOperator = 'AND';
1752                        if ( $matchType == 'one' ) {
1753                            $segmentOperator = 'OR';
1754                        }
1755
1756                        // This generates a query like 'zbsc_fname LIKE %s OR/AND zbsc_lname LIKE %s',
1757                        // which we then need to include as direct subquery (below) in main query :)
1758                        $segmentQueryArr = $this->buildWheres( $contactGetArgs['additionalWhereArr'], '', array(), $segmentOperator, false );
1759
1760                        if ( is_array( $segmentQueryArr ) && isset( $segmentQueryArr['where'] ) && ! empty( $segmentQueryArr['where'] ) ) {
1761
1762                            // add it
1763                            $wheres['direct'][] = array( '(' . $segmentQueryArr['where'] . ')', $segmentQueryArr['params'] );
1764
1765                        }
1766                        // -----------------------
1767
1768                        // following didn't work for OR situations: (worked for most situations though, is a shame)
1769                        // -----------------------
1770                        // so we MERGE that into our wheres... :o
1771                        // this'll override any settings above.
1772                        // Needs to be multi-dimensional
1773                        // $wheres = array_merge_recursive($wheres,$contactGetArgs['additionalWhereArr']);
1774                        // -----------------------
1775
1776                    } elseif ( ! empty( $contactGetArgs['additional_joins'] ) && is_array( $contactGetArgs['additional_joins'] ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,
1777
1778                        list( $join_sql, $join_params ) = $this->DAL()->build_joins( $contactGetArgs['additional_joins'], $matchType === 'all' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1779                    }
1780                } else {
1781
1782                    // normal/hardtyped
1783
1784                    switch ( $qFilter ) {
1785
1786                        case 'lead':
1787                            // hack "leads only" - adapted from DAL1 (probs can be slicker)
1788                            $wheres['quickfilterlead'] = array( 'zbsc_status', 'LIKE', '%s', 'Lead' );
1789
1790                            break;
1791
1792                        case 'customer':
1793                            // hack - adapted from DAL1 (probs can be slicker)
1794                            $wheres['quickfiltercustomer'] = array( 'zbsc_status', 'LIKE', '%s', 'Customer' );
1795
1796                            break;
1797
1798                        default:
1799                            // if we've hit no filter query, let external logic hook in to provide alternatives
1800                            // First used in WooSync module
1801                            $wheres = apply_filters( 'jpcrm_contact_query_quickfilter', $wheres, $qFilter );
1802
1803                            break;
1804
1805                    }  // / switch
1806
1807                } // / hardtyped
1808
1809            }
1810        } // / quickfilters
1811
1812        #} Is Tagged (expects 1 tag ID OR array)
1813
1814        // catch 1 item arr
1815        if ( is_array( $isTagged ) && count( $isTagged ) == 1 ) {
1816            $isTagged = $isTagged[0];
1817        }
1818
1819        if ( ! empty( $isTagged ) && ! is_array( $isTagged ) && $isTagged > 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1820
1821            // add where tagged
1822            // 1 int:
1823            $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = contact.ID AND zbstl_tagid = %d) > 0)', array( ZBS_TYPE_CONTACT, $isTagged ) );
1824
1825        } elseif ( is_array( $isTagged ) && count( $isTagged ) > 0 ) {
1826
1827            // foreach in array :)
1828            $tagStr = '';
1829            foreach ( $isTagged as $iTag ) {
1830                $i = (int) $iTag;
1831                if ( $i > 0 ) {
1832
1833                    if ( $tagStr !== '' ) {
1834                        $tagStr . ',';
1835                    }
1836                    $tagStr .= $i;
1837                }
1838            }
1839            if ( ! empty( $tagStr ) ) {
1840
1841                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = contact.ID AND zbstl_tagid IN (%s)) > 0)', array( ZBS_TYPE_CONTACT, $tagStr ) );
1842
1843            }
1844        }
1845        #} Is NOT Tagged (expects 1 tag ID OR array)
1846
1847        // catch 1 item arr
1848        if ( is_array( $isNotTagged ) && count( $isNotTagged ) == 1 ) {
1849            $isNotTagged = $isNotTagged[0];
1850        }
1851
1852        if ( ! empty( $isNotTagged ) && ! is_array( $isNotTagged ) && $isNotTagged > 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1853
1854            // add where tagged
1855            // 1 int:
1856            $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = contact.ID AND zbstl_tagid = %d) = 0)', array( ZBS_TYPE_CONTACT, $isNotTagged ) );
1857
1858        } elseif ( is_array( $isNotTagged ) && count( $isNotTagged ) > 0 ) {
1859
1860            // foreach in array :)
1861            $tagStr = '';
1862            foreach ( $isNotTagged as $iTag ) {
1863                $i = (int) $iTag;
1864                if ( $i > 0 ) {
1865
1866                    if ( $tagStr !== '' ) {
1867                        $tagStr . ',';
1868                    }
1869                    $tagStr .= $i;
1870                }
1871            }
1872            if ( ! empty( $tagStr ) ) {
1873
1874                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = contact.ID AND zbstl_tagid IN (%s)) = 0)', array( ZBS_TYPE_CONTACT, $tagStr ) );
1875
1876            }
1877        }
1878
1879        #} ============ / WHERE ===============
1880
1881        #} ============   SORT   ==============
1882
1883        // latest log
1884        // Latest Contact Log (as sort) needs an additional SQL where str:
1885        $contact_log_types_str = '';
1886        $sort_function         = 'MAX';
1887        if ( $sortOrder !== 'DESC' ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1888            $sort_function = 'MIN';
1889        }
1890        // @phan-suppress-next-line PhanImpossibleCondition -- This var is initialized by arbitrary data in $args.
1891        if ( $withLastLog ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1892
1893            // retrieve log types to include
1894            $contact_log_types = $zbs->DAL->logs->contact_log_types; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1895
1896            // build sql
1897            if ( is_array( $contact_log_types ) ) {
1898                // create escaped csv
1899                $contact_log_types_str = $this->build_csv( $contact_log_types );
1900            }
1901        }
1902
1903        // include invoices without deleted status in the total value for invoices_total_inc_deleted:
1904        $inv_status_query_add = $this->DAL()->invoices->get_invoice_status_except_deleted_for_query();
1905
1906        // Mapped sorts
1907        // This catches listview and other specific sort cases
1908        // Note: Prefix here is a legacy leftover from the fact the AJAX List view retrieve goes through zeroBS_getCustomers() which prefixes zbsc_
1909        $sort_map = array(
1910            'zbsc_id'                       => 'ID',
1911            'zbsc_owner'                    => 'zbs_owner',
1912            'zbsc_zbs_owner'                => 'zbs_owner',
1913
1914            // company (name)
1915            'zbsc_company'                  => '(SELECT zbsco_name FROM ' . $ZBSCRM_t['companies'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_to FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objtype_to = ' . ZBS_TYPE_COMPANY . ' AND zbsol_objid_from = contact.ID) ORDER BY zbsco_name ' . $sortOrder . ' LIMIT 0,1)', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1916
1917            // sort by subquery: Logs
1918            // sort by latest log is effectively 'sort by last log added'
1919            'zbsc_latestlog'                => '(SELECT ' . $sort_function . '(zbsl_created) FROM ' . $ZBSCRM_t['logs'] . ' WHERE zbsl_objid = contact.ID AND zbsl_objtype = ' . ZBS_TYPE_CONTACT . ')', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1920            // sort by latest contact log is effectively 'sort by last contact log added' (requires $withLastLog = true)
1921            'zbsc_lastcontacted'            => '(SELECT ' . $sort_function . '(zbsl_created) FROM ' . $ZBSCRM_t['logs'] . ' WHERE zbsl_objid = contact.ID AND zbsl_objtype = ' . ZBS_TYPE_CONTACT . ' AND zbsl_type IN (' . $contact_log_types_str . '))', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1922
1923            // has & counts (same queries)
1924            // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1925            'zbsc_hasquote'                 => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['quotes'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID))',
1926            'zbsc_hasinvoice'               => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID))',
1927            'zbsc_hastransaction'           => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['transactions'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID))',
1928            'zbsc_quotecount'               => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['quotes'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID))',
1929            'zbsc_invoicecount_inc_deleted' => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID))',
1930            'zbsc_invoicecount'             => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID)' . $inv_status_query_add . ')',
1931            'zbsc_transactioncount'         => '(SELECT COUNT(ID) FROM ' . $ZBSCRM_t['transactions'] . ' WHERE ID IN (SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = contact.ID))',
1932            // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1933            // following will only work if obj total value subqueries triggered above ^
1934            'zbsc_totalvalue'               => '((IFNULL(invoices_total,0) + IFNULL(transactions_total,0)) - IFNULL(transactions_paid_total,0))', // custom sort by total invoice value + transaction value - paid transactions (as mimicking tidy_contact php logic into SQL)
1935            'zbsc_transactiontotal'         => 'transactions_total',
1936            'zbsc_quotetotal'               => 'quotes_total',
1937            'zbsc_invoicetotal'             => 'invoices_total',
1938        );
1939
1940        // either from $sort_map, or multi-dimensional name search
1941        if ( array_key_exists( $sortByField, $sort_map ) ) {
1942
1943            $sortByField = $sort_map[ $sortByField ];
1944
1945        } elseif ( $sortByField === 'zbsc_fullname' || $sortByField === 'fullname' ) {
1946
1947            $sortByField = array(
1948                'zbsc_lname' => $sortOrder,
1949                'zbsc_fname' => $sortOrder,
1950            );
1951
1952        }
1953
1954        #} ============ / SORT   ==============
1955
1956        #} CHECK this + reset to default if faulty
1957        if ( ! in_array( $whereCase, array( 'AND', 'OR' ) ) ) {
1958            $whereCase = 'AND';
1959        }
1960
1961        #} Build out any WHERE clauses
1962        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params, $whereCase );
1963        $whereStr  = $wheresArr['where'];
1964        $params    = $params + $wheresArr['params'];
1965        #} / Build WHERE
1966
1967        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
1968        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
1969        $ownQ   = $this->ownershipSQL( $ignoreowner, 'contact' );
1970        if ( ! empty( $ownQ ) ) {
1971            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
1972        }
1973        #} / Ownership
1974
1975        $query .= $join_sql;
1976
1977        if ( ! empty( $join_sql ) ) {
1978            $params = array_merge( $params, $join_params );
1979        }
1980
1981        #} Append to sql (this also automatically deals with sortby and paging)
1982        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
1983
1984        try {
1985
1986            // Prep & run query
1987            $queryObj = $this->prepare( $query, $params );
1988
1989            // Catch count + return if requested
1990            if ( $count ) {
1991                return $wpdb->get_var( $queryObj );
1992            }
1993
1994            // Totals override
1995            // This is non-performant, and shouldn't run when we've got extra joins (e.g. from segments).
1996            if ( $onlyObjTotals && empty( $join_sql ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1997
1998                $contact_query = 'SELECT contact.ID FROM ' . $ZBSCRM_t['contacts'] . ' AS contact' . $joinQ . $this->buildWhereStr( $whereStr, $additionalWhere ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1999                $contact_query = $this->prepare( $contact_query, $params );
2000
2001                $query = 'SELECT ';
2002
2003                if ( zeroBSCRM_getSetting( 'feat_quotes' ) == 1 ) {
2004
2005                    $query .= '(SELECT SUM(q.zbsq_value)
2006                                FROM ' . $ZBSCRM_t['quotes'] . ' AS q
2007                                INNER JOIN ' . $ZBSCRM_t['objlinks'] . ' AS ol
2008                                ON q.ID = ol.zbsol_objid_from
2009                                WHERE
2010                                ol.zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . '
2011                                AND ol.zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . '
2012                                AND ol.zbsol_objid_to IN ( ' . $contact_query . ' )) AS quotes_total';
2013
2014                }
2015
2016                if ( zeroBSCRM_getSetting( 'feat_invs' ) == 1 ) {
2017
2018                    // if previous query, add comma
2019                    if ( $query !== 'SELECT ' ) {
2020                        $query .= ', ';
2021                    }
2022
2023                    // include invoices without deleted status in the total value for invoices_total_inc_deleted:
2024                    $inv_status_query_add = $this->DAL()->invoices->get_invoice_status_except_deleted_for_query();
2025
2026                    // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2027                    $query .= '(SELECT SUM(i.zbsi_total)
2028                            FROM ' . $ZBSCRM_t['invoices'] . ' AS i
2029                            INNER JOIN ' . $ZBSCRM_t['objlinks'] . ' AS ol
2030                            ON i.ID = ol.zbsol_objid_from
2031                            WHERE
2032                            ol.zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . '
2033                            AND ol.zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . '
2034                            AND ol.zbsol_objid_to IN ( ' . $contact_query . ' ) ' . $inv_status_query_add . ') AS invoices_total,';
2035
2036                    $query .= '(SELECT SUM(inc_deleted_invoices.zbsi_total)
2037                            FROM ' . $ZBSCRM_t['invoices'] . ' AS inc_deleted_invoices
2038                            INNER JOIN ' . $ZBSCRM_t['objlinks'] . ' AS ol
2039                            ON inc_deleted_invoices.ID = ol.zbsol_objid_from
2040                            WHERE
2041                            ol.zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . '
2042                            AND ol.zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . '
2043                            AND ol.zbsol_objid_to IN ( ' . $contact_query . ' )) AS invoices_total_inc_deleted';
2044                    // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2045                }
2046
2047                if ( zeroBSCRM_getSetting( 'feat_transactions' ) == 1 ) {
2048
2049                    // if previous query, add comma
2050                    if ( $query !== 'SELECT ' ) {
2051                        $query .= ', ';
2052                    }
2053
2054                    // only include transactions with statuses which should be included in total value:
2055                    $transaction_status_query_addition = $this->DAL()->transactions->getTransactionStatusesToIncludeQuery();
2056
2057                    $query .= '(SELECT SUM(t.zbst_total)
2058                                FROM ' . $ZBSCRM_t['transactions'] . ' AS t
2059                                INNER JOIN ' . $ZBSCRM_t['objlinks'] . ' AS ol
2060                                ON t.ID = ol.zbsol_objid_from
2061                                WHERE
2062                                ol.zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . '
2063                                AND ol.zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . '
2064                                AND ol.zbsol_objid_to IN ( ' . $contact_query . ' ) ' . $transaction_status_query_addition . ') AS transactions_total, ';
2065
2066                    $query .= '(SELECT SUM(assigned_transactions.zbst_total)
2067                                FROM ' . $ZBSCRM_t['transactions'] . ' AS assigned_transactions
2068                                WHERE assigned_transactions.ID IN
2069                                (
2070                                    SELECT DISTINCT zbsol_objid_from
2071                                    FROM ' . $ZBSCRM_t['objlinks'] . '
2072                                    WHERE
2073                                    zbsol_objtype_from = ' . ZBS_TYPE_TRANSACTION . '
2074                                    AND zbsol_objtype_to = ' . ZBS_TYPE_INVOICE . '
2075                                    AND zbsol_objid_to IN
2076                                    (
2077                                        SELECT DISTINCT zbsol_objid_from
2078                                        FROM ' . $ZBSCRM_t['objlinks'] . '
2079                                        WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND
2080                                        zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND
2081                                        zbsol_objid_to IN ( ' . $contact_query . ' )
2082                                    )
2083                                )) AS assigned_transactions_total';
2084
2085                }
2086
2087                if ( $query !== 'SELECT ' ) {
2088
2089                    $totals_data = $wpdb->get_row( $query );
2090
2091                    if ( zeroBSCRM_getSetting( 'feat_invs' ) == 1 && zeroBSCRM_getSetting( 'feat_transactions' ) == 1 ) {
2092
2093                        // calculate a total sum (invoices + unassigned transactions)
2094                        $totals_data->total_sum = (float) $totals_data->invoices_total + (float) $totals_data->transactions_total - (float) $totals_data->assigned_transactions_total;
2095                        // total_sum_inc_deleted currently factors in deleted invoices
2096                        $totals_data->total_sum_inc_deleted = (float) $totals_data->invoices_total_inc_deleted + (float) $totals_data->transactions_total - (float) $totals_data->assigned_transactions_total;
2097
2098                    } elseif ( zeroBSCRM_getSetting( 'feat_invs' ) == 1 ) {
2099
2100                        // just include invoices in total
2101                        $totals_data->total_sum             = (float) $totals_data->invoices_total;
2102                        $totals_data->total_sum_inc_deleted = (float) $totals_data->invoices_total_inc_deleted;
2103
2104                    } elseif ( zeroBSCRM_getSetting( 'feat_quotes' ) == 1 ) {
2105
2106                        // just include quotes in total
2107                        $totals_data->total_sum = (float) $totals_data->quotes_total;
2108
2109                    }
2110
2111                    // provide formatted equivilents
2112                    if ( zeroBSCRM_getSetting( 'feat_quotes' ) == 1 ) {
2113
2114                        $totals_data->quotes_total_formatted = zeroBSCRM_formatCurrency( $totals_data->quotes_total );
2115
2116                    }
2117                    if ( zeroBSCRM_getSetting( 'feat_invs' ) == 1 ) {
2118
2119                        $totals_data->invoices_total_formatted = zeroBSCRM_formatCurrency( $totals_data->invoices_total );
2120
2121                    }
2122                    if ( zeroBSCRM_getSetting( 'feat_transactions' ) == 1 ) {
2123
2124                        $totals_data->transactions_total_formatted          = zeroBSCRM_formatCurrency( $totals_data->transactions_total );
2125                        $totals_data->assigned_transactions_total_formatted = zeroBSCRM_formatCurrency( $totals_data->assigned_transactions_total );
2126
2127                    }
2128
2129                    if ( isset( $totals_data->total_sum ) ) {
2130
2131                        $totals_data->total_sum_formatted = zeroBSCRM_formatCurrency( $totals_data->total_sum );
2132
2133                    }
2134                } else {
2135
2136                    $totals_data = null;
2137
2138                }
2139                return $totals_data;
2140
2141            }
2142
2143            #} else continue..
2144            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
2145
2146        } catch ( Exception $e ) {
2147
2148            #} General SQL Err
2149            $this->catchSQLError( $e );
2150
2151        }
2152
2153        #} Interpret results (Result Set - multi-row)
2154        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
2155
2156            #} Has results, tidy + return
2157            foreach ( $potentialRes as $resDataLine ) {
2158
2159                #} simplified override
2160                if ( $simplified ) {
2161
2162                    $resArr = array(
2163                        'id'      => $resDataLine->id,
2164                        'name'    => $resDataLine->name,
2165                        'created' => $resDataLine->created,
2166                        'email'   => $resDataLine->email,
2167                    );
2168
2169                } elseif ( $onlyColumns && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
2170
2171                    // only coumns return.
2172                    $resArr = array();
2173                    foreach ( $onlyColumnsFieldArr as $colDBKey => $colStr ) {
2174
2175                        if ( isset( $resDataLine->$colDBKey ) ) {
2176                            $resArr[ $colStr ] = $resDataLine->$colDBKey;
2177                        }
2178                    }
2179                } else {
2180
2181                    // tidy (normal)
2182                    $resArr = $this->tidy_contact( $resDataLine, $withCustomFields );
2183
2184                }
2185
2186                // Checking and fixing name clashes between custom fields and linked objects
2187                // (e.g. custom field with slug `company` and the company linked object)
2188                // See: https://github.com/Automattic/zero-bs-crm/issues/3477
2189                $this->add_name_clash_suffix_if_needed(
2190                    $resArr, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2191                    array(
2192                        'tags',
2193                        'dnd',
2194                        'company',
2195                        'lastlog',
2196                        'owner',
2197                        'invoices',
2198                        'quotes',
2199                        'transactions',
2200                        'tasks',
2201                        'external_sources',
2202                    )
2203                );
2204
2205                if ( $withTags ) {
2206
2207                    // add all tags lines
2208                    $resArr['tags'] = $this->DAL()->getTagsForObjID(
2209                        array(
2210                            'objtypeid' => ZBS_TYPE_CONTACT,
2211                            'objid'     => $resDataLine->ID,
2212                        )
2213                    );
2214
2215                }
2216
2217                if ( $withDND ) {
2218
2219                    // retrieve :) (paranoia mode)
2220                    $dnd          = -1;
2221                    $potentialDND = $this->stripSlashes( $this->decodeIfJSON( $resDataLine->dnd ) );
2222                    if ( $potentialDND == '1' ) {
2223                        $dnd = 1;
2224                    }
2225
2226                    $resArr['dnd'] = $dnd;
2227                }
2228
2229                // ===================================================
2230                // ========== #} #DB1LEGACY (TOMOVE)
2231                // == Following is all using OLD DB stuff, here until we migrate inv etc.
2232                // ===================================================
2233
2234                #} With most recent log? #DB1LEGACY (TOMOVE)
2235                if ( $withLastLog ) {
2236
2237                    // doesn't return singular, for now using arr
2238                    $potentialLogs = $this->DAL()->logs->getLogsForObj(
2239                        array(
2240
2241                            'objtype'     => ZBS_TYPE_CONTACT,
2242                            'objid'       => $resDataLine->ID,
2243
2244                            'incMeta'     => true,
2245
2246                            'sortByField' => 'zbsl_created',
2247                            'sortOrder'   => 'DESC',
2248                            'page'        => 0,
2249                            'perPage'     => 1,
2250
2251                        )
2252                    );
2253
2254                    if ( is_array( $potentialLogs ) && count( $potentialLogs ) > 0 ) {
2255                        $resArr['lastlog'] = $potentialLogs[0];
2256                    }
2257
2258                    // CONTACT logs specifically
2259                    // doesn't return singular, for now using arr
2260                    $potentialLogs = $this->DAL()->logs->getLogsForObj( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2261                        array(
2262
2263                            'objtype'     => ZBS_TYPE_CONTACT,
2264                            'objid'       => $resDataLine->ID, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2265
2266                            'notetypes'   => $zbs->DAL->logs->contact_log_types, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2267
2268                            'incMeta'     => true,
2269
2270                            'sortByField' => 'zbsl_created',
2271                            'sortOrder'   => 'DESC',
2272                            'page'        => 0,
2273                            'perPage'     => 1,
2274
2275                        )
2276                    );
2277
2278                    if ( is_array( $potentialLogs ) && count( $potentialLogs ) > 0 ) {
2279                        $resArr['lastcontactlog'] = $potentialLogs[0];
2280                    }
2281                }
2282
2283                #} With Assigned?
2284                if ( $withOwner ) {
2285
2286                    $resArr['owner'] = zeroBS_getOwner( $resDataLine->ID, true, 'zerobs_customer', $resDataLine->zbs_owner );
2287
2288                }
2289
2290                if ( $withAssigned ) {
2291
2292                    /*
2293                    This is for MULTIPLE (e.g. multi companies assigned to a contact)
2294
2295                        // add all assigned companies
2296                        $res['companies'] = $this->DAL()->companies->getCompanies(array(
2297                    'hasObjTypeLinkedTo'=>ZBS_TYPE_INVOICE,
2298                    'hasObjIDLinkedTo'=>$resDataLine->ID,
2299                    'perPage'=>-1,
2300                    'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_COMPANY)));
2301
2302                    .. but we use 1:1, at least now: */
2303
2304                    // add all assigned companies
2305                    $resArr['company'] = $zbs->DAL->companies->getCompanies(
2306                        array(
2307                            'hasObjTypeLinkedTo' => ZBS_TYPE_CONTACT,
2308                            'hasObjIDLinkedTo'   => $resDataLine->ID,
2309                            'page'               => 0,
2310                            'perPage'            => 1, // FORCES 1
2311                            'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
2312                        )
2313                    );
2314
2315                }
2316
2317                if ( $withInvoices ) {
2318
2319                    #} only gets first 100?
2320                    // DAL3 ver, more perf, gets all
2321                    $resArr['invoices'] = $zbs->DAL->invoices->getInvoices(
2322                        array(
2323
2324                            'assignedContact' => $resDataLine->ID, // assigned to company id (int)
2325                            'page'            => -1,
2326                            'perPage'         => -1,
2327                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_INVOICE ),
2328                            'sortByField'     => 'ID',
2329                            'sortOrder'       => 'DESC',
2330
2331                        )
2332                    );
2333
2334                }
2335
2336                if ( $withQuotes ) {
2337
2338                    // DAL3 ver, more perf, gets all
2339                    $resArr['quotes'] = $zbs->DAL->quotes->getQuotes(
2340                        array(
2341
2342                            'assignedContact' => $resDataLine->ID, // assigned to company id (int)
2343                            'page'            => -1,
2344                            'perPage'         => -1,
2345                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_QUOTE ),
2346                            'sortByField'     => 'ID',
2347                            'sortOrder'       => 'DESC',
2348
2349                        )
2350                    );
2351
2352                }
2353
2354                #} ... brutal for mvp #DB1LEGACY (TOMOVE)
2355                if ( $withTransactions ) {
2356
2357                    // DAL3 ver, more perf, gets all
2358                    $resArr['transactions'] = $zbs->DAL->transactions->getTransactions(
2359                        array(
2360
2361                            'assignedContact' => $resDataLine->ID, // assigned to company id (int)
2362                            'page'            => -1,
2363                            'perPage'         => -1,
2364                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_TRANSACTION ),
2365                            'sortByField'     => 'ID',
2366                            'sortOrder'       => 'DESC',
2367
2368                        )
2369                    );
2370
2371                }
2372
2373                #} ... brutal for mvp #DB1LEGACY (TOMOVE)
2374                if ( $withTasks ) {
2375
2376                    // DAL3 ver, more perf, gets all
2377                    $resArr['tasks'] = $zbs->DAL->events->getEvents(
2378                        array(
2379
2380                            'assignedContact' => $resDataLine->ID, // assigned to company id (int)
2381                            'page'            => -1,
2382                            'perPage'         => -1,
2383                            'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_TASK ),
2384                            'sortByField'     => 'zbse_start',
2385                            'sortOrder'       => 'DESC',
2386                            'withAssigned'    => false, // no need, it's assigned to this obj already
2387
2388                        )
2389                    );
2390
2391                }
2392
2393                // simplistic, could be optimised (though low use means later.)
2394                if ( $withExternalSources ) {
2395
2396                    $resArr['external_sources'] = $zbs->DAL->contacts->getExternalSourcesForContact(
2397                        array(
2398
2399                            'contactID'   => $resDataLine->ID,
2400
2401                            'sortByField' => 'ID',
2402                            'sortOrder'   => 'ASC',
2403                            'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
2404
2405                        )
2406                    );
2407
2408                }
2409                if ( $withExternalSourcesGrouped ) {
2410
2411                    $resArr['external_sources'] = $zbs->DAL->getExternalSources(
2412                        -1,
2413                        array(
2414
2415                            'objectID'          => $resDataLine->ID,
2416                            'objectType'        => ZBS_TYPE_CONTACT,
2417                            'grouped_by_source' => true,
2418                            'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
2419
2420                        )
2421                    );
2422
2423                }
2424
2425                // }
2426
2427                // ===================================================
2428                // ========== / #DB1LEGACY (TOMOVE)
2429                // ===================================================
2430
2431                $res[] = $resArr;
2432
2433            }
2434        }
2435
2436        return $res;
2437    }
2438
2439    /**
2440     * adds or updates a contact object
2441     *
2442     * @param array $args Associative array of arguments
2443     *              id (if update), owner, data (array of field data)
2444     *
2445     * @return int line ID
2446     */
2447    public function addUpdateContact( $args = array() ) {
2448
2449        global $ZBSCRM_t, $wpdb, $zbs;
2450
2451        #} Retrieve any cf
2452        $customFields     = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_CONTACT ) );
2453        $addrCustomFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_ADDRESS ) );
2454
2455        #} ============ LOAD ARGS =============
2456        $defaultArgs = array(
2457
2458            'id'                   => -1,
2459            'owner'                => -1,
2460
2461            // fields (directly)
2462            'data'                 => array(
2463
2464                'email'           => '', // Unique Field !
2465
2466                'status'          => '',
2467                'prefix'          => '',
2468                'fname'           => '',
2469                'lname'           => '',
2470                'addr1'           => '',
2471                'addr2'           => '',
2472                'city'            => '',
2473                'county'          => '',
2474                'country'         => '',
2475                'postcode'        => '',
2476                'secaddr1'        => '',
2477                'secaddr2'        => '',
2478                'seccity'         => '',
2479                'seccounty'       => '',
2480                'seccountry'      => '',
2481                'secpostcode'     => '',
2482                'hometel'         => '',
2483                'worktel'         => '',
2484                'mobtel'          => '',
2485                'wpid'            => -1,
2486                'avatar'          => '',
2487
2488                // social basics :)
2489                'tw'              => '',
2490                'fb'              => '',
2491                'li'              => '',
2492
2493                // Note Custom fields may be passed here, but will not have defaults so check isset()
2494
2495                // tags
2496                'tags'            => -1, // pass an array of tag ids or tag strings
2497                'tag_mode'        => 'replace', // replace|append|remove
2498
2499                'externalSources' => -1, // if this is an array(array('source'=>src,'uid'=>uid),multiple()) it'll add :)
2500
2501                'companies'       => -1, // array of co id's :)
2502
2503                // wh added for later use.
2504                'lastcontacted'   => -1,
2505                // allow this to be set for MS sync etc.
2506                'created'         => -1,
2507
2508                // add/update aliases
2509                'aliases'         => -1, // array of email strings (will be verified)
2510
2511            ),
2512
2513            'limitedFields'        => -1, // if this is set it OVERRIDES data (allowing you to set specific fields + leave rest in tact)
2514            // ^^ will look like: array(array('key'=>x,'val'=>y,'type'=>'%s'))
2515
2516            // this function as DAL1 func did.
2517            'extraMeta'            => -1,
2518            'automatorPassthrough' => -1,
2519            'fallBackLog'          => -1,
2520
2521            'silentInsert'         => false, // this was for init Migration - it KILLS all IA for newContact (because is migrating, not creating new :) this was -1 before
2522
2523            'do_not_update_blanks' => false, // this allows you to not update fields if blank (same as fieldoverride for extsource -> in)
2524
2525        );
2526        foreach ( $defaultArgs as $argK => $argV ) {
2527            $$argK = $argV;
2528            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2529                if ( is_array( $args[ $argK ] ) ) {
2530                    $newData = $$argK;
2531                    if ( ! is_array( $newData ) ) {
2532                        $newData = array();
2533                    }
2534                    foreach ( $args[ $argK ] as $subK => $subV ) {
2535                        $newData[ $subK ] = $subV;
2536                    }
2537                    $$argK = $newData;
2538                } else {
2539                    $$argK = $args[ $argK ];
2540                }
2541            }
2542        }
2543
2544        // Needs this to grab custom fields (if passed) too :)
2545        if ( is_array( $customFields ) ) {
2546            foreach ( $customFields as $cK => $cF ) {
2547
2548                // only for data, limited fields below
2549                if ( is_array( $data ) ) {
2550
2551                    if ( isset( $args['data'][ $cK ] ) ) {
2552                        $data[ $cK ] = $args['data'][ $cK ];
2553                    }
2554                }
2555            }
2556        }
2557
2558            /*
2559            NOT REQ: // Needs this to grab custom addr fields (if passed) too :)
2560            if ( is_array( $addrCustomFields)) foreach ( $addrCustomFields as $cK => $cF){
2561
2562                // only for data, limited fields below
2563                if ( is_array( $data) ) {
2564
2565                    //if (isset($args['data'][$cK])) $data[$cK] = $args['data'][$cK];
2566
2567                }
2568
2569            } */
2570
2571            // this takes limited fields + checks through for custom fields present
2572            // (either as key zbsc_source or source, for example)
2573            // then switches them into the $data array, for separate update
2574            // where this'll fall over is if NO normal contact data is sent to update, just custom fields
2575        if ( is_array( $limitedFields ) && is_array( $customFields ) ) {
2576
2577                // $customFieldKeys = array_keys($customFields);
2578                $newLimitedFields = array();
2579
2580                // cycle through
2581            foreach ( $limitedFields as $field ) {
2582
2583                // some weird case where getting empties, so added check
2584                if ( isset( $field['key'] ) && ! empty( $field['key'] ) ) {
2585
2586                    $dePrefixed = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2587                    if ( str_starts_with( $field['key'], 'zbsc_' ) ) {
2588                        $dePrefixed = substr( $field['key'], strlen( 'zbsc_' ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2589                    }
2590
2591                    if ( isset( $customFields[ $field['key'] ] ) ) {
2592
2593                        // is custom, move to data
2594                        $data[ $field['key'] ] = $field['val'];
2595
2596                    } elseif ( ! empty( $dePrefixed ) && isset( $customFields[ $dePrefixed ] ) ) {
2597
2598                        // is custom, move to data
2599                        $data[ $dePrefixed ] = $field['val'];
2600
2601                    } else {
2602
2603                        // add it to limitedFields (it's not dealt with post-update)
2604                        $newLimitedFields[] = $field;
2605                    }
2606                }
2607            }
2608
2609                // move this back in
2610                $limitedFields = $newLimitedFields;
2611                unset( $newLimitedFields );
2612
2613        }
2614
2615        #} =========== / LOAD ARGS ============
2616
2617        #} ========== CHECK FIELDS ============
2618
2619        $id = (int) $id;
2620
2621        // here we check that the potential owner CAN even own
2622        if (
2623                // specified owner is not an admin
2624                ! user_can( $owner, 'admin_zerobs_usr' )
2625            ) {
2626            $owner = -1;
2627        }
2628
2629        if ( is_array( $limitedFields ) ) {
2630
2631            // LIMITED UPDATE (only a few fields.)
2632            // phpcs:ignore
2633            if ( count( $limitedFields ) === 0 ) {
2634                return false;
2635            }
2636            // REQ. ID too (can only update)
2637            if ( empty( $id ) || $id <= 0 ) {
2638                return false;
2639            }
2640        } else {
2641
2642                // NORMAL, FULL UPDATE
2643
2644                // check email + load that user if present
2645            if ( ! isset( $data['email'] ) || empty( $data['email'] ) ) {
2646
2647                // no email
2648                // Allow users without emails? WH removed this for db1->2 migration
2649                // leaving this in breaks MIGRATIONS from DAL 1
2650                // in that those contacts without emails will not be copied in
2651                // return false;
2652
2653            } else {
2654
2655                // email present, check if it matches ID?
2656                if ( ! empty( $id ) && $id > 0 ) {
2657
2658                    // if ID + email, check if existing contact with email, (e.g. in use)
2659                    // ... allow it if the ID of that email contact matches the ID given here
2660                    // (else e.g. add email x to ID y without checking)
2661                    $potentialUSERID = (int) $this->getContact(
2662                        -1,
2663                        array(
2664                            'email'       => $data['email'],
2665                            'ignoreOwner' => 1,
2666                            'onlyID'      => 1,
2667                        )
2668                    );
2669                    if ( ! empty( $potentialUSERID ) && $potentialUSERID > 0 && $id > 0 && $potentialUSERID != $id ) {
2670
2671                        // email doesn't match ID
2672                        return false;
2673                    }
2674
2675                    // also check if has rights?!? Could be just email passed here + therefor got around owner check? hmm.
2676
2677                } else {
2678
2679                    // no ID, check if email present, and then update that user if so
2680                    $potentialUSERID = (int) $this->getContact(
2681                        -1,
2682                        array(
2683                            'email'       => $data['email'],
2684                            'ignoreOwner' => 1,
2685                            'onlyID'      => 1,
2686                        )
2687                    );
2688                    if ( isset( $potentialUSERID ) && ! empty( $potentialUSERID ) && $potentialUSERID > 0 ) {
2689                        $id = $potentialUSERID;
2690                    }
2691                }
2692            }
2693
2694                // companies
2695            if ( isset( $data['companies'] ) && is_array( $data['companies'] ) ) {
2696
2697                $coArr = array();
2698                /*
2699                there was a bug happening here where same company could get dude at a few times...
2700                so for now only use the first company */
2701                /*
2702                foreach ( $data['companies'] as $c){
2703                    $cI = (int)$c;
2704                    if ( $cI > 0 && !in_array( $cI, $coArr)) $coArr[] = $cI;
2705                }*/
2706
2707                $cI = (int) $data['companies'][0];
2708                if ( $cI > 0 && ! in_array( $cI, $coArr ) ) {
2709                    $coArr[] = $cI;
2710                }
2711
2712                // reset the main
2713                if ( count( $coArr ) > 0 ) {
2714                    $data['companies'] = $coArr;
2715                } else {
2716                    $data['companies'] = 'unset';
2717                }
2718                unset( $coArr );
2719
2720            }
2721        }
2722
2723            // If no status passed or previously set, use the default status
2724        if ( empty( $data['status'] ) ) {
2725
2726            // copy any previously set
2727            $data['status'] = $this->getContactStatus( $id );
2728
2729            // if not previously set, use default
2730            if ( empty( $data['status'] ) ) {
2731                $data['status'] = zeroBSCRM_getSetting( 'defaultstatus' );
2732            }
2733        }
2734
2735        #} ========= / CHECK FIELDS ===========
2736
2737        #} ========= OVERRIDE SETTING (Deny blank overrides) ===========
2738
2739        // this only functions if externalsource is set (e.g. api/form, etc.)
2740        if ( isset( $data['externalSources'] ) && is_array( $data['externalSources'] ) && count( $data['externalSources'] ) > 0 ) {
2741            if ( zeroBSCRM_getSetting( 'fieldoverride' ) == '1' ) {
2742
2743                $do_not_update_blanks = true;
2744
2745            }
2746        }
2747
2748                // either ext source + setting, or set by the func call
2749        if ( $do_not_update_blanks ) {
2750
2751            // this setting says 'don't override filled-out data with blanks'
2752            // so here we check through any passed blanks + convert to limitedFields
2753            // only matters if $id is set (there is somt to update not add
2754            if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
2755
2756                // get data to copy over (for now, this is required to remove 'fullname' etc.)
2757                $dbData = $this->db_ready_contact( $data );
2758                // unset($dbData['id']); // this is unset because we use $id, and is update, so not req. legacy issue
2759                // unset($dbData['created']); // this is unset because this uses an obj which has been 'updated' against original details, where created is output in the WRONG format :)
2760
2761                $origData    = $data; // $data = array();
2762                $limitedData = array(); // array(array('key'=>'zbsc_x','val'=>y,'type'=>'%s'))
2763
2764                // cycle through + translate into limitedFields (removing any blanks, or arrays (e.g. externalSources))
2765                // we also have to remake a 'faux' data (removing blanks for tags etc.) for the post-update updates
2766                foreach ( $dbData as $k => $v ) {
2767
2768                    $intV = (int) $v;
2769
2770                    // only add if valuenot empty
2771                    if ( ! is_array( $v ) && ! empty( $v ) && $v != '' && $v !== 0 && $v !== -1 && $intV !== -1 ) {
2772
2773                        // add to update arr
2774                        $limitedData[] = array(
2775                            'key'  => 'zbsc_' . $k, // we have to add zbsc_ here because translating from data -> limited fields
2776                            'val'  => $v,
2777                            'type' => $this->getTypeStr( 'zbsc_' . $k ),
2778                        );
2779
2780                        // add to remade $data for post-update updates
2781                        $data[ $k ] = $v;
2782
2783                    }
2784                }
2785
2786                // copy over
2787                $limitedFields = $limitedData;
2788
2789            } // / if ID
2790
2791        } // / if do_not_update_blanks
2792
2793        // ========= / OVERRIDE SETTING (Deny blank overrides) ===========
2794
2795        // ========= BUILD DATA ===========
2796
2797        // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- to be refactoerd later.
2798        $update                    = false;
2799        $dataArr                   = array();
2800        $typeArr                   = array();
2801        $contactsPreUpdateSegments = array();
2802
2803        if ( is_array( $limitedFields ) ) {
2804
2805            // LIMITED FIELDS
2806            $update = true;
2807
2808            // cycle through
2809            foreach ( $limitedFields as $field ) {
2810
2811                // some weird case where getting empties, so added check
2812                if ( empty( $field['key'] ) ) {
2813                    continue;
2814                }
2815                // Created date field is immutable. Skip.
2816                if ( $field['key'] === 'zbsc_created' ) {
2817                    continue;
2818                }
2819
2820                $dataArr[ $field['key'] ] = $field['val'];
2821                $typeArr[]                = $field['type'];
2822            }
2823
2824            // add update time
2825            if ( ! isset( $dataArr['zbsc_lastupdated'] ) ) {
2826                $dataArr['zbsc_lastupdated'] = time();
2827                $typeArr[]                   = '%d';
2828            }
2829        } else {
2830
2831            // FULL UPDATE/INSERT
2832
2833            // UPDATE
2834            $dataArr = array(
2835
2836                'zbs_owner'        => $owner,
2837
2838                // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- to be refactored.
2839                // fields
2840                'zbsc_status'      => $data['status'],
2841                'zbsc_email'       => $data['email'],
2842                'zbsc_prefix'      => $data['prefix'],
2843                'zbsc_fname'       => $data['fname'],
2844                'zbsc_lname'       => $data['lname'],
2845                'zbsc_addr1'       => $data['addr1'],
2846                'zbsc_addr2'       => $data['addr2'],
2847                'zbsc_city'        => $data['city'],
2848                'zbsc_county'      => $data['county'],
2849                'zbsc_country'     => $data['country'],
2850                'zbsc_postcode'    => $data['postcode'],
2851                'zbsc_secaddr1'    => $data['secaddr1'],
2852                'zbsc_secaddr2'    => $data['secaddr2'],
2853                'zbsc_seccity'     => $data['seccity'],
2854                'zbsc_seccounty'   => $data['seccounty'],
2855                'zbsc_seccountry'  => $data['seccountry'],
2856                'zbsc_secpostcode' => $data['secpostcode'],
2857                'zbsc_hometel'     => $data['hometel'],
2858                'zbsc_worktel'     => $data['worktel'],
2859                'zbsc_mobtel'      => $data['mobtel'],
2860                'zbsc_wpid'        => $data['wpid'],
2861                'zbsc_avatar'      => $data['avatar'],
2862
2863                'zbsc_tw'          => $data['tw'],
2864                'zbsc_fb'          => $data['fb'],
2865                'zbsc_li'          => $data['li'],
2866
2867                'zbsc_lastupdated' => time(),
2868            );
2869
2870            // if set.
2871            if ( $data['lastcontacted'] !== -1 ) {
2872                $dataArr['zbsc_lastcontacted'] = $data['lastcontacted'];
2873            }
2874
2875            $typeArr = array( // field data types
2876                // '%d',  // site
2877                // '%d',  // team
2878                '%d',    // owner
2879                '%s',
2880                '%s',
2881                '%s',
2882                '%s',
2883                '%s',
2884                '%s',
2885                '%s',
2886                '%s',
2887                '%s',
2888                '%s',
2889                '%s',
2890                '%s',
2891                '%s',
2892                '%s',
2893                '%s',
2894                '%s',
2895                '%s',
2896                '%s',
2897                '%s',
2898                '%s',
2899                '%d',
2900                '%s',
2901                '%s',
2902                '%s',
2903                '%s',
2904                '%d',   // last updated
2905            );
2906            // if set
2907            if ( $data['lastcontacted'] !== -1 ) {
2908                $typeArr[] = '%d';
2909            }
2910
2911            if ( ! empty( $id ) && $id > 0 ) {
2912
2913                // is update
2914                $update = true;
2915
2916            } else {
2917
2918                // INSERT (get's few extra :D)
2919                $update              = false;
2920                $dataArr['zbs_site'] = zeroBSCRM_site();
2921                $typeArr[]           = '%d';
2922                $dataArr['zbs_team'] = zeroBSCRM_team();
2923                $typeArr[]           = '%d';
2924
2925                if ( isset( $data['created'] ) && ! empty( $data['created'] ) && $data['created'] !== -1 ) {
2926                    $dataArr['zbsc_created'] = $data['created'];
2927                    $typeArr[]               = '%d';
2928                } else {
2929                    $dataArr['zbsc_created'] = time();
2930                    $typeArr[]               = '%d';
2931                }
2932
2933                $dataArr['zbsc_lastcontacted'] = -1;
2934                $typeArr[]                     = '%d';
2935            }
2936        }
2937        #} ========= / BUILD DATA ===========
2938
2939        #} ============================================================
2940        #} ========= CHECK force_uniques & not_empty & max_len ========
2941
2942            // if we're passing limitedFields we skip these, for now
2943            // #v3.1 - would make sense to unique/nonempty check just the limited fields. #gh-145
2944        if ( ! is_array( $limitedFields ) ) {
2945
2946            // verify uniques
2947            if ( ! $this->verifyUniqueValues( $data, $id ) ) {
2948                return false; // / fails unique field verify
2949            }
2950
2951            // verify not_empty
2952            if ( ! $this->verifyNonEmptyValues( $data ) ) {
2953                return false; // / fails empty field verify
2954            }
2955        }
2956
2957        // whatever we do we check for max_len breaches and abbreviate to avoid wpdb rejections
2958        $dataArr = $this->wpdbChecks( $dataArr );
2959
2960        // CHECK force_uniques & not_empty
2961
2962        // Check if ID present
2963        if ( $update ) {
2964
2965            // Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
2966            $originalStatus = $this->getContactStatus( $id );
2967
2968            $previous_contact_obj = $this->getContact( $id );
2969
2970            // get any segments (whom counts may be affected by changes)
2971            // $contactsPreUpdateSegments = $this->DAL()->segments->getSegmentsContainingContact($id,true);
2972
2973            // log any change of status
2974            if ( isset( $dataArr['zbsc_status'] ) && ! empty( $dataArr['zbsc_status'] ) && ! empty( $originalStatus ) && $dataArr['zbsc_status'] !== $originalStatus ) {
2975
2976                // status change
2977                $statusChange = array(
2978                    'from' => $originalStatus,
2979                    'to'   => $dataArr['zbsc_status'],
2980                );
2981            }
2982
2983            // Attempt update
2984            if ( $wpdb->update(
2985                $ZBSCRM_t['contacts'],
2986                $dataArr,
2987                array( // where
2988                    'ID' => $id,
2989                ),
2990                $typeArr,
2991                array( // where data types
2992                    '%d',
2993                )
2994            ) !== false ) {
2995
2996                // if passing limitedFields instead of data, we ignore the following
2997                // this doesn't work, because data is in args default as arr
2998                // if (isset($data) && is_array($data)){
2999                // so...
3000                if ( ! isset( $limitedFields ) || ! is_array( $limitedFields ) || $limitedFields == -1 ) {
3001
3002                    // tags
3003                    if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
3004
3005                        $this->addUpdateContactTags(
3006                            array(
3007                                'id'        => $id,
3008                                'tag_input' => $data['tags'],
3009                                'mode'      => $data['tag_mode'],
3010                            )
3011                        );
3012
3013                    }
3014
3015                    // externalSources
3016                    $approvedExternalSource = $this->DAL()->addUpdateExternalSources(
3017                        array(
3018                            'obj_id'           => $id,
3019                            'obj_type_id'      => ZBS_TYPE_CONTACT,
3020                            'external_sources' => isset( $data['externalSources'] ) ? $data['externalSources'] : array(),
3021                        )
3022                    ); // for IA below
3023
3024                    // co's work?
3025                    // OBJ LINKS - to companies (1liner now as genericified)
3026                    $this->addUpdateObjectLinks( $id, $data['companies'], ZBS_TYPE_COMPANY );
3027
3028                    // Aliases
3029                    // Maintain an array of AKA emails
3030                    if ( isset( $data['aliases'] ) && is_array( $data['aliases'] ) ) {
3031
3032                        $existingAliasesSimple = array();
3033                        $existingAliases       = zeroBS_getObjAliases( ZBS_TYPE_CONTACT, $id );
3034                        if ( ! is_array( $existingAliases ) ) {
3035                            $existingAliases = array();
3036                        }
3037
3038                        // compare
3039                        if ( is_array( $existingAliases ) ) {
3040                            foreach ( $existingAliases as $alias ) {
3041
3042                                // is this alias in the new list?
3043                                if ( in_array( $alias['aka_alias'], $data['aliases'] ) ) {
3044                                    $existingAliasesSimple[] = $alias['aka_alias'];
3045                                    continue;
3046                                }
3047
3048                                // it's not in the new list, thus, remove it:
3049                                // this could be a smidgen more performant if it just deleted the line
3050                                zeroBS_removeObjAlias( ZBS_TYPE_CONTACT, $id, $alias['aka_alias'] );
3051
3052                            }
3053                        }
3054                        foreach ( $data['aliases'] as $alias ) {
3055
3056                            // valid?
3057                            if ( zeroBS_canUseCustomerAlias( $alias ) ) {
3058
3059                                // is this alias in the existing list? (nothing to do)
3060                                if ( in_array( $alias, $existingAliasesSimple ) ) {
3061                                    continue;
3062                                }
3063
3064                                // it's not in the existing list, thus, add it:
3065                                zeroBS_addObjAlias( ZBS_TYPE_CONTACT, $id, $alias );
3066
3067                            } else {
3068
3069                                // err - tried to use an invalid alias
3070                                $msg = __( 'Could not add alias (unavailable or invalid):', 'zero-bs-crm' ) . ' ' . $alias;
3071                                $zbs->DAL->addError( 307, $this->objectType, $msg, $alias );
3072
3073                            }
3074                        }
3075                    }
3076                } // / if $data/limitedData
3077
3078                // 2.98.1+ ... custom fields should update if present, regardless of limitedData rule
3079                // ... UNLESS BLANK!
3080                // Custom fields?
3081
3082                #} Cycle through + add/update if set
3083                if ( is_array( $customFields ) ) {
3084                    foreach ( $customFields as $cK => $cF ) {
3085
3086                        // any?
3087                        if ( isset( $data[ $cK ] ) ) {
3088
3089                            // updating blanks?
3090                            if ( $do_not_update_blanks && empty( $data[ $cK ] ) ) {
3091
3092                                // skip it
3093
3094                            } else {
3095
3096                                // it's either not in do_not_update_blank mode, or it has a val
3097
3098                                // add update
3099                                $cfID = $this->addUpdateCustomField(
3100                                    array(
3101                                        'data' => array(
3102                                            'objtype' => ZBS_TYPE_CONTACT,
3103                                            'objid'   => $id,
3104                                            'objkey'  => $cK,
3105                                            'objval'  => $data[ $cK ],
3106                                        ),
3107                                    )
3108                                );
3109                            }
3110                        }
3111                    }
3112                }
3113
3114                // Also got to catch any 'addr' custom fields :)
3115                if ( is_array( $addrCustomFields ) && count( $addrCustomFields ) > 0 ) {
3116
3117                    // cycle through addr custom fields + save
3118                    // see #ZBS-518, not easy until addr's get DAL2
3119                    // WH deferring here
3120                    // WH later added via the addUpdateContactField method - should work fine if we catch properly in get
3121                    foreach ( $addrCustomFields as $cK => $cF ) {
3122
3123                        // v2:
3124                        // $cKN = (int)$cK+1;
3125                        // $cKey = 'addr_cf'.$cKN;
3126                        // $cKey2 = 'secaddr_cf'.$cKN;
3127                        // v3:
3128                        $cKey  = 'addr_' . $cK;
3129                        $cKey2 = 'secaddr_' . $cK;
3130
3131                        if ( isset( $data[ $cKey ] ) ) {
3132
3133                            // updating blanks?
3134                            if ( $do_not_update_blanks && empty( $data[ $cKey ] ) ) {
3135
3136                                // skip it
3137
3138                            } else {
3139
3140                                // it's either not in do_not_update_blank mode, or it has a val
3141
3142                                // add update
3143                                $cfID = $this->addUpdateCustomField(
3144                                    array(
3145                                        'data' => array(
3146                                            'objtype' => ZBS_TYPE_CONTACT,
3147                                            'objid'   => $id,
3148                                            'objkey'  => $cKey,
3149                                            'objval'  => $data[ $cKey ],
3150                                        ),
3151                                    )
3152                                );
3153
3154                            }
3155                        }
3156
3157                        // any?
3158                        if ( isset( $data[ $cKey2 ] ) ) {
3159
3160                            // updating blanks?
3161                            if ( $do_not_update_blanks && empty( $data[ $cKey2 ] ) ) {
3162
3163                                // skip it
3164
3165                            } else {
3166
3167                                // it's either not in do_not_update_blank mode, or it has a val
3168
3169                                // add update
3170                                $cfID = $this->addUpdateCustomField(
3171                                    array(
3172                                        'data' => array(
3173                                            'objtype' => ZBS_TYPE_CONTACT,
3174                                            'objid'   => $id,
3175                                            'objkey'  => $cKey2,
3176                                            'objval'  => $data[ $cKey2 ],
3177                                        ),
3178                                    )
3179                                );
3180
3181                            }
3182                        }
3183                    }
3184                }
3185
3186                // / Custom Fields
3187
3188                #} Any extra meta keyval pairs?
3189                // BRUTALLY updates (no checking)
3190                $confirmedExtraMeta = false;
3191                if ( is_array( $extraMeta ) ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
3192
3193                    $confirmedExtraMeta = array();
3194
3195                    foreach ( $extraMeta as $k => $v ) {
3196
3197                        #} This won't fix stupid keys, just catch basic fails...
3198                        $cleanKey = strtolower( str_replace( ' ', '_', $k ) );
3199
3200                        #} Brutal update
3201                        // update_post_meta($postID, 'zbs_customer_extra_'.$cleanKey, $v);
3202                        $this->DAL()->updateMeta( ZBS_TYPE_CONTACT, $id, 'extra_' . $cleanKey, $v );
3203
3204                        #} Add it to this, which passes to IA
3205                        $confirmedExtraMeta[ $cleanKey ] = $v;
3206
3207                    }
3208                }
3209
3210                #} INTERNAL AUTOMATOR
3211                #} &
3212                #} FALLBACKS
3213                // UPDATING CONTACT
3214                if ( ! $silentInsert ) {
3215
3216                    #} FALLBACK
3217                    #} (This fires for customers that weren't added because they already exist.)
3218                    #} e.g. x@g.com exists, so add log "x@g.com filled out form"
3219                    #} Requires a type and a shortdesc
3220                    if ( is_array( $fallBackLog ) && ! empty( $fallBackLog['type'] ) && ! empty( $fallBackLog['shortdesc'] ) // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
3221                    ) {
3222
3223                        #} Brutal add, maybe validate more?!
3224
3225                        #} Long desc if present:
3226                        $zbsNoteLongDesc = '';
3227                        if ( isset( $fallBackLog['longdesc'] ) && ! empty( $fallBackLog['longdesc'] ) ) {
3228                            $zbsNoteLongDesc = $fallBackLog['longdesc'];
3229                        }
3230
3231                        #} Only raw checked... but proceed.
3232                        $newOrUpdatedLogID = zeroBS_addUpdateContactLog(
3233                            $id,
3234                            -1,
3235                            -1,
3236                            array(
3237                                #} Anything here will get wrapped into an array and added as the meta vals
3238                                'type'      => $fallBackLog['type'],
3239                                'shortdesc' => $fallBackLog['shortdesc'],
3240                                'longdesc'  => $zbsNoteLongDesc,
3241                            )
3242                        );
3243
3244                    }
3245
3246                    // catch dirty flag (update of status) (note, after update_post_meta - as separate)
3247                    // if (isset($_POST['zbsc_status_dirtyflag']) && $_POST['zbsc_status_dirtyflag'] == "1"){
3248                    // actually here, it's set above
3249                    if ( isset( $statusChange ) && is_array( $statusChange ) ) {
3250
3251                        // status has changed
3252
3253                        // IA
3254                        zeroBSCRM_FireInternalAutomator(
3255                            'contact.status.update',
3256                            array(
3257                                'id'        => $id,
3258                                'againstid' => $id,
3259                                'userMeta'  => $dataArr,
3260                                'from'      => $statusChange['from'],
3261                                'to'        => $statusChange['to'],
3262                            )
3263                        );
3264
3265                    }
3266
3267                    // IA General contact update (2.87+)
3268                    zeroBSCRM_FireInternalAutomator(
3269                        'contact.update',
3270                        array(
3271                            'id'           => $id,
3272                            'againstid'    => $id,
3273                            'userMeta'     => $dataArr,
3274                            'prevSegments' => $contactsPreUpdateSegments,
3275                            'prev_contact' => $previous_contact_obj,
3276                        )
3277                    );
3278
3279                    $dataArr['id'] = $id;
3280                    $this->events_manager->contact()->updated( $dataArr, $previous_contact_obj );
3281
3282                }
3283
3284                // Successfully updated - Return id
3285                return $id;
3286
3287            } else {
3288
3289                $msg = __( 'DB Update Failed', 'zero-bs-crm' );
3290                $zbs->DAL->addError( 302, $this->objectType, $msg, $dataArr );
3291
3292                // FAILED update
3293                return false;
3294
3295            }
3296        } else {
3297
3298            #} No ID - must be an INSERT
3299            if ( $wpdb->insert(
3300                $ZBSCRM_t['contacts'],
3301                $dataArr,
3302                $typeArr
3303            ) > 0 ) {
3304
3305                #} Successfully inserted, lets return new ID
3306                $newID = $wpdb->insert_id;
3307
3308                // tags
3309                if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
3310
3311                    $this->addUpdateContactTags(
3312                        array(
3313                            'id'        => $newID,
3314                            'tag_input' => $data['tags'],
3315                            'mode'      => $data['tag_mode'],
3316                        )
3317                    );
3318
3319                }
3320
3321                // externalSources
3322                $approvedExternalSource = $this->DAL()->addUpdateExternalSources(
3323                    array(
3324                        'obj_id'           => $newID,
3325                        'obj_type_id'      => ZBS_TYPE_CONTACT,
3326                        'external_sources' => isset( $data['externalSources'] ) ? $data['externalSources'] : array(),
3327                    )
3328                ); // for IA below
3329
3330                // co's work?
3331                // OBJ LINKS - to companies (1liner now as genericified)
3332                $this->addUpdateObjectLinks( $newID, $data['companies'], ZBS_TYPE_COMPANY );
3333                /*
3334                if ( isset( $data['companies']) && is_array( $data['companies']) && count( $data['companies']) > 0)
3335                    $this->DAL()->addUpdateObjLinks(array(
3336                                                    'objtypefrom'       => ZBS_TYPE_CONTACT,
3337                                                    'objtypeto'         => ZBS_TYPE_COMPANY,
3338                                                    'objfromid'         => $newID,
3339                                                    'objtoids'          => $data['companies']));
3340                */
3341
3342                // Aliases
3343                // Maintain an array of AKA emails
3344                if ( isset( $data['aliases'] ) && is_array( $data['aliases'] ) ) {
3345
3346                    $existingAliasesSimple = array();
3347                    $existingAliases       = zeroBS_getObjAliases( ZBS_TYPE_CONTACT, $newID );
3348                    if ( ! is_array( $existingAliases ) ) {
3349                        $existingAliases = array();
3350                    }
3351
3352                    // compare
3353                    if ( is_array( $existingAliases ) ) {
3354                        foreach ( $existingAliases as $alias ) {
3355
3356                            // is this alias in the new list?
3357                            if ( in_array( $alias['aka_alias'], $data['aliases'] ) ) {
3358                                $existingAliasesSimple[] = $alias['aka_alias'];
3359                                continue;
3360                            }
3361
3362                            // it's not in the new list, thus, remove it:
3363                            // this could be a smidgen more performant if it just deleted the line
3364                            zeroBS_removeObjAlias( ZBS_TYPE_CONTACT, $newID, $alias['aka_alias'] );
3365
3366                        }
3367                    }
3368                    foreach ( $data['aliases'] as $alias ) {
3369
3370                        // valid?
3371                        if ( zeroBS_canUseCustomerAlias( $alias ) ) {
3372
3373                            // is this alias in the existing list? (nothing to do)
3374                            if ( in_array( $alias, $existingAliasesSimple ) ) {
3375                                continue;
3376                            }
3377
3378                            // it's not in the existing list, thus, add it:
3379                            zeroBS_addObjAlias( ZBS_TYPE_CONTACT, $newID, $alias );
3380
3381                        } else {
3382
3383                            // err - tried to use an invalid alias
3384                            $msg = __( 'Could not add alias (unavailable or invalid):', 'zero-bs-crm' ) . ' ' . $alias;
3385                            $zbs->DAL->addError( 307, $this->objectType, $msg, $alias );
3386
3387                        }
3388                    }
3389                }
3390
3391                // Custom fields?
3392
3393                #} Cycle through + add/update if set
3394                if ( is_array( $customFields ) ) {
3395                    foreach ( $customFields as $cK => $cF ) {
3396
3397                        // any?
3398                        if ( isset( $data[ $cK ] ) ) {
3399
3400                            // add update
3401                            $cfID = $this->DAL()->addUpdateCustomField(
3402                                array(
3403                                    'data' => array(
3404                                        'objtype' => ZBS_TYPE_CONTACT,
3405                                        'objid'   => $newID,
3406                                        'objkey'  => $cK,
3407                                        'objval'  => $data[ $cK ],
3408                                    ),
3409                                )
3410                            );
3411
3412                        }
3413                    }
3414                }
3415
3416                // Also got to catch any 'addr' custom fields :)
3417                if ( is_array( $addrCustomFields ) && count( $addrCustomFields ) > 0 ) {
3418
3419                    // cycle through addr custom fields + save
3420                    // see #ZBS-518, not easy until addr's get DAL2
3421                    // WH deferring here
3422                    // WH later added via the addUpdateContactField method - should work fine if we catch properly in get
3423                    foreach ( $addrCustomFields as $cK => $cF ) {
3424
3425                        // v2:
3426                        // $cKN = (int)$cK+1;
3427                        // $cKey = 'addr_cf'.$cKN;
3428                        // $cKey2 = 'secaddr_cf'.$cKN;
3429                        // v3:
3430                        $cKey  = 'addr_' . $cK;
3431                        $cKey2 = 'secaddr_' . $cK;
3432
3433                        if ( isset( $data[ $cKey ] ) ) {
3434
3435                            // add update
3436                            $cfID = $this->DAL()->addUpdateCustomField(
3437                                array(
3438                                    'data' => array(
3439                                        'objtype' => ZBS_TYPE_CONTACT,
3440                                        'objid'   => $newID,
3441                                        'objkey'  => $cKey,
3442                                        'objval'  => $data[ $cKey ],
3443                                    ),
3444                                )
3445                            );
3446
3447                        }
3448
3449                        // any?
3450                        if ( isset( $data[ $cKey2 ] ) ) {
3451
3452                            // add update
3453                            $cfID = $this->DAL()->addUpdateCustomField(
3454                                array(
3455                                    'data' => array(
3456                                        'objtype' => ZBS_TYPE_CONTACT,
3457                                        'objid'   => $newID,
3458                                        'objkey'  => $cKey2,
3459                                        'objval'  => $data[ $cKey2 ],
3460                                    ),
3461                                )
3462                            );
3463
3464                        }
3465                        // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
3466
3467                    }
3468                }
3469
3470                // / Custom Fields
3471
3472                #} Any extra meta keyval pairs?
3473                // BRUTALLY updates (no checking)
3474                $confirmedExtraMeta = false;
3475                if ( is_array( $extraMeta ) ) { // phpcs:ignore -- PHPCS is choking on this line, so ignoring it altogether...the var is defined at the beginning of the function.
3476
3477                    $confirmedExtraMeta = array();
3478
3479                    foreach ( $extraMeta as $k => $v ) {
3480
3481                        #} This won't fix stupid keys, just catch basic fails...
3482                        $cleanKey = strtolower( str_replace( ' ', '_', $k ) );
3483
3484                        #} Brutal update
3485                        // update_post_meta($postID, 'zbs_customer_extra_'.$cleanKey, $v);
3486                        $this->DAL()->updateMeta( ZBS_TYPE_CONTACT, $newID, 'extra_' . $cleanKey, $v );
3487
3488                        #} Add it to this, which passes to IA
3489                        $confirmedExtraMeta[ $cleanKey ] = $v;
3490
3491                    }
3492                }
3493
3494                #} INTERNAL AUTOMATOR
3495                #} &
3496                #} FALLBACKS
3497                // NEW CONTACT
3498
3499                // zbs_write_log("ABOUT TO HIT THE AUTOMATOR... " . $silentInsert);
3500
3501                if ( ! $silentInsert ) {
3502
3503                    // zbs_write_log("HITTING IT NOW...");
3504
3505                    #} Add to automator
3506                    zeroBSCRM_FireInternalAutomator(
3507                        'contact.new',
3508                        array(
3509                            'id'                   => $newID,
3510                            'customerMeta'         => $dataArr,
3511                            'extsource'            => $approvedExternalSource,
3512                            'automatorpassthrough' => $automatorPassthrough, #} This passes through any custom log titles or whatever into the Internal automator recipe.
3513                            'customerExtraMeta'    => $confirmedExtraMeta, #} This is the "extraMeta" passed (as saved)
3514                        )
3515                    );
3516
3517                    $dataArr['ID'] = $newID;
3518                    $this->events_manager->contact()->created( $dataArr );
3519
3520                }
3521
3522                return $newID;
3523
3524            } else {
3525
3526                $msg = __( 'DB Insert Failed', 'zero-bs-crm' );
3527                $zbs->DAL->addError( 303, $this->objectType, $msg, $dataArr );
3528                // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
3529
3530                #} Failed to Insert
3531                return false;
3532
3533            }
3534        }
3535
3536        return false;
3537    }
3538
3539    /**
3540     * adds or updates a contact's tags
3541     * ... this is really just a wrapper for addUpdateObjectTags
3542     *
3543     * @param array $args Associative array of arguments
3544     *              id (if update), owner, data (array of field data)
3545     *
3546     * @return int line ID
3547     */
3548    public function addUpdateContactTags( $args = array() ) {
3549
3550        global $ZBSCRM_t, $wpdb;
3551
3552        #} ============ LOAD ARGS =============
3553        $defaultArgs = array(
3554
3555            'id'        => -1,
3556
3557            // generic pass-through (array of tag strings or tag IDs):
3558            'tag_input' => -1,
3559
3560            // or either specific:
3561            'tagIDs'    => -1,
3562            'tags'      => -1,
3563
3564            'mode'      => 'append',
3565
3566        );
3567        foreach ( $defaultArgs as $argK => $argV ) {
3568            $$argK = $argV;
3569            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3570                if ( is_array( $args[ $argK ] ) ) {
3571                    $newData = $$argK;
3572                    if ( ! is_array( $newData ) ) {
3573                        $newData = array();
3574                    }
3575                    foreach ( $args[ $argK ] as $subK => $subV ) {
3576                        $newData[ $subK ] = $subV;
3577                    }
3578                    $$argK = $newData;
3579                } else {
3580                    $$argK = $args[ $argK ];
3581                }
3582            }
3583        }
3584        #} =========== / LOAD ARGS ============
3585
3586        #} ========== CHECK FIELDS ============
3587
3588        // check id
3589        $id = (int) $id;
3590        if ( empty( $id ) || $id <= 0 ) {
3591            return false;
3592        }
3593
3594        #} ========= / CHECK FIELDS ===========
3595
3596        return $this->DAL()->addUpdateObjectTags(
3597            array(
3598                'objtype'   => ZBS_TYPE_CONTACT,
3599                'objid'     => $id,
3600                'tag_input' => $tag_input,
3601                'tags'      => $tags,
3602                'tagIDs'    => $tagIDs,
3603                'mode'      => $mode,
3604            )
3605        );
3606    }
3607
3608    /**
3609     * adds or updates a contact's company links
3610     * ... this is really just a wrapper for addUpdateObjLinks
3611     * fill in for zbsCRM_addUpdateCustomerCompany + zeroBS_setCustomerCompanyID
3612     *
3613     * @param array $args Associative array of arguments
3614     *              id (if update), owner, data (array of field data)
3615     *
3616     * @return int line ID
3617     */
3618    public function addUpdateContactCompanies( $args = array() ) {
3619
3620        global $ZBSCRM_t, $wpdb;
3621
3622        #} ============ LOAD ARGS =============
3623        $defaultArgs = array(
3624
3625            'id'         => -1,
3626            'companyIDs' => -1,
3627
3628        );
3629        foreach ( $defaultArgs as $argK => $argV ) {
3630            $$argK = $argV;
3631            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3632                if ( is_array( $args[ $argK ] ) ) {
3633                    $newData = $$argK;
3634                    if ( ! is_array( $newData ) ) {
3635                        $newData = array();
3636                    }
3637                    foreach ( $args[ $argK ] as $subK => $subV ) {
3638                        $newData[ $subK ] = $subV;
3639                    }
3640                    $$argK = $newData;
3641                } else {
3642                    $$argK = $args[ $argK ];
3643                }
3644            }
3645        }
3646        #} =========== / LOAD ARGS ============
3647
3648        #} ========== CHECK FIELDS ============
3649
3650        // check id
3651        $id = (int) $id;
3652        if ( empty( $id ) || $id <= 0 ) {
3653            return false;
3654        }
3655
3656        // check co id's
3657        if ( ! is_array( $companyIDs ) ) {
3658            $companyIDs = array();
3659        }
3660
3661        #} ========= / CHECK FIELDS ===========
3662
3663        return $this->DAL()->addUpdateObjLinks(
3664            array(
3665                'objtypefrom' => ZBS_TYPE_CONTACT,
3666                'objtypeto'   => ZBS_TYPE_COMPANY,
3667                'objfromid'   => $id,
3668                'objtoids'    => $companyIDs,
3669            )
3670        );
3671    }
3672
3673    /**
3674     * adds or updates a contact's WPID
3675     * ... this is really just a wrapper for addUpdateContact
3676     * ... and replaces zeroBS_setCustomerWPID
3677     *
3678     * @param array $args Associative array of arguments
3679     *              id (if update), owner, data (array of field data)
3680     *
3681     * @return int line ID
3682     */
3683    public function addUpdateContactWPID( $args = array() ) {
3684
3685        global $ZBSCRM_t, $wpdb;
3686
3687        #} ============ LOAD ARGS =============
3688        $defaultArgs = array(
3689
3690            'id'   => -1,
3691            'WPID' => -1,
3692
3693        );
3694        foreach ( $defaultArgs as $argK => $argV ) {
3695            $$argK = $argV;
3696            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3697                if ( is_array( $args[ $argK ] ) ) {
3698                    $newData = $$argK;
3699                    if ( ! is_array( $newData ) ) {
3700                        $newData = array();
3701                    }
3702                    foreach ( $args[ $argK ] as $subK => $subV ) {
3703                        $newData[ $subK ] = $subV;
3704                    }
3705                    $$argK = $newData;
3706                } else {
3707                    $$argK = $args[ $argK ];
3708                }
3709            }
3710        }
3711        #} =========== / LOAD ARGS ============
3712
3713        #} ========== CHECK FIELDS ============
3714
3715        // check id
3716        $id = (int) $id;
3717        if ( empty( $id ) || $id <= 0 ) {
3718            return false;
3719        }
3720
3721        // WPID may be -1 (NULL)
3722        // -1 does okay here if ($WPID == -1) $WPID = '';
3723
3724        #} ========= / CHECK FIELDS ===========
3725
3726        #} Enact
3727        return $this->addUpdateContact(
3728            array(
3729                'id'            => $id,
3730                'limitedFields' => array(
3731                    array(
3732                        'key'  => 'zbsc_wpid',
3733                        'val'  => $WPID,
3734                        'type' => '%d',
3735                    ),
3736                ),
3737            )
3738        );
3739    }
3740
3741    /**
3742     * deletes a contact object
3743     *
3744     * @param array $args Associative array of arguments
3745     *              id
3746     *
3747     * @return int success;
3748     */
3749    public function deleteContact( $args = array() ) {
3750
3751        global $zbs;
3752
3753        #} ============ LOAD ARGS =============
3754        $defaultArgs = array(
3755
3756            'id'          => -1,
3757            'saveOrphans' => true,
3758
3759        );
3760        foreach ( $defaultArgs as $argK => $argV ) {
3761            $$argK = $argV;
3762            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
3763                if ( is_array( $args[ $argK ] ) ) {
3764                    $newData = $$argK;
3765                    if ( ! is_array( $newData ) ) {
3766                        $newData = array();
3767                    }
3768                    foreach ( $args[ $argK ] as $subK => $subV ) {
3769                        $newData[ $subK ] = $subV;
3770                    }
3771                    $$argK = $newData;
3772                } else {
3773                    $$argK = $args[ $argK ];
3774                }
3775            }
3776        }
3777        #} =========== / LOAD ARGS ============
3778
3779        #} Before we actually delete - allow a hook and pass the args (which is just the id and whether saveOrphans or not)
3780        zeroBSCRM_FireInternalAutomator(
3781            'contact.before.delete',
3782            array(
3783                'id'          => $id,
3784                'saveOrphans' => $saveOrphans,
3785            )
3786        );
3787        // phpcs:ignore
3788        $this->events_manager->contact()->before_delete( $id );
3789
3790        #} Check ID & Delete :)
3791        $id = (int) $id;
3792        if ( ! empty( $id ) && $id > 0 ) {
3793
3794            // delete orphans?
3795            if ( $saveOrphans === false ) {
3796
3797                #DB1LEGACY (TOMOVE -> where)
3798                // delete quotes
3799                $qs = zeroBS_getQuotesForCustomer( $id, false, 1000000, 0, false, false );
3800                foreach ( $qs as $q ) {
3801
3802                    // delete post
3803                    $zbs->DAL->quotes->deleteQuote( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
3804                        array(
3805                            'id'          => $q['id'],
3806                            'saveOrphans' => false,
3807                        )
3808                    );
3809
3810                }
3811                unset( $qs );
3812
3813                #DB1LEGACY (TOMOVE -> where)
3814                // delete invoices
3815                $is = zeroBS_getInvoicesForCustomer( $id, false, 1000000, 0, false );
3816                foreach ( $is as $i ) {
3817
3818                    // delete post
3819                    $zbs->DAL->invoices->deleteInvoice( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
3820                        array(
3821                            'id'          => $i['id'],
3822                            'saveOrphans' => false,
3823                        )
3824                    );
3825
3826                }
3827                unset( $qs );
3828
3829                #DB1LEGACY (TOMOVE -> where)
3830                // delete transactions
3831                $trans = zeroBS_getTransactionsForCustomer( $id, false, 1000000, 0, false );
3832                foreach ( $trans as $tran ) {
3833
3834                    // delete post
3835                    $zbs->DAL->transactions->deleteTransaction( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
3836                        array(
3837                            'id'          => $tran['id'],
3838                            'saveOrphans' => false,
3839                        )
3840                    );
3841
3842                }
3843                unset( $trans );
3844
3845                // delete events
3846                $events = zeroBS_getEventsByCustomerID( $id, false, 1000000, 0 );
3847                foreach ( $events as $event ) {
3848
3849                    // delete post
3850                    $zbs->DAL->events->deleteEvent( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
3851                        array(
3852                            'id'          => $event['id'],
3853                            'saveOrphans' => false,
3854                        )
3855                    );
3856
3857                }
3858                unset( $events );
3859
3860                // delete any tag links
3861                $this->DAL()->deleteTagObjLinks(
3862                    array(
3863
3864                        'objtype' => ZBS_TYPE_CONTACT,
3865                        'objid'   => $id,
3866                    )
3867                );
3868
3869                // delete any external source information
3870                $this->DAL()->delete_external_sources(
3871                    array(
3872
3873                        'obj_type'   => ZBS_TYPE_CONTACT,
3874                        'obj_id'     => $id,
3875                        'obj_source' => 'all',
3876
3877                    )
3878                );
3879
3880            }
3881
3882            // delete any alias information (must delete regardless of
3883            // $saveOrphans because there isn't a place where aliases are
3884            // listed, so they would block forever usage of aliased emails)
3885            $existing_aliases = zeroBS_getObjAliases( ZBS_TYPE_CONTACT, $id );
3886            if ( is_array( $existing_aliases ) ) {
3887                foreach ( $existing_aliases as $alias ) {
3888                    zeroBS_removeObjAlias( ZBS_TYPE_CONTACT, $id, $alias['aka_alias'] );
3889                }
3890            }
3891
3892            $del = zeroBSCRM_db2_deleteGeneric( $id, 'contacts' );
3893
3894            #} Add to automator
3895            zeroBSCRM_FireInternalAutomator(
3896                'contact.delete',
3897                array(
3898                    'id'          => $id,
3899                    'saveOrphans' => $saveOrphans,
3900                )
3901            );
3902
3903            $this->events_manager->contact()->deleted( $id );
3904
3905            return $del;
3906
3907        }
3908
3909        return false;
3910    }
3911
3912    /**
3913     * tidy's the object from wp db into clean array
3914     *
3915     * @param array $obj (DB obj)
3916     *
3917     * @return array (clean obj)
3918     */
3919    public function tidy_contact( $obj = false, $withCustomFields = false ) { // phpcs:ignore
3920
3921        global $zbs;
3922
3923        $res = false;
3924
3925        if ( isset( $obj->ID ) ) {
3926            $res       = array();
3927            $res['id'] = $obj->ID;
3928            /*
3929            `zbs_site` INT NULL DEFAULT NULL,
3930            `zbs_team` INT NULL DEFAULT NULL,
3931            `zbs_owner` INT NOT NULL,
3932            */
3933            $res['owner'] = $obj->zbs_owner;
3934
3935            $res['status']   = $this->stripSlashes( $obj->zbsc_status );
3936            $res['email']    = $obj->zbsc_email;
3937            $res['prefix']   = $this->stripSlashes( $obj->zbsc_prefix );
3938            $res['fname']    = $this->stripSlashes( $obj->zbsc_fname );
3939            $res['lname']    = $this->stripSlashes( $obj->zbsc_lname );
3940            $res['addr1']    = $this->stripSlashes( $obj->zbsc_addr1 );
3941            $res['addr2']    = $this->stripSlashes( $obj->zbsc_addr2 );
3942            $res['city']     = $this->stripSlashes( $obj->zbsc_city );
3943            $res['county']   = $this->stripSlashes( $obj->zbsc_county );
3944            $res['country']  = $this->stripSlashes( $obj->zbsc_country );
3945            $res['postcode'] = $this->stripSlashes( $obj->zbsc_postcode );
3946
3947            // until we add multi-addr support, these get translated into old field names (secaddr_)
3948            $res['secaddr_addr1']    = $this->stripSlashes( $obj->zbsc_secaddr1 );
3949            $res['secaddr_addr2']    = $this->stripSlashes( $obj->zbsc_secaddr2 );
3950            $res['secaddr_city']     = $this->stripSlashes( $obj->zbsc_seccity );
3951            $res['secaddr_county']   = $this->stripSlashes( $obj->zbsc_seccounty );
3952            $res['secaddr_country']  = $this->stripSlashes( $obj->zbsc_seccountry );
3953            $res['secaddr_postcode'] = $this->stripSlashes( $obj->zbsc_secpostcode );
3954            $res['hometel']          = $obj->zbsc_hometel;
3955            $res['worktel']          = $obj->zbsc_worktel;
3956            $res['mobtel']           = $obj->zbsc_mobtel;
3957            // $res['notes'] = $obj->zbsc_notes;
3958            $res['worktel'] = $obj->zbsc_worktel;
3959            $res['wpid']    = $obj->zbsc_wpid;
3960            $res['avatar']  = $obj->zbsc_avatar;
3961            $res['tw']      = $obj->zbsc_tw;
3962            $res['li']      = $obj->zbsc_li;
3963            $res['fb']      = $obj->zbsc_fb;
3964
3965            // gross backward compat
3966            if ( $zbs->db1CompatabilitySupport ) {
3967                $res['meta'] = $res;
3968            }
3969
3970            // to maintain old obj more easily, here we refine created into datestamp
3971            $res['created'] = zeroBSCRM_locale_utsToDatetime( $obj->zbsc_created );
3972            if ( $obj->zbsc_lastcontacted != -1 && ! empty( $obj->zbsc_lastcontacted ) && $obj->zbsc_lastcontacted > 0 ) {
3973                $res['lastcontacted'] = zeroBSCRM_locale_utsToDatetime( $obj->zbsc_lastcontacted );
3974            } else {
3975                $res['lastcontacted'] = -1;
3976            }
3977            $res['createduts'] = $obj->zbsc_created; // this is the UTS (int14)
3978
3979            // this is in v3.0+ format.
3980            $res['created_date']       = ( isset( $obj->zbsc_created ) && $obj->zbsc_created > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsc_created ) : false;
3981            $res['lastupdated']        = $obj->zbsc_lastupdated;
3982            $res['lastupdated_date']   = ( isset( $obj->zbsc_lastupdated ) && $obj->zbsc_lastupdated > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsc_lastupdated ) : false;
3983            $res['lastcontacteduts']   = $obj->zbsc_lastcontacted; // this is the UTS (int14)
3984            $res['lastcontacted_date'] = ( isset( $obj->zbsc_lastcontacted ) && $obj->zbsc_lastcontacted > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsc_lastcontacted ) : false;
3985
3986            // latest logs
3987            if ( isset( $obj->lastlog ) ) {
3988                $res['lastlog'] = $obj->lastlog;
3989            }
3990            if ( isset( $obj->lastcontactlog ) ) {
3991                $res['lastcontactlog'] = $obj->lastcontactlog;
3992            }
3993
3994            // Build any extra formats (using fields)
3995            $res['fullname'] = $this->format_fullname( $res );
3996            $res['name']     = $res['fullname']; // this one is for backward compat (pre db2)
3997
3998            // if have totals, pass them :)
3999            if ( isset( $obj->quotes_total ) ) {
4000                $res['quotes_total'] = $obj->quotes_total;
4001            }
4002            if ( isset( $obj->invoices_total ) ) {
4003                $res['invoices_total']             = $obj->invoices_total;
4004                $res['invoices_total_inc_deleted'] = $obj->invoices_total_inc_deleted;
4005                $res['invoices_count']             = $obj->invoices_count;
4006                $res['invoices_count_inc_deleted'] = $obj->invoices_count_inc_deleted;
4007            }
4008            if ( isset( $obj->transactions_total ) ) {
4009                $res['transactions_total'] = $obj->transactions_total;
4010            }
4011            if ( isset( $obj->transactions_paid_total ) ) {
4012                $res['transactions_paid_total'] = $obj->transactions_paid_total;
4013            }
4014
4015            // and if have invs + trans totals, add to make total val
4016            // This now accounts for "part payments" where trans are part/whole payments against invs
4017            if ( isset( $res['invoices_total'] ) || isset( $res['transactions_total'] ) ) {
4018                $res['total_value'] = jpcrm_get_total_value_from_contact_or_company( $res );
4019            }
4020
4021            // custom fields - tidy any that are present:
4022            if ( $withCustomFields ) {
4023                $res = $this->tidyAddCustomFields( ZBS_TYPE_CONTACT, $obj, $res, true );
4024            }
4025
4026            // Aliases
4027            if ( isset( $obj->aliases ) && is_string( $obj->aliases ) && ! empty( $obj->aliases ) ) {
4028
4029                // csv => array
4030                $res['aliases'] = explode( ',', $obj->aliases );
4031
4032            }
4033        }
4034
4035        return $res;
4036    }
4037
4038    /**
4039     * remove any non-db fields from the object
4040     * basically takes array like array('owner'=>1,'fname'=>'x','fullname'=>'x')
4041     * and returns array like array('owner'=>1,'fname'=>'x')
4042     *
4043     * @param array $obj (clean obj)
4044     *
4045     * @return array (db ready arr)
4046     */
4047    private function db_ready_contact( $obj = false ) {
4048
4049        global $zbs;
4050
4051        /*
4052        if ( is_array( $obj)){
4053
4054            $removeNonDBFields = array('meta','fullname','name');
4055
4056            foreach ( $removeNonDBFields as $fKey){
4057
4058                if ( isset( $obj[$fKey])) unset( $obj[$fKey]);
4059
4060            }
4061
4062        }
4063        */
4064
4065        $legitFields = array(
4066            'owner',
4067            'status',
4068            'email',
4069            'prefix',
4070            'fname',
4071            'lname',
4072            'addr1',
4073            'addr2',
4074            'city',
4075            'county',
4076            'country',
4077            'postcode',
4078            // WH corrected 13/06/18 2.84 'secaddr_addr1','secaddr_addr2','secaddr_city','secaddr_county','secaddr_country','secaddr_postcode',
4079            'secaddr1',
4080            'secaddr2',
4081            'seccity',
4082            'seccounty',
4083            'seccountry',
4084            'secpostcode',
4085            'hometel',
4086            'worktel',
4087            'mobtel',
4088            'wpid',
4089            'avatar',
4090            'tw',
4091            'fb',
4092            'li',
4093            'created',
4094            'lastupdated',
4095            'lastcontacted',
4096        );
4097
4098        $ret = array();
4099        if ( is_array( $obj ) ) {
4100
4101            foreach ( $legitFields as $fKey ) {
4102
4103                if ( isset( $obj[ $fKey ] ) ) {
4104                    $ret[ $fKey ] = $obj[ $fKey ];
4105                }
4106            }
4107        }
4108
4109        return $ret;
4110    }
4111
4112    /**
4113     * Wrapper, use $this->getContactMeta($contactID,$key) for easy retrieval of singular
4114     * Simplifies $this->getMeta
4115     *
4116     * @param int objtype
4117     * @param int objid
4118     * @param string key
4119     *
4120     * @return bool result
4121     */
4122    public function getContactMeta( $id = -1, $key = '', $default = false ) {
4123
4124        global $zbs;
4125
4126        if ( ! empty( $key ) ) {
4127
4128            return $this->DAL()->getMeta(
4129                array(
4130
4131                    'objtype'     => ZBS_TYPE_CONTACT,
4132                    'objid'       => $id,
4133                    'key'         => $key,
4134                    'fullDetails' => false,
4135                    'default'     => $default,
4136                    'ignoreowner' => true, // for now !!
4137
4138                )
4139            );
4140
4141        }
4142
4143        return $default;
4144    }
4145
4146    /**
4147     * returns external source detail lines for a contact
4148     *
4149     * @param array $args Associative array of arguments
4150     *              withStats, searchPhrase, sortByField, sortOrder, page, perPage
4151     *
4152     * @return array of tag lines
4153     */
4154    public function getExternalSourcesForContact( $args = array() ) {
4155
4156        global $zbs;
4157
4158        #} ============ LOAD ARGS =============
4159        $defaultArgs = array(
4160
4161            'contactID'   => -1,
4162
4163            'sortByField' => 'ID',
4164            'sortOrder'   => 'ASC',
4165            'page'        => 0,
4166            'perPage'     => 100,
4167
4168            // permissions
4169            'ignoreowner' => false, // this'll let you not-check the owner of obj
4170
4171        );
4172        foreach ( $defaultArgs as $argK => $argV ) {
4173            $$argK = $argV;
4174            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4175                if ( is_array( $args[ $argK ] ) ) {
4176                    $newData = $$argK;
4177                    if ( ! is_array( $newData ) ) {
4178                        $newData = array();
4179                    }
4180                    foreach ( $args[ $argK ] as $subK => $subV ) {
4181                        $newData[ $subK ] = $subV;
4182                    }
4183                    $$argK = $newData;
4184                } else {
4185                    $$argK = $args[ $argK ];
4186                }
4187            }
4188        }
4189        #} =========== / LOAD ARGS =============
4190
4191        #} ========== CHECK FIELDS ============
4192
4193        $contactID = (int) $contactID;
4194
4195        #} ========= / CHECK FIELDS ===========
4196
4197        global $ZBSCRM_t, $wpdb;
4198        $wheres          = array( 'direct' => array() );
4199        $whereStr        = '';
4200        $additionalWhere = '';
4201        $params          = array();
4202        $res             = array();
4203
4204        #} Build query
4205        $query = 'SELECT * FROM ' . $ZBSCRM_t['externalsources'];
4206
4207        #} ============= WHERE ================
4208
4209        #} contactID
4210        if ( ! empty( $contactID ) && $contactID > 0 ) {
4211            $wheres['zbss_objid'] = array( 'zbss_objid', '=', '%d', $contactID );
4212        }
4213
4214        // type
4215        $wheres['zbss_objtype'] = array( 'zbss_objtype', '=', '%d', 1 );
4216
4217        #} ============ / WHERE ===============
4218
4219        #} Build out any WHERE clauses
4220        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
4221        $whereStr  = $wheresArr['where'];
4222        $params    = $params + $wheresArr['params'];
4223        #} / Build WHERE
4224
4225        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
4226        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
4227        $ownQ   = $this->ownershipSQL( $ignoreowner );
4228        if ( ! empty( $ownQ ) ) {
4229            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
4230        }
4231        #} / Ownership
4232
4233        #} Append to sql (this also automatically deals with sortby and paging)
4234        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
4235
4236        try {
4237
4238            #} Prep & run query
4239            $queryObj     = $this->prepare( $query, $params );
4240            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
4241
4242        } catch ( Exception $e ) {
4243
4244            #} General SQL Err
4245            $this->catchSQLError( $e );
4246
4247        }
4248
4249        #} Interpret results (Result Set - multi-row)
4250        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
4251
4252            #} Has results, tidy + return
4253            foreach ( $potentialRes as $resDataLine ) {
4254
4255                    // tidy
4256                    $resArr = $this->DAL()->tidy_externalsource( $resDataLine );
4257
4258                    $res[] = $resArr;
4259
4260            }
4261        }
4262
4263        return $res;
4264    }
4265
4266    /**
4267     * returns tracking detail lines for a contact
4268     *
4269     * @param array $args Associative array of arguments
4270     *              withStats, searchPhrase, sortByField, sortOrder, page, perPage
4271     *
4272     * @return array of tag lines
4273     */
4274    public function getTrackingForContact( $args = array() ) {
4275
4276        global $zbs;
4277
4278        #} ============ LOAD ARGS =============
4279        $defaultArgs = array(
4280
4281            'contactID'   => -1,
4282
4283            // optional
4284            'action'      => '',
4285
4286            'sortByField' => 'ID',
4287            'sortOrder'   => 'ASC',
4288            'page'        => 0,
4289            'perPage'     => 100,
4290
4291            // permissions
4292            'ignoreowner' => false, // this'll let you not-check the owner of obj
4293
4294        );
4295        foreach ( $defaultArgs as $argK => $argV ) {
4296            $$argK = $argV;
4297            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
4298                if ( is_array( $args[ $argK ] ) ) {
4299                    $newData = $$argK;
4300                    if ( ! is_array( $newData ) ) {
4301                        $newData = array();
4302                    }
4303                    foreach ( $args[ $argK ] as $subK => $subV ) {
4304                        $newData[ $subK ] = $subV;
4305                    }
4306                    $$argK = $newData;
4307                } else {
4308                    $$argK = $args[ $argK ];
4309                }
4310            }
4311        }
4312        #} =========== / LOAD ARGS =============
4313
4314        #} ========== CHECK FIELDS ============
4315
4316        $contactID = (int) $contactID;
4317
4318        #} ========= / CHECK FIELDS ===========
4319
4320        global $ZBSCRM_t, $wpdb;
4321        $wheres          = array( 'direct' => array() );
4322        $whereStr        = '';
4323        $additionalWhere = '';
4324        $params          = array();
4325        $res             = array();
4326
4327        #} Build query
4328        $query = 'SELECT * FROM ' . $ZBSCRM_t['tracking'];
4329
4330        #} ============= WHERE ================
4331
4332        #} contactID
4333        if ( ! empty( $contactID ) && $contactID > 0 ) {
4334            $wheres['zbst_contactid'] = array( 'zbst_contactid', '=', '%d', $contactID );
4335        }
4336
4337        #} action
4338        if ( ! empty( $action ) ) {
4339            $wheres['zbst_action'] = array( 'zbst_action', '=', '%s', $action );
4340        }
4341
4342        #} ============ / WHERE ===============
4343
4344        #} Build out any WHERE clauses
4345        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
4346        $whereStr  = $wheresArr['where'];
4347        $params    = $params + $wheresArr['params'];
4348        #} / Build WHERE
4349
4350        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
4351        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
4352        $ownQ   = $this->ownershipSQL( $ignoreowner );
4353        if ( ! empty( $ownQ ) ) {
4354            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
4355        }
4356        #} / Ownership
4357
4358        #} Append to sql (this also automatically deals with sortby and paging)
4359        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
4360
4361        try {
4362
4363            #} Prep & run query
4364            $queryObj     = $this->prepare( $query, $params );
4365            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
4366
4367        } catch ( Exception $e ) {
4368
4369            #} General SQL Err
4370            $this->catchSQLError( $e );
4371
4372        }
4373
4374        #} Interpret results (Result Set - multi-row)
4375        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
4376
4377            #} Has results, tidy + return
4378            foreach ( $potentialRes as $resDataLine ) {
4379
4380                    // tidy
4381                    $resArr = $this->DAL()->tidy_tracking( $resDataLine );
4382
4383                    $res[] = $resArr;
4384
4385            }
4386        }
4387
4388        return $res;
4389    }
4390
4391    /**
4392     * Returns an ownerid against a contact
4393     *
4394     * @param int id Contact ID
4395     *
4396     * @return int contact owner id
4397     */
4398    public function getContactOwner( $id = -1 ) {
4399
4400        global $zbs;
4401
4402        $id = (int) $id;
4403
4404        if ( $id > 0 ) {
4405
4406            return $this->DAL()->getFieldByID(
4407                array(
4408                    'id'          => $id,
4409                    'objtype'     => ZBS_TYPE_CONTACT,
4410                    'colname'     => 'zbs_owner',
4411                    'ignoreowner' => true,
4412                )
4413            );
4414
4415        }
4416
4417        return false;
4418    }
4419
4420    /**
4421     * Returns an status against a contact
4422     *
4423     * @param int $id Contact ID.
4424     *
4425     * @return string contact status string
4426     */
4427    public function getContactStatus( $id = -1 ) {
4428
4429        global $zbs;
4430
4431        $id = (int) $id;
4432
4433        if ( $id > 0 ) {
4434
4435            return $this->DAL()->getFieldByID(
4436                array(
4437                    'id'          => $id,
4438                    'objtype'     => ZBS_TYPE_CONTACT,
4439                    'colname'     => 'zbsc_status',
4440                    'ignoreowner' => true,
4441                )
4442            );
4443
4444        }
4445
4446        return false;
4447    }
4448
4449    /**
4450     * Sets the status of a contact
4451     *
4452     * @param int    $id Contact ID.
4453     * @param string $status Contact status.
4454     *
4455     * @return int|false contact ID if successful, false otherwise
4456     */
4457    public function setContactStatus( $id = -1, $status = -1 ) {
4458
4459        global $zbs;
4460
4461        $id = (int) $id;
4462
4463        if ( $id > 0 && ! empty( $status ) && $status !== -1 ) {
4464
4465            return $this->addUpdateContact(
4466                array(
4467                    'id'            => $id,
4468                    'limitedFields' => array(
4469                        array(
4470                            'key'  => 'zbsc_status',
4471                            'val'  => $status,
4472                            'type' => '%s',
4473                        ),
4474                    ),
4475                )
4476            );
4477
4478        }
4479
4480        return false;
4481    }
4482
4483    /**
4484     * Sets the owner of a contact
4485     *
4486     * @param int id Contact ID
4487     * @param int owner Contact owner
4488     *
4489     * @return int changed
4490     */
4491    public function setContactOwner( $id = -1, $owner = -1 ) {
4492
4493        global $zbs;
4494
4495        $id    = (int) $id;
4496        $owner = (int) $owner;
4497
4498        if ( $id > 0 && $owner > 0 ) {
4499
4500            return $this->addUpdateContact(
4501                array(
4502                    'id'            => $id,
4503                    'limitedFields' => array(
4504                        array(
4505                            'key'  => 'zbs_owner',
4506                            'val'  => $owner,
4507                            'type' => '%d',
4508                        ),
4509                    ),
4510                )
4511            );
4512
4513        }
4514
4515        return false;
4516    }
4517
4518    /**
4519     * Returns an email addr against a contact
4520     * Replaces getContactEmail
4521     *
4522     * @param int id Contact ID
4523     *
4524     * @return string Contact email
4525     */
4526    public function getContactEmail( $id = -1 ) {
4527
4528        global $zbs;
4529
4530        $id = (int) $id;
4531
4532        if ( $id > 0 ) {
4533
4534            return $this->DAL()->getFieldByID(
4535                array(
4536                    'id'          => $id,
4537                    'objtype'     => ZBS_TYPE_CONTACT,
4538                    'colname'     => 'zbsc_email',
4539                    'ignoreowner' => true,
4540                )
4541            );
4542
4543        }
4544
4545        return false;
4546    }
4547
4548    /**
4549     * Updates an email address against a contact
4550     *
4551     * @param int id Contact ID
4552     * @param string            $email_address
4553     *
4554     * @return bool success
4555     */
4556    public function update_contact_email( $id, $email_address ) {
4557
4558        global $zbs;
4559
4560        $id = (int) $id;
4561
4562        if ( $id > 0 && zeroBSCRM_validateEmail( $email_address ) ) {
4563
4564            $this->DAL()->addUpdateContact(
4565                array(
4566                    'id'            => $id,
4567                    'limitedFields' => array(
4568                        array(
4569                            'key'  => 'zbsc_email',
4570                            'val'  => $email_address,
4571                            'type' => '%s',
4572                        ),
4573                    ),
4574                )
4575            );
4576
4577            return true;
4578
4579        }
4580
4581        return false;
4582    }
4583
4584    /**
4585     * Returns all email addrs against a contact
4586     * ... including aliases
4587     *
4588     * @param int id Contact ID
4589     *
4590     * @return array of strings (Contact emails)
4591     */
4592    public function getContactEmails( $id = -1 ) {
4593
4594        global $zbs;
4595
4596        $id     = (int) $id;
4597        $emails = array();
4598
4599        if ( $id > 0 ) {
4600
4601            // main record
4602            $mainEmail = $this->DAL()->getFieldByID(
4603                array(
4604                    'id'          => $id,
4605                    'objtype'     => ZBS_TYPE_CONTACT,
4606                    'colname'     => 'zbsc_email',
4607                    'ignoreowner' => true,
4608                )
4609            );
4610
4611            if ( zeroBSCRM_validateEmail( $mainEmail ) ) {
4612                $emails[] = $mainEmail;
4613            }
4614
4615            // aliases
4616            $aliases = zeroBS_getObjAliases( ZBS_TYPE_CONTACT, $id );
4617            if ( is_array( $aliases ) ) {
4618                foreach ( $aliases as $alias ) {
4619                    if ( ! in_array( $alias['aka_alias'], $emails ) ) {
4620                        $emails[] = $alias['aka_alias'];
4621                    }
4622                }
4623            }
4624        }
4625
4626        return $emails;
4627    }
4628
4629    /**
4630     * Returns an email addr against a contact
4631     * Replaces zeroBS_customerMobile
4632     *
4633     * @param int id Contact ID
4634     *
4635     * @return string Contact email
4636     */
4637    public function getContactMobile( $id = -1 ) {
4638
4639        global $zbs;
4640
4641        $id = (int) $id;
4642
4643        if ( $id > 0 ) {
4644
4645            return $this->DAL()->getFieldByID(
4646                array(
4647                    'id'          => $id,
4648                    'objtype'     => ZBS_TYPE_CONTACT,
4649                    'colname'     => 'zbsc_mobtel',
4650                    'ignoreowner' => true,
4651                )
4652            );
4653
4654        }
4655
4656        return false;
4657    }
4658
4659    /**
4660     * Returns a formatted fullname of a
4661     * Replaces zeroBS_customerName
4662     *
4663     * @param int id Contact ID
4664     * @param array Contact array (if already loaded can pass)
4665     * @param array args (see format_fullname func)
4666     *
4667     * @return string Contact full name
4668     */
4669    public function getContactFullName( $id = -1, $contactArr = false ) {
4670
4671        global $zbs;
4672
4673        $id = (int) $id;
4674
4675        if ( $id > 0 ) {
4676
4677            // get a limited-fields contact obj
4678            $contact = $this->getContact(
4679                $id,
4680                array(
4681                    'withCustomFields' => false,
4682                    'fields'           => array( 'zbsc_prefix', 'zbsc_fname', 'zbsc_lname' ),
4683                    'ignoreowner'      => true,
4684                )
4685            );
4686            if ( isset( $contact ) && is_array( $contact ) && isset( $contact['prefix'] ) ) {
4687                return $this->format_fullname( $contact );
4688            }
4689        } elseif ( is_array( $contactArr ) ) {
4690
4691            // pass through
4692            return $this->format_fullname( $contactArr );
4693
4694        }
4695
4696        return false;
4697    }
4698
4699    /**
4700     * Returns a formatted fullname (optionally including ID + first line of addr)
4701     * Replaces zeroBS_customerName more fully than getContactFullName
4702     * Also replaces zeroBS_getCustomerName
4703     *
4704     * @param int id Contact ID
4705     * @param array Contact array (if already loaded can pass)
4706     * @param array args (see format_fullname func)
4707     *
4708     * @return string Contact full name
4709     */
4710    public function getContactFullNameEtc( $id = -1, $contactArr = false, $args = array() ) {
4711
4712        global $zbs;
4713
4714        $id = (int) $id;
4715
4716        if ( $id > 0 ) {
4717
4718            // get a limited-fields contact obj
4719            $contact = $this->getContact(
4720                $id,
4721                array(
4722                    'withCustomFields' => false,
4723                    'fields'           => array( 'zbsc_addr1', 'zbsc_prefix', 'zbsc_fname', 'zbsc_lname' ),
4724                    'ignoreowner'      => true,
4725                )
4726            );
4727            if ( isset( $contact ) && is_array( $contact ) && isset( $contact['prefix'] ) ) {
4728                return $this->format_name_etc( $contact, $args );
4729            }
4730        } elseif ( is_array( $contactArr ) ) {
4731
4732            // pass through
4733            return $this->format_name_etc( $contactArr, $args );
4734
4735        }
4736
4737        return false;
4738    }
4739
4740    /**
4741     * Returns a formatted name (e.g. Dave Davids) or fallback
4742     * If there is no name, return "Contact #" or a provided hard-coded fallback. Optionally return an email if it exists.
4743     *
4744     * @param int     $id Contact ID
4745     * @param array   $contactArr (if already loaded can pass)
4746     * @param boolean $do_email_fallback
4747     * @param string  $hardcoded_fallback
4748     *
4749     * @return string name or fallback
4750     */
4751    public function getContactNameWithFallback( $id = -1, $contactArr = false, $do_email_fallback = true, $hardcoded_fallback = '' ) {
4752
4753        global $zbs;
4754
4755        $id = (int) $id;
4756
4757        if ( $id > 0 ) {
4758
4759            // get a limited-fields contact obj
4760            $contact = $this->getContact(
4761                $id,
4762                array(
4763                    'withCustomFields' => false,
4764                    'fields'           => array(
4765                        'zbsc_fname',
4766                        'zbsc_lname',
4767                        'zbsc_email',
4768                    ),
4769                    'ignoreowner'      => true,
4770                )
4771            );
4772            if ( isset( $contact ) && is_array( $contact ) ) {
4773                return $this->format_name_with_fallback( $contact, $do_email_fallback, $hardcoded_fallback );
4774            }
4775        } elseif ( is_array( $contactArr ) ) {
4776
4777            // pass through
4778            return $this->format_name_with_fallback( $contactArr, $do_email_fallback, $hardcoded_fallback );
4779
4780        }
4781
4782        return false;
4783    }
4784
4785    /**
4786     * Returns a formatted address of a contact
4787     * Replaces zeroBS_customerAddr
4788     *
4789     * @param int id Contact ID
4790     * @param array Contact array (if already loaded can pass)
4791     * @param array args (see format_address func)
4792     *
4793     * @return string Contact addr html
4794     */
4795    public function getContactAddress( $id = -1, $contactArr = false, $args = array() ) {
4796
4797        global $zbs;
4798
4799        $id = (int) $id;
4800
4801        if ( $id > 0 ) {
4802
4803            // get a limited-fields contact obj
4804            // this is hacky, but basically get whole basic contact record for this for now, because
4805            // this doesn't properly get addr custom fields:
4806            // $contact = $this->getContact($id,array('withCustomFields' => false,'fields'=>$this->field_list_address,'ignoreowner'=>true));
4807            $contact = $this->getContact(
4808                $id,
4809                array(
4810                    'withCustomFields' => true,
4811                    'ignoreowner'      => true,
4812                )
4813            );
4814            if ( isset( $contact ) && is_array( $contact ) && isset( $contact['addr1'] ) ) {
4815                return $this->format_address( $contact, $args );
4816            }
4817        } elseif ( is_array( $contactArr ) ) {
4818
4819            // pass through
4820            return $this->format_address( $contactArr, $args );
4821
4822        }
4823
4824        return false;
4825    }
4826
4827    /**
4828     * Returns a formatted address of a contact (2nd addr)
4829     * Replaces zeroBS_customerAddr
4830     *
4831     * @param int id Contact ID
4832     * @param array Contact array (if already loaded can pass)
4833     * @param array args (see format_address func)
4834     *
4835     * @return string Contact addr html
4836     */
4837    public function getContact2ndAddress( $id = -1, $contactArr = false, $args = array() ) {
4838
4839        global $zbs;
4840
4841        $id = (int) $id;
4842
4843        $args['secondaddr'] = true;
4844
4845        if ( $id > 0 ) {
4846
4847            // get a limited-fields contact obj
4848            // this is hacky, but basically get whole basic contact record for this for now, because
4849            // this doesn't properly get addr custom fields:
4850            // $contact = $this->getContact($id,array('withCustomFields' => false,'fields'=>$this->field_list_address2,'ignoreowner'=>true));
4851            $contact = $this->getContact(
4852                $id,
4853                array(
4854                    'withCustomFields' => true,
4855                    'ignoreowner'      => true,
4856                )
4857            );
4858            if ( isset( $contact ) && is_array( $contact ) && isset( $contact['addr1'] ) ) {
4859                return $this->format_address( $contact, $args );
4860            }
4861        } elseif ( is_array( $contactArr ) ) {
4862
4863            // pass through
4864            return $this->format_address( $contactArr, $args );
4865
4866        }
4867
4868        return false;
4869    }
4870
4871    /**
4872     * Returns a contacts tag array
4873     * Replaces zeroBSCRM_getCustomerTags AND  zeroBSCRM_getContactTagsArr
4874     *
4875     * @param int id Contact ID
4876     *
4877     * @return mixed
4878     */
4879    public function getContactTags( $id = -1 ) {
4880
4881        global $zbs;
4882
4883        $id = (int) $id;
4884
4885        if ( $id > 0 ) {
4886
4887            return $this->DAL()->getTagsForObjID(
4888                array(
4889                    'objtypeid' => ZBS_TYPE_CONTACT,
4890                    'objid'     => $id,
4891                )
4892            );
4893
4894        }
4895
4896        return false;
4897    }
4898
4899    /**
4900     * Returns last contacted uts against a contact
4901     *
4902     * @param int id Contact ID
4903     *
4904     * @return int Contact last contacted date as uts (or -1)
4905     */
4906    public function getContactLastContactUTS( $id = -1 ) {
4907
4908        global $zbs;
4909
4910        $id = (int) $id;
4911
4912        if ( $id > 0 ) {
4913
4914            return $this->DAL()->getFieldByID(
4915                array(
4916                    'id'          => $id,
4917                    'objtype'     => ZBS_TYPE_CONTACT,
4918                    'colname'     => 'zbsc_lastcontacted',
4919                    'ignoreowner' => true,
4920                )
4921            );
4922
4923        }
4924
4925        return false;
4926    }
4927
4928    /**
4929     * updates lastcontacted date for a contact
4930     *
4931     * @param int id Contact ID
4932     * @param int uts last contacted
4933     *
4934     * @return bool
4935     */
4936    public function setContactLastContactUTS( $id = -1, $lastContactedUTS = -1 ) {
4937
4938        global $zbs;
4939
4940        $id = (int) $id;
4941
4942        if ( $id > 0 ) {
4943
4944            return $this->addUpdateContact(
4945                array(
4946                    'id'            => $id,
4947                    'limitedFields' => array(
4948                        array(
4949                            'key'  => 'zbsc_lastcontacted',
4950                            'val'  => $lastContactedUTS,
4951                            'type' => '%d',
4952                        ),
4953                    ),
4954                )
4955            );
4956
4957        }
4958
4959        return false;
4960    }
4961
4962    /**
4963     * Returns a set of social accounts for a contact (tw,li,fb)
4964     *
4965     * @param int id Contact ID
4966     *
4967     * @return array social acc's
4968     */
4969    public function getContactSocials( $id = -1 ) {
4970
4971        global $zbs;
4972
4973        $id = (int) $id;
4974
4975        if ( $id > 0 ) {
4976
4977            // lazy 3 queries, optimise later
4978
4979            $tw = $this->DAL()->getFieldByID(
4980                array(
4981                    'id'          => $id,
4982                    'objtype'     => ZBS_TYPE_CONTACT,
4983                    'colname'     => 'zbsc_tw',
4984                    'ignoreowner' => true,
4985                )
4986            );
4987
4988            $li = $this->DAL()->getFieldByID(
4989                array(
4990                    'id'          => $id,
4991                    'objtype'     => ZBS_TYPE_CONTACT,
4992                    'colname'     => 'zbsc_li',
4993                    'ignoreowner' => true,
4994                )
4995            );
4996
4997            $fb = $this->DAL()->getFieldByID(
4998                array(
4999                    'id'          => $id,
5000                    'objtype'     => ZBS_TYPE_CONTACT,
5001                    'colname'     => 'zbsc_fb',
5002                    'ignoreowner' => true,
5003                )
5004            );
5005
5006            return array(
5007                'tw' => $tw,
5008                'li' => $li,
5009                'fb' => $fb,
5010            );
5011
5012        }
5013
5014        return false;
5015    }
5016
5017    /**
5018     * Returns a linked WP ID against a contact
5019     * Replaces zeroBS_getCustomerWPID
5020     *
5021     * @param int id Contact ID
5022     *
5023     * @return int Contact wp id
5024     */
5025    public function getContactWPID( $id = -1 ) {
5026
5027        global $zbs;
5028
5029        $id = (int) $id;
5030
5031        if ( $id > 0 ) {
5032
5033            return $this->DAL()->getFieldByID(
5034                array(
5035                    'id'          => $id,
5036                    'objtype'     => ZBS_TYPE_CONTACT,
5037                    'colname'     => 'zbsc_wpid',
5038                    'ignoreowner' => true,
5039                )
5040            );
5041
5042        }
5043
5044        return false;
5045    }
5046
5047    /**
5048     * Returns true/false whether or not user has 'do-not-email' flag (from unsub email link click)
5049     *
5050     * @param int id Contact ID
5051     *
5052     * @return bool
5053     */
5054    public function getContactDoNotMail( $id = -1 ) {
5055
5056        global $zbs;
5057
5058        $id = (int) $id;
5059
5060        if ( $id > 0 ) {
5061
5062            return $this->DAL()->meta( ZBS_TYPE_CONTACT, $id, 'do-not-email', false );
5063
5064        }
5065
5066        return false;
5067    }
5068
5069    /**
5070     * updates true/false whether or not user has 'do-not-email' flag (from unsub email link click)
5071     *
5072     * @param int id Contact ID
5073     * @param bool whether or not to set donotmail
5074     *
5075     * @return bool
5076     */
5077    public function setContactDoNotMail( $id = -1, $doNotMail = true ) {
5078
5079        global $zbs;
5080
5081        $id = (int) $id;
5082
5083        if ( $id > 0 ) {
5084
5085            if ( $doNotMail ) {
5086                return $this->DAL()->updateMeta( ZBS_TYPE_CONTACT, $id, 'do-not-email', true );
5087            } else { // remove
5088                return $this->DAL()->deleteMeta(
5089                    array(
5090                        'objtype' => ZBS_TYPE_CONTACT,
5091                        'objid'   => $id,
5092                        'key'     => 'do-not-email',
5093                    )
5094                );
5095            }
5096        }
5097
5098        return false;
5099    }
5100
5101    /**
5102     * Returns an url to contact avatar (Gravatar if not set?)
5103     * For now just returns the field
5104     * Replaces zeroBS_getCustomerIcoHTML?
5105     *
5106     * @param int id Contact ID
5107     *
5108     * @return int Contact wp id
5109     */
5110    public function getContactAvatarURL( $id = -1 ) {
5111
5112        global $zbs;
5113
5114        $id = (int) $id;
5115
5116        if ( $id > 0 ) {
5117
5118            return $this->DAL()->getFieldByID(
5119                array(
5120                    'id'          => $id,
5121                    'objtype'     => ZBS_TYPE_CONTACT,
5122                    'colname'     => 'zbsc_avatar',
5123                    'ignoreowner' => true,
5124                )
5125            );
5126
5127        }
5128
5129        return false;
5130    }
5131
5132    /**
5133     * Returns an url to contact avatar (Gravatar if not set?)
5134     * Or empty if 'show default empty' = false
5135     *
5136     * @param int id Contact ID
5137     * @param bool showPlaceholder does what it says on tin
5138     *
5139     * @return string URL for img
5140     */
5141    public function getContactAvatar( $id = -1, $showPlaceholder = true ) {
5142
5143        global $zbs;
5144
5145        $id = (int) $id;
5146
5147        if ( $id > 0 ) {
5148
5149            $avatarMode = zeroBSCRM_getSetting( 'avatarmode' );
5150            switch ( $avatarMode ) {
5151
5152                case 1: // gravitar
5153                    $potentialEmail = $this->getContactEmail( $id );
5154                    if ( ! empty( $potentialEmail ) ) {
5155                        return zeroBSCRM_getGravatarURLfromEmail( $potentialEmail );
5156                    }
5157
5158                    // default
5159                    return zeroBSCRM_getDefaultContactAvatar();
5160
5161                    break;
5162
5163                case 2: // custom img
5164                    $dbURL = $this->getContactAvatarURL( $id );
5165                    if ( ! empty( $dbURL ) ) {
5166                        return $dbURL;
5167                    }
5168
5169                    // default
5170                    return zeroBSCRM_getDefaultContactAvatar();
5171
5172                    break;
5173
5174                case 3: // none
5175                    return '';
5176                    break;
5177
5178            }
5179        }
5180
5181        // fallback
5182        if ( $showPlaceholder ) {
5183            return zeroBSCRM_getDefaultContactAvatar();
5184        }
5185
5186        return false;
5187    }
5188
5189    /**
5190     * Returns html of contact avatar (Gravatar if not set?)
5191     * Or empty if 'show default empty' = false
5192     *
5193     * @param int id Contact ID
5194     *
5195     * @return string HTML
5196     */
5197    public function getContactAvatarHTML( $id = -1, $size = 100, $extraClasses = '' ) {
5198
5199        $id = (int) $id;
5200
5201        if ( $id > 0 ) {
5202
5203            $avatarMode = zeroBSCRM_getSetting( 'avatarmode' );
5204            switch ( $avatarMode ) {
5205
5206                case 1: // gravitar
5207                    $potentialEmail = $this->getContactEmail( $id );
5208                    if ( ! empty( $potentialEmail ) ) {
5209                        return '<img src="' . zeroBSCRM_getGravatarURLfromEmail( $potentialEmail, $size ) . '" class="' . $extraClasses . ' zbs-gravatar" alt="" />';
5210                    }
5211
5212                    // default
5213                    return zeroBSCRM_getDefaultContactAvatarHTML();
5214
5215                    break;
5216
5217                case 2: // custom img
5218                    $dbURL = $this->getContactAvatarURL( $id );
5219                    if ( ! empty( $dbURL ) ) {
5220                        return '<img src="' . $dbURL . '" class="' . $extraClasses . ' zbs-custom-avatar" alt="" />';
5221                    }
5222
5223                    // default
5224                    return zeroBSCRM_getDefaultContactAvatarHTML();
5225
5226                    break;
5227
5228                case 3: // none
5229                    return '';
5230                    break;
5231
5232            }
5233        }
5234
5235        return '';
5236    }
5237
5238    /**
5239     * Returns a count of contacts (owned)
5240     * Replaces zeroBS_customerCount
5241     *
5242     * @param object $args - DAL args.
5243     *
5244     * @return int count
5245     */
5246    public function getContactCount( $args = array() ) {
5247
5248        #} ============ LOAD ARGS =============
5249        $defaultArgs = array(
5250
5251            // Search/Filtering (leave as false to ignore)
5252            'inCompany'   => false, // will be an ID if used
5253            'withStatus'  => false, // will be str if used
5254
5255            // permissions
5256            'ignoreowner' => true, // this'll let you not-check the owner of obj
5257
5258        );
5259        foreach ( $defaultArgs as $argK => $argV ) {
5260            $$argK = $argV;
5261            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5262                if ( is_array( $args[ $argK ] ) ) {
5263                    $newData = $$argK;
5264                    if ( ! is_array( $newData ) ) {
5265                        $newData = array();
5266                    }
5267                    foreach ( $args[ $argK ] as $subK => $subV ) {
5268                        $newData[ $subK ] = $subV;
5269                    }
5270                    $$argK = $newData;
5271                } else {
5272                    $$argK = $args[ $argK ];
5273                }
5274            }
5275        }
5276        #} =========== / LOAD ARGS =============
5277
5278        $whereArr = array();
5279
5280            // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable, WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
5281        if ( $inCompany ) {
5282            global $ZBSCRM_t;
5283            $whereArr['incompany'] = array( 'ID', 'IN', '(SELECT DISTINCT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objtype_to = ' . ZBS_TYPE_COMPANY . ' AND zbsol_objid_to = %d)', $inCompany );
5284        }
5285
5286        if ( $withStatus !== false && ! empty( $withStatus ) ) {
5287            $whereArr['status'] = array( 'zbsc_status', '=', 'convert(%s using utf8mb4) collate utf8mb4_bin', $withStatus );
5288        }
5289            // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable, WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
5290
5291        return $this->DAL()->getFieldByWHERE(
5292            array(
5293                'objtype'     => ZBS_TYPE_CONTACT,
5294                'colname'     => 'COUNT(ID)',
5295                'where'       => $whereArr,
5296                'ignoreowner' => $ignoreowner,
5297            )
5298        );
5299
5300        return 0;
5301    }
5302
5303    /**
5304     * Returns a customer's associated company ID's
5305     * Replaces zeroBS_getCustomerCompanyID (via LEGACY func)
5306     *
5307     * @param int id
5308     *
5309     * @return array int id
5310     */
5311    public function getContactCompanies( $id = -1 ) {
5312
5313        if ( ! empty( $id ) ) {
5314
5315            /*
5316            $contact = $this->getContact(
5317                $id,
5318                array(
5319                    'withCompanies' => true,
5320                    'fields'        => array( 'ID' ),
5321                )
5322            );
5323
5324            if ( is_array( $contact ) && isset( $contact['companies'] ) ) {
5325                return $contact['companies'];
5326            }
5327            */
5328
5329            // cleaner:
5330            return $this->DAL()->getObjsLinkedToObj(
5331                array(
5332                    'objtypefrom' => ZBS_TYPE_CONTACT, // contact
5333                    'objtypeto'   => ZBS_TYPE_COMPANY, // company
5334                    'objfromid'   => $id,
5335                    'ignoreowner' => true,
5336                )
5337            );
5338
5339        }
5340
5341        return array();
5342    }
5343
5344    /**
5345     * Returns a bool whether contact has a quote linked to them
5346     * NOTE: this only counts objlinks, so if the obj is deleted and they're not tidied, this'll give false positive
5347     * (Shorthand for contactHasObjLink)
5348     *
5349     * @param int contactID
5350     * @param int obj type id
5351     *
5352     * @return bool
5353     */
5354    public function contactHasQuote( $contactID = -1 ) {
5355
5356        if ( $contactID > 0 ) {
5357
5358            // cleaner:
5359            $c = $this->contactHasObjLink( $contactID, ZBS_TYPE_QUOTE );
5360
5361            if ( $c > 0 ) {
5362                return true;
5363            }
5364        }
5365
5366        return false;
5367    }
5368
5369    /**
5370     * Returns a bool whether contact has a Invoice linked to them
5371     * NOTE: this only counts objlinks, so if the obj is deleted and they're not tidied, this'll give false positive
5372     * (Shorthand for contactHasObjLink)
5373     *
5374     * @param int contactID
5375     * @param int obj type id
5376     *
5377     * @return bool
5378     */
5379    public function contactHasInvoice( $contactID = -1 ) {
5380
5381        if ( $contactID > 0 ) {
5382
5383            // cleaner:
5384            $c = $this->contactHasObjLink( $contactID, ZBS_TYPE_INVOICE );
5385
5386            if ( $c > 0 ) {
5387                return true;
5388            }
5389        }
5390
5391        return false;
5392    }
5393
5394    /**
5395     * Returns a bool whether contact has a transaction linked to them
5396     * NOTE: this only counts objlinks, so if the obj is deleted and they're not tidied, this'll give false positive
5397     * (Shorthand for contactHasObjLink)
5398     *
5399     * @param int contactID
5400     * @param int obj type id
5401     *
5402     * @return bool
5403     */
5404    public function contactHasTransaction( $contactID = -1 ) {
5405
5406        if ( $contactID > 0 ) {
5407
5408            // cleaner:
5409            $c = $this->contactHasObjLink( $contactID, ZBS_TYPE_TRANSACTION );
5410
5411            if ( $c > 0 ) {
5412                return true;
5413            }
5414        }
5415
5416        return false;
5417    }
5418
5419    /**
5420     * Returns a bool whether contact has objtype linked to them
5421     * specifically *obj -> THIS (contact)
5422     * NOTE: this only counts objlinks, so if the obj is deleted and they're not tidied, this'll give false positive
5423     *
5424     * @param int id
5425     * @param int obj type id
5426     *
5427     * @return bool
5428     */
5429    private function contactHasObjLink( $id = -1, $objTypeID = -1 ) {
5430
5431        if ( $id > 0 && $objTypeID > 0 ) {
5432
5433            // cleaner:
5434            $c = $this->DAL()->getObjsLinksLinkedToObj(
5435                array(
5436                    'objtypefrom' => $objTypeID, // obj type
5437                    'objtypeto'   => ZBS_TYPE_CONTACT, // contact
5438                    'objtoid'     => $id,
5439                    'count'       => true,
5440                    'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
5441                )
5442            );
5443
5444            if ( $c > 0 ) {
5445                return true;
5446            }
5447        }
5448
5449        return false;
5450    }
5451
5452    /**
5453     * Returns the next customer ID and the previous customer ID
5454     * Used for the navigation between contacts.
5455     *
5456     * @param int id
5457     *
5458     * @return array int id
5459     */
5460    public function getContactPrevNext( $id = -1 ) {
5461
5462        global $ZBSCRM_t, $wpdb;
5463
5464        if ( $id > 0 ) {
5465            // then run the queries..
5466            $nextSQL = $this->prepare( 'SELECT MIN(ID) FROM ' . $ZBSCRM_t['contacts'] . ' WHERE ID > %d', $id );
5467
5468            $res['next'] = $wpdb->get_var( $nextSQL );
5469
5470            $prevSQL = $this->prepare( 'SELECT MAX(ID) FROM ' . $ZBSCRM_t['contacts'] . ' WHERE ID < %d', $id );
5471
5472            $res['prev'] = $wpdb->get_var( $prevSQL );
5473
5474            return $res;
5475
5476        }
5477
5478        return false;
5479    }
5480
5481    /**
5482     * Takes full object and makes a "list view" boiled down version
5483     * Used to generate listview objs
5484     *
5485     * @param array $obj (clean obj)
5486     *
5487     * @return array (listview ready obj)
5488     */
5489    public function listViewObj( $contact = false, $columnsRequired = array() ) {
5490
5491        if ( is_array( $contact ) && isset( $contact['id'] ) ) {
5492
5493            global $zbs;
5494
5495            $resArr = $contact;
5496
5497            $resArr['avatar'] = $zbs->DAL->contacts->getContactAvatar( $resArr['id'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
5498
5499            // use created original $resArr['created'] = zeroBSCRM_date_i18n(-1, $resArr['createduts']);
5500
5501            #} Custom columns
5502
5503            #} Total value
5504            if ( in_array( 'totalvalue', $columnsRequired ) ) {
5505
5506                #} Calc total value + add to return array
5507                $resArr['totalvalue'] = zeroBSCRM_formatCurrency( 0 );
5508                if ( isset( $contact['total_value'] ) ) {
5509                    $resArr['totalvalue'] = zeroBSCRM_formatCurrency( $contact['total_value'] );
5510                }
5511            }
5512
5513            #} Quotes
5514            if ( in_array( 'quotetotal', $columnsRequired ) ) {
5515                if ( isset( $contact['quotes_total'] ) ) {
5516                    $resArr['quotestotal'] = zeroBSCRM_formatCurrency( $contact['quotes_total'] );
5517                } else {
5518                    $resArr['quotestotal'] = zeroBSCRM_formatCurrency( 0 );
5519                }
5520            }
5521
5522            #} Invoices
5523            if ( in_array( 'invoicetotal', $columnsRequired ) ) {
5524                if ( isset( $contact['invoices_total'] ) ) {
5525                    $resArr['invoicestotal'] = zeroBSCRM_formatCurrency( $contact['invoices_total'] );
5526                } else {
5527                    $resArr['invoicestotal'] = zeroBSCRM_formatCurrency( 0 );
5528                }
5529            }
5530
5531            #} Transactions
5532            if ( in_array( 'transactiontotal', $columnsRequired, true ) ) {
5533                if ( isset( $contact['transactions_total'] ) ) {
5534                    $resArr['transactionstotal'] = zeroBSCRM_formatCurrency( $contact['transactions_total'] );
5535                } else {
5536                    $resArr['transactionstotal'] = zeroBSCRM_formatCurrency( 0 );
5537                }
5538            }
5539
5540            // v3.0
5541            if ( isset( $contact['transactions_total'] ) ) {
5542
5543                // DAL2 way, brutal effort.
5544                $resArr['transactions_total'] = zeroBSCRM_formatCurrency( $contact['transactions_total'] );
5545
5546                // also pass total without formatting (used for hastransactions check)
5547                $resArr['transactions_total_value'] = $contact['transactions_total'];
5548
5549            }
5550
5551            #} Company
5552            if ( in_array( 'company', $columnsRequired ) ) {
5553
5554                $resArr['company'] = false;
5555
5556                #} Co Name Default
5557                $coName = '';
5558
5559                // glob as used above 1 step in ajax. not pretty
5560                global $companyNameCache;
5561
5562                // get
5563                $coID = zeroBS_getCustomerCompanyID( $resArr['id'] );// get_post_meta($post->ID,'zbs_company',true);
5564                if ( ! empty( $coID ) ) {
5565
5566                    // cache as we go
5567                    if ( ! isset( $companyNameCache[ $coID ] ) ) {
5568
5569                        // get
5570                        $co = zeroBS_getCompany( $coID );
5571                        if ( isset( $co ) && isset( $co['name'] ) ) {
5572                            $coName = $co['name'];
5573                        }
5574                        if ( empty( $coName ) ) {
5575                            $coName = jpcrm_label_company() . ' #' . $co['id'];
5576                        }
5577
5578                        // cache
5579                        $companyNameCache[ $coID ] = $coName;
5580
5581                    } else {
5582                        $coName = $companyNameCache[ $coID ];
5583                    }
5584                }
5585
5586                if ( $coID > 0 ) {
5587                    $resArr['company'] = array(
5588                        'id'   => $coID,
5589                        'name' => $coName,
5590                    );
5591                }
5592            }
5593
5594            // Object view. Escaping JS for Phone link attr to avoid XSS
5595            // phpcs:disable
5596            $resArr['hometel'] = isset( $resArr['hometel'] ) ? esc_js( $resArr['hometel'] ) : '';
5597            $resArr['worktel'] = isset( $resArr['worktel'] ) ? esc_js( $resArr['worktel'] ) : '';
5598            $resArr['mobtel']  = isset( $resArr['mobtel'] ) ? esc_js( $resArr['mobtel'] ) : '';
5599            // phpcs:enable
5600
5601            return $resArr;
5602        }
5603
5604        return false;
5605    }
5606
5607    // ===============================================================================
5608    // ============  Formatting    ===================================================
5609
5610    /**
5611     * Returns a formatted full name (e.g. Mr. Dave Davids)
5612     *
5613     * @param array $obj (tidied db obj)
5614     *
5615     * @return string fullname
5616     */
5617    public function format_fullname( $contactArr = array() ) {
5618
5619        $usePrefix = zeroBSCRM_getSetting( 'showprefix' );
5620
5621        $str = '';
5622        if ( $usePrefix ) {
5623            if ( isset( $contactArr['prefix'] ) ) {
5624                $str .= $contactArr['prefix'];
5625            }
5626        }
5627
5628        if ( isset( $contactArr['fname'] ) ) {
5629            if ( ! empty( $str ) ) {
5630                $str .= ' ';
5631            }
5632            $str .= $contactArr['fname'];
5633        }
5634        if ( isset( $contactArr['lname'] ) ) {
5635            if ( ! empty( $str ) ) {
5636                $str .= ' ';
5637            }
5638            $str .= $contactArr['lname'];
5639        }
5640
5641        return $str;
5642    }
5643
5644    /**
5645     * Returns a formatted full name +- id, address (e.g. Mr. Dave Davids 12 London Street #23)
5646     * Replaces zeroBS_customerName from DAL1 more realistically than format_fullname
5647     *
5648     * @param array $obj (tidied db obj)
5649     *
5650     * @return string fullname
5651     */
5652    public function format_name_etc( $contactArr = array(), $args = array() ) {
5653
5654        #} =========== LOAD ARGS ==============
5655        $defaultArgs = array(
5656
5657            'incFirstLineAddr' => false,
5658            'incID'            => false,
5659
5660        );
5661        foreach ( $defaultArgs as $argK => $argV ) {
5662            $$argK = $argV;
5663            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
5664                if ( is_array( $args[ $argK ] ) ) {
5665                    $newData = $$argK;
5666                    if ( ! is_array( $newData ) ) {
5667                        $newData = array();
5668                    }
5669                    foreach ( $args[ $argK ] as $subK => $subV ) {
5670                        $newData[ $subK ] = $subV;
5671                    }
5672                    $$argK = $newData;
5673                } else {
5674                    $$argK = $args[ $argK ];
5675                }
5676            }
5677        }
5678        #} =========== / LOAD ARGS =============
5679
5680        // full name first
5681        $str = $this->format_fullname( $contactArr );
5682
5683        // First line of addr?
5684        if ( $incFirstLineAddr ) {
5685            if ( isset( $contactArr['addr1'] ) && ! empty( $contactArr['addr1'] ) ) {
5686                $str .= ' (' . $contactArr['addr1'] . ')';
5687            }
5688        }
5689
5690        // ID?
5691        if ( $incID ) {
5692            $str .= ' #' . $contactArr['id'];
5693        }
5694
5695        return $str;
5696    }
5697
5698    /**
5699     * Returns a formatted name (e.g. Dave Davids) or fallback
5700     * If there is no name, return "Contact #" or a provided hard-coded fallback. Optionally return an email if it exists.
5701     *
5702     * @param array   $contactArr (tidied db obj)
5703     * @param boolean $do_email_fallback
5704     * @param string  $hardcoded_fallback
5705     *
5706     * @return string name or fallback
5707     */
5708    public function format_name_with_fallback( $contactArr = array(), $do_email_fallback = true, $hardcoded_fallback = '' ) {
5709
5710        $str = $this->format_fullname( $contactArr );
5711
5712        if ( $do_email_fallback && empty( $str ) && ! empty( $contactArr['email'] ) ) {
5713            $str = $contactArr['email'];
5714        }
5715
5716        if ( empty( $str ) ) {
5717            if ( ! empty( $hardcoded_fallback ) ) {
5718                return $hardcoded_fallback;
5719            } else {
5720                return __( 'Contact', 'zero-bs-crm' ) . ' #' . $contactArr['id'];
5721            }
5722        }
5723
5724        return $str;
5725    }
5726
5727    // =========== / Formatting    ===================================================
5728    // ===============================================================================
5729} // / class