Passed
Push — master ( f921d8...35bd8d )
by Brian
17:00 queued 11:33
created

WPInv_Invoice::get_special_fields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 31
nc 1
nop 0
dl 0
loc 33
rs 9.424
c 0
b 0
f 0
1
<?php
2
 
3
// MUST have WordPress.
4
if ( !defined( 'WPINC' ) ) {
5
    exit( 'Do NOT access this file directly: ' . basename( __FILE__ ) );
6
}
7
8
/**
9
 * Invoice class.
10
 */
11
class WPInv_Invoice {
12
13
    /**
14
     * @var int the invoice ID.
15
     */
16
    public $ID  = 0;
17
18
    /**
19
     * @var string the invoice title.
20
     */
21
    public $title;
22
23
    /**
24
     * @var string the invoice post type.
25
     */
26
    public $post_type;
27
    
28
    /**
29
     * @var array unsaved changes.
30
     */
31
    public $pending = array();
32
33
    /**
34
     * @var array Invoice items.
35
     */
36
    public $items = array();
37
38
    /**
39
     * @var array User info.
40
     */
41
    public $user_info = array();
42
43
    /**
44
     * @var array Payment meta.
45
     */
46
    public $payment_meta = array();
47
    
48
    /**
49
     * @var bool whether or not the invoice is saved.
50
     */
51
    public $new = false;
52
53
    /**
54
     * @var string Invoice number.
55
     */
56
    public $number = '';
57
58
    /**
59
     * @var string test or live mode.
60
     */
61
    public $mode = 'live';
62
63
    /**
64
     * @var string invoice key.
65
     */
66
    public $key = '';
67
68
    /**
69
     * @var float invoice total.
70
     */
71
    public $total = 0.00;
72
73
    /**
74
     * @var float invoice subtotal.
75
     */
76
    public $subtotal = 0;
77
78
    /**
79
     * @var int 0 = taxable, 1 not taxable.
80
     */
81
    public $disable_taxes = 0;
82
83
    /**
84
     * @var float invoice tax.
85
     */
86
    public $tax = 0;
87
88
    /**
89
     * @var array invoice fees.
90
     */
91
    public $fees = array();
92
93
    /**
94
     * @var float total fees.
95
     */
96
    public $fees_total = 0;
97
98
    /**
99
     * @var array invoice discounts.
100
     */
101
    public $discounts = '';
102
103
    /**
104
     * @var float total discount.
105
     */
106
    public $discount = 0;
107
108
    /**
109
     * @var string discount code.
110
     */
111
    public $discount_code = '';
112
113
    /**
114
     * @var string date created.
115
     */
116
    public $date = '';
117
118
    /**
119
     * @var string date due.
120
     */
121
    public $due_date = '';
122
123
    /**
124
     * @var string date it was completed.
125
     */
126
    public $completed_date = '';
127
128
    /**
129
     * @var string invoice status.
130
     */
131
    public $status = 'wpi-pending';
132
133
    /**
134
     * @var string invoice status.
135
     */
136
    public $post_status = 'wpi-pending';
137
138
    /**
139
     * @var string old invoice status.
140
     */
141
    public $old_status = '';
142
143
    /**
144
     * @var string formatted invoice status.
145
     */
146
    public $status_nicename = '';
147
148
    /**
149
     * @var int invoice user id.
150
     */
151
    public $user_id = 0;
152
153
    /**
154
     * @var string user first name.
155
     */
156
    public $first_name = '';
157
158
    /**
159
     * @var string user last name.
160
     */
161
    public $last_name = '';
162
163
    /**
164
     * @var string user email.
165
     */
166
    public $email = '';
167
168
    /**
169
     * @var string user phone number.
170
     */
171
    public $phone = '';
172
173
    /**
174
     * @var string user address.
175
     */
176
    public $address = '';
177
178
    /**
179
     * @var string user city.
180
     */
181
    public $city = '';
182
183
    /**
184
     * @var string user country.
185
     */
186
    public $country = '';
187
188
    /**
189
     * @var string user state.
190
     */
191
    public $state = '';
192
193
    /**
194
     * @var string user zip.
195
     */
196
    public $zip = '';
197
198
    /**
199
     * @var string transaction id.
200
     */
201
    public $transaction_id = '';
202
203
    /**
204
     * @var string user ip.
205
     */
206
    public $ip = '';
207
208
    /**
209
     * @var string gateway.
210
     */
211
    public $gateway = '';
212
213
    /**
214
     * @var string gateway title.
215
     */
216
    public $gateway_title = '';
217
218
    /**
219
     * @var string currency.
220
     */
221
    public $currency = '';
222
223
    /**
224
     * @var array cart_details.
225
     */
226
    public $cart_details = array();
227
    
228
    /**
229
     * @var string company.
230
     */
231
    public $company = '';
232
233
    /**
234
     * @var string vat number.
235
     */
236
    public $vat_number = '';
237
238
    /**
239
     * @var string vat rate.
240
     */
241
    public $vat_rate = '';
242
243
    /**
244
     * @var int whether or not the address is confirmed.
245
     */
246
    public $adddress_confirmed = '';
247
248
    /**
249
     * @var string full name.
250
     */
251
    public $full_name = '';
252
253
    /**
254
     * @var int parent.
255
     */
256
    public $parent_invoice = 0;
257
258
    /**
259
     * @param int|WPInv_Invoice|WP_Post $invoice The invoice.
260
     */
261
    public function __construct( $invoice = false ) {
262
        
263
        // Do we have an invoice?
264
        if ( empty( $invoice ) ) {
265
            return false;
266
        }
267
268
        $this->setup_invoice( $invoice );
269
    }
270
271
    /**
272
     * Retrieves an invoice key.
273
     */
274
    public function get( $key ) {
275
        if ( method_exists( $this, 'get_' . $key ) ) {
276
            $value = call_user_func( array( $this, 'get_' . $key ) );
277
        } else {
278
            $value = $this->$key;
279
        }
280
281
        return $value;
282
    }
283
284
     /**
285
     * Sets an invoice key.
286
     */
287
    public function set( $key, $value ) {
288
        $ignore = array( 'items', 'cart_details', 'fees', '_ID' );
289
290
        if ( $key === 'status' ) {
291
            $this->old_status = $this->status;
292
        }
293
294
        if ( ! in_array( $key, $ignore ) ) {
295
            $this->pending[ $key ] = $value;
296
        }
297
298
        if( '_ID' !== $key ) {
299
            $this->$key = $value;
300
        }
301
    }
302
303
    /**
304
     * Checks if an invoice key is set.
305
     */
306
    public function _isset( $name ) {
307
        if ( property_exists( $this, $name) ) {
308
            return false === empty( $this->$name );
309
        } else {
310
            return null;
311
        }
312
    }
313
314
    /**
315
     * @param int|WPInv_Invoice|WP_Post $invoice The invoice.
316
     */
317
    private function setup_invoice( $invoice ) {
318
        global $wpdb;
319
        $this->pending = array();
320
321
        if ( empty( $invoice ) ) {
322
            return false;
323
        }
324
325
        if ( is_a( $invoice, 'WPInv_Invoice' ) ) {
326
            foreach ( get_object_vars( $invoice ) as $prop => $value ) {
327
                $this->$prop = $value;
328
            }
329
            return true;
330
        }
331
332
        // Retrieve post object.
333
        $invoice      = get_post( $invoice );
334
335
        if( ! $invoice || is_wp_error( $invoice ) ) {
0 ignored issues
show
introduced by
$invoice is of type WP_Post, thus it always evaluated to true.
Loading history...
336
            return false;
337
        }
338
339
        if( ! ( 'wpi_invoice' == $invoice->post_type OR 'wpi_quote' == $invoice->post_type ) ) {
340
            return false;
341
        }
342
343
        // Retrieve post data.
344
        $table = $wpdb->prefix . 'getpaid_invoices';
345
        $data  = $wpdb->get_row(
346
            $wpdb->prepare( "SELECT * FROM $table WHERE post_id=%d", $invoice->ID )
347
        );
348
349
        do_action( 'wpinv_pre_setup_invoice', $this, $invoice->ID, $data );
350
351
        // Primary Identifier
352
        $this->ID              = absint( $invoice->ID );
353
        $this->post_type       = $invoice->post_type;
354
355
        $this->date            = $invoice->post_date;
356
        $this->status          = $invoice->post_status;
357
358
        if ( 'future' == $this->status ) {
359
            $this->status = 'publish';
360
        }
361
362
        $this->post_status     = $this->status;
363
        $this->parent_invoice  = $invoice->post_parent;
364
        $this->post_name       = $this->setup_post_name( $invoice );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->post_name is correct as $this->setup_post_name($invoice) targeting WPInv_Invoice::setup_post_name() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug Best Practice introduced by
The property post_name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
365
        $this->status_nicename = $this->setup_status_nicename( $invoice->post_status );
366
367
        $this->user_id         = ! empty( $invoice->post_author ) ? $invoice->post_author : get_current_user_id();
0 ignored issues
show
Documentation Bug introduced by
It seems like ! empty($invoice->post_a...: get_current_user_id() can also be of type string. However, the property $user_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
368
        $this->email           = get_the_author_meta( 'email', $this->user_id );
0 ignored issues
show
Bug introduced by
It seems like $this->user_id can also be of type string; however, parameter $user_id of get_the_author_meta() does only seem to accept false|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

368
        $this->email           = get_the_author_meta( 'email', /** @scrutinizer ignore-type */ $this->user_id );
Loading history...
369
        $this->currency        = wpinv_get_currency();
370
        $this->setup_invoice_data( $data );
371
372
        // Other Identifiers
373
        $this->title           = ! empty( $invoice->post_title ) ? $invoice->post_title : $this->number;
374
375
        // Allow extensions to add items to this object via hook
376
        do_action( 'wpinv_setup_invoice', $this, $invoice->ID, $data );
377
378
        return true;
379
    }
380
381
    /**
382
     * @param stdClass $data The invoice data.
383
     */
384
    private function setup_invoice_data( $data ) {
385
386
        if ( empty( $data ) ) {
387
            $this->number = $this->setup_invoice_number( $data );
388
            return;
389
        }
390
391
        $data = map_deep( $data, 'maybe_unserialize' );
392
393
        $this->payment_meta    = is_array( $data->custom_meta ) ? $data->custom_meta : array();
394
        $this->due_date        = $data->due_date;
395
        $this->completed_date  = $data->completed_date;
396
        $this->mode            = $data->mode;
397
398
        // Items
399
        $this->fees            = $this->setup_fees();
400
        $this->cart_details    = $this->setup_cart_details();
401
        $this->items           = ! empty( $this->payment_meta['items'] ) ? $this->payment_meta['items'] : array();
402
403
        // Currency Based
404
        $this->total           = $data->total;
405
        $this->disable_taxes   = (int) $data->disable_taxes;
406
        $this->tax             = $data->tax;
407
        $this->fees_total      = $data->fees_total;
408
        $this->subtotal        = $data->subtotal;
409
        $this->currency        = empty( $data->currency ) ? wpinv_get_currency() : $data->currency ;
410
411
        // Gateway based
412
        $this->gateway         = $data->gateway;
413
        $this->gateway_title   = $this->setup_gateway_title();
414
        $this->transaction_id  = $data->transaction_id;
415
416
        // User based
417
        $this->ip              = $data->user_ip;
418
        $this->user_info       = ! empty( $this->payment_meta['user_info'] ) ? $this->payment_meta['user_info'] : array();
419
420
        $this->first_name      = $data->first_name;
421
        $this->last_name       = $data->last_name;
422
        $this->company         = $data->company;
423
        $this->vat_number      = $data->vat_number;
424
        $this->vat_rate        = $data->vat_rate;
425
        $this->adddress_confirmed  = (int) $data->adddress_confirmed;
426
        $this->address         = $data->address;
427
        $this->city            = $data->city;
428
        $this->country         = $data->country;
429
        $this->state           = $data->state;
430
        $this->zip             = $data->zip;
431
        $this->phone           = ! empty( $this->user_info['phone'] ) ? $this->user_info['phone'] : '';
432
433
        $this->discounts       = ! empty( $this->user_info['discount'] ) ? $this->user_info['discount'] : '';
0 ignored issues
show
Documentation Bug introduced by
It seems like ! empty($this->user_info...r_info['discount'] : '' can also be of type string. However, the property $discounts is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
434
        $this->discount        = $data->discount;
435
        $this->discount_code   = $data->discount_code;
436
437
        // Other Identifiers
438
        $this->key             = $data->key;
439
        $this->number          = $this->setup_invoice_number( $data );
440
441
        $this->full_name       = trim( $this->first_name . ' '. $this->last_name );
442
443
444
        return true;
445
    }
446
447
448
    /**
449
     * Sets up the status nice name.
450
     */
451
    private function setup_status_nicename( $status ) {
452
        $all_invoice_statuses  = wpinv_get_invoice_statuses( true, true, $this );
453
454
        if ( $this->is_quote() && class_exists( 'Wpinv_Quotes_Shared' ) ) {
455
            $all_invoice_statuses  = Wpinv_Quotes_Shared::wpinv_get_quote_statuses();
0 ignored issues
show
Bug introduced by
The type Wpinv_Quotes_Shared was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
456
        }
457
        $status   = isset( $all_invoice_statuses[$status] ) ? $all_invoice_statuses[$status] : __( $status, 'invoicing' );
458
459
        return apply_filters( 'setup_status_nicename', $status );
460
    }
461
462
    /**
463
     * Set's up the invoice number.
464
     */
465
    private function setup_invoice_number( $data ) {
466
467
        if ( ! empty( $data ) && ! empty( $data->number ) ) {
468
            return $data->number;
469
        }
470
471
        $number = $this->ID;
472
473
        if ( $this->status == 'auto-draft' && wpinv_sequential_number_active( $this->post_type ) ) {
474
            $next_number = wpinv_get_next_invoice_number( $this->post_type );
475
            $number      = $next_number;
476
        }
477
        
478
        return wpinv_format_invoice_number( $number, $this->post_type );
479
480
    }
481
482
    /**
483
     * Invoice's post name.
484
     */
485
    private function setup_post_name( $post = NULL ) {
486
        global $wpdb;
487
        
488
        $post_name = '';
489
490
        if ( !empty( $post ) ) {
491
            if( !empty( $post->post_name ) ) {
492
                $post_name = $post->post_name;
493
            } else if ( !empty( $post->ID ) ) {
494
                $post_name = wpinv_generate_post_name( $post->ID );
495
496
                $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) );
497
            }
498
        }
499
500
        $this->post_name = $post_name;
0 ignored issues
show
Bug Best Practice introduced by
The property post_name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
501
    }
502
503
    /**
504
     * Set's up the cart details.
505
     */
506
    public function setup_cart_details() {
507
        global $wpdb;
508
509
        $table =  $wpdb->prefix . 'getpaid_invoice_items';
510
        $items = $wpdb->get_results(
511
            $wpdb->prepare( "SELECT * FROM $table WHERE `post_id`=%d", $this->ID )
512
        );
513
514
        if ( empty( $items ) ) {
515
            return array();
516
        }
517
518
        $details = array();
519
520
        foreach ( $items as $item ) {
521
            $item = (array) $item;
522
            $details[] = array(
523
                'name'          => $item['item_name'],
524
                'id'            => $item['item_id'],
525
                'item_price'    => $item['item_price'],
526
                'custom_price'  => $item['custom_price'],
527
                'quantity'      => $item['quantity'],
528
                'discount'      => $item['discount'],
529
                'subtotal'      => $item['subtotal'],
530
                'tax'           => $item['tax'],
531
                'price'         => $item['price'],
532
                'vat_rate'      => $item['vat_rate'],
533
                'vat_class'     => $item['vat_class'],
534
                'meta'          => $item['meta'],
535
                'fees'          => $item['fees'],
536
            );
537
        }
538
539
        return map_deep( $details, 'maybe_unserialize' );
540
541
    }
542
543
    /**
544
     * Convert this to an array.
545
     */
546
    public function array_convert() {
547
        return get_object_vars( $this );
548
    }
549
    
550
    private function setup_fees() {
551
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
552
        return $payment_fees;
553
    }
554
555
    private function setup_gateway_title() {
556
        $gateway_title = wpinv_get_gateway_checkout_label( $this->gateway );
557
        return $gateway_title;
558
    }
559
    
560
    /**
561
     * Refreshes payment data.
562
     */
563
    private function refresh_payment_data() {
564
565
        $payment_data = array(
566
            'price'        => $this->total,
567
            'date'         => $this->date,
568
            'user_email'   => $this->email,
569
            'invoice_key'  => $this->key,
570
            'currency'     => $this->currency,
571
            'items'        => $this->items,
572
            'user_info' => array(
573
                'user_id'    => $this->user_id,
574
                'email'      => $this->email,
575
                'first_name' => $this->first_name,
576
                'last_name'  => $this->last_name,
577
                'address'    => $this->address,
578
                'phone'      => $this->phone,
579
                'city'       => $this->city,
580
                'country'    => $this->country,
581
                'state'      => $this->state,
582
                'zip'        => $this->zip,
583
                'company'    => $this->company,
584
                'vat_number' => $this->vat_number,
585
                'discount'   => $this->discounts,
586
            ),
587
            'cart_details' => $this->cart_details,
588
            'status'       => $this->status,
589
            'fees'         => $this->fees,
590
        );
591
592
        $this->payment_meta = array_merge( $this->payment_meta, $payment_data );
593
594
    }
595
596
    private function insert_invoice() {
597
598
        if ( empty( $this->post_type ) ) {
599
            if ( !empty( $this->ID ) && $post_type = get_post_type( $this->ID ) ) {
600
                $this->post_type = $post_type;
601
            } else if ( !empty( $this->parent_invoice ) && $post_type = get_post_type( $this->parent_invoice ) ) {
602
                $this->post_type = $post_type;
603
            } else {
604
                $this->post_type = 'wpi_invoice';
605
            }
606
        }
607
608
        $invoice_number = $this->ID;
609
        if ( $number = $this->number ) {
610
            $invoice_number = $number;
611
        }
612
613
        if ( empty( $this->key ) ) {
614
            $this->key = self::generate_key();
0 ignored issues
show
Bug Best Practice introduced by
The method WPInv_Invoice::generate_key() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

614
            /** @scrutinizer ignore-call */ 
615
            $this->key = self::generate_key();
Loading history...
615
            $this->pending['key'] = $this->key;
616
        }
617
618
        if ( empty( $this->ip ) ) {
619
            $this->ip = wpinv_get_ip();
620
            $this->pending['ip'] = $this->ip;
621
        }
622
623
        $payment_data = array(
624
            'price'        => $this->total,
625
            'date'         => $this->date,
626
            'user_email'   => $this->email,
627
            'invoice_key'  => $this->key,
628
            'currency'     => $this->currency,
629
            'items'        => $this->items,
630
            'user_info' => array(
631
                'user_id'    => $this->user_id,
632
                'email'      => $this->email,
633
                'first_name' => $this->first_name,
634
                'last_name'  => $this->last_name,
635
                'address'    => $this->address,
636
                'phone'      => $this->phone,
637
                'city'       => $this->city,
638
                'country'    => $this->country,
639
                'state'      => $this->state,
640
                'zip'        => $this->zip,
641
                'company'    => $this->company,
642
                'vat_number' => $this->vat_number,
643
                'discount'   => $this->discounts,
644
            ),
645
            'cart_details' => $this->cart_details,
646
            'status'       => $this->status,
647
            'fees'         => $this->fees,
648
        );
649
650
        $post_data = array(
651
            'post_title'    => $invoice_number,
652
            'post_status'   => $this->status,
653
            'post_author'   => $this->user_id,
654
            'post_type'     => $this->post_type,
655
            'post_date'     => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? $this->date : current_time( 'mysql' ),
656
            'post_date_gmt' => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? get_gmt_from_date( $this->date ) : current_time( 'mysql', 1 ),
657
            'post_parent'   => $this->parent_invoice,
658
        );
659
        $args = apply_filters( 'wpinv_insert_invoice_args', $post_data, $this );
660
661
        // Create a blank invoice
662
        if ( !empty( $this->ID ) ) {
663
            $args['ID']         = $this->ID;
664
            $invoice_id = wp_update_post( $args, true );
665
        } else {
666
            $invoice_id = wp_insert_post( $args, true );
667
        }
668
669
        if ( is_wp_error( $invoice_id ) ) {
670
            return false;
671
        }
672
673
        if ( ! empty( $invoice_id ) ) {
674
            $this->ID  = $invoice_id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $invoice_id can also be of type WP_Error. However, the property $ID is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
675
            $this->_ID = $invoice_id;
0 ignored issues
show
Bug Best Practice introduced by
The property _ID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
676
677
            $this->payment_meta = array_merge( $this->payment_meta, $payment_data );
678
679
            if ( ! empty( $this->payment_meta['fees'] ) ) {
680
                $this->fees = array_merge( $this->fees, $this->payment_meta['fees'] );
681
                foreach( $this->fees as $fee ) {
682
                    $this->increase_fees( $fee['amount'] );
683
                }
684
            }
685
686
            $this->pending['payment_meta'] = $this->payment_meta;
687
            $this->save();
688
        }
689
690
        return $this->ID;
691
    }
692
693
    /**
694
     * Saves special fields in our custom table.
695
     */
696
    public function get_special_fields() {
697
698
        return array (
699
            'post_id'        => $this->ID,
700
            'number'         => $this->get_number(),
701
            'key'            => $this->get_key(),
702
            'type'           => str_replace( 'wpi_', '', $this->post_type ),
703
            'mode'           => $this->mode,
704
            'user_ip'        => $this->get_ip(),
705
            'first_name'     => $this->get_first_name(),
706
            'last_name'      => $this->get_last_name(),
707
            'address'        => $this->get_address(),
708
            'city'           => $this->city,
709
            'state'          => $this->state,
710
            'country'        => $this->country,
711
            'zip'            => $this->zip,
712
            'adddress_confirmed' => (int) $this->adddress_confirmed,
713
            'gateway'        => $this->get_gateway(),
714
            'transaction_id' => $this->get_transaction_id(),
715
            'currency'       => $this->get_currency(),
716
            'subtotal'       => $this->get_subtotal(),
717
            'tax'            => $this->get_tax(),
718
            'fees_total'     => $this->get_fees_total(),
719
            'total'          => $this->get_total(),
720
            'discount'       => $this->get_discount(),
721
            'discount_code'  => $this->get_discount_code(),
722
            'disable_taxes'  => $this->disable_taxes,
723
            'due_date'       => $this->get_due_date(),
724
            'completed_date' => $this->get_completed_date(),
725
            'company'        => $this->company,
726
            'vat_number'     => $this->vat_number,
727
            'vat_rate'       => $this->vat_rate,
728
            'custom_meta'    => $this->payment_meta
729
        );
730
731
    }
732
733
    /**
734
     * Saves special fields in our custom table.
735
     */
736
    public function save_special() {
737
        global $wpdb;
738
739
        $this->refresh_payment_data();
740
741
        $fields = $this->get_special_fields();
742
        $fields = array_map( 'maybe_serialize', $fields );
743
744
        $table =  $wpdb->prefix . 'getpaid_invoices';
745
746
        $id = (int) $this->ID;
747
748
        if ( empty( $id ) ) {
749
            return;
750
        }
751
752
        if ( $wpdb->get_var( "SELECT `post_id` FROM $table WHERE `post_id`=$id" ) ) {
753
754
            $wpdb->update( $table, $fields, array( 'post_id' => $id ) );
755
756
        } else {
757
758
            $wpdb->insert( $table, $fields );
759
760
        }
761
762
        $table =  $wpdb->prefix . 'getpaid_invoice_items';
763
        $wpdb->delete( $table, array( 'post_id' => $this->ID ) );
764
765
        foreach ( $this->get_cart_details() as $details ) {
766
            $fields = array(
767
                'post_id'          => $this->ID,
768
                'item_id'          => $details['id'],
769
                'item_name'        => $details['name'],
770
                'item_description' => empty( $details['meta']['description'] ) ? '' : $details['meta']['description'],
771
                'vat_rate'         => $details['vat_rate'],
772
                'vat_class'        => empty( $details['vat_class'] ) ? '_standard' : $details['vat_class'],
773
                'tax'              => $details['tax'],
774
                'item_price'       => $details['item_price'],
775
                'custom_price'     => $details['custom_price'],
776
                'quantity'         => $details['quantity'],
777
                'discount'         => $details['discount'],
778
                'subtotal'         => $details['subtotal'],
779
                'price'            => $details['price'],
780
                'meta'             => $details['meta'],
781
                'fees'             => $details['fees'],
782
            );
783
784
            $item_columns = array_keys ( $fields );
785
786
            foreach ( $fields as $key => $val ) {
787
                if ( is_null( $val ) ) {
788
                    $val = '';
789
                }
790
                $val = maybe_serialize( $val );
791
                $fields[ $key ] = $wpdb->prepare( '%s', $val );
792
            }
793
794
            $fields = implode( ', ', $fields );
795
            $item_rows[] = "($fields)";
796
        }
797
798
        $item_rows    = implode( ', ', $item_rows );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $item_rows seems to be defined by a foreach iteration on line 765. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
799
        $item_columns = implode( ', ', $item_columns );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $item_columns seems to be defined by a foreach iteration on line 765. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
800
        $wpdb->query( "INSERT INTO $table ($item_columns) VALUES $item_rows" );
801
    }
802
803
    public function save( $setup = false ) {
804
        global $wpi_session;
805
        
806
        $saved = false;
807
        if ( empty( $this->items ) ) {
808
            return $saved;
809
        }
810
811
        if ( empty( $this->key ) ) {
812
            $this->key = self::generate_key();
0 ignored issues
show
Bug Best Practice introduced by
The method WPInv_Invoice::generate_key() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

812
            /** @scrutinizer ignore-call */ 
813
            $this->key = self::generate_key();
Loading history...
813
        }
814
815
        if ( empty( $this->ID ) ) {
816
            $invoice_id = $this->insert_invoice();
817
818
            if ( false === $invoice_id ) {
819
                $saved = false;
820
            } else {
821
                $this->ID = $invoice_id;
822
            }
823
        }
824
825
        // If we have something pending, let's save it
826
        if ( ! empty( $this->pending ) ) {
827
            $total_increase = 0;
828
            $total_decrease = 0;
829
830
            foreach ( $this->pending as $key => $value ) {
831
832
                switch( $key ) {
833
                    case 'items':
834
                        // Update totals for pending items
835
                        foreach ( $this->pending[ $key ] as $item ) {
836
                            switch( $item['action'] ) {
837
                                case 'add':
838
                                    $price = $item['price'];
839
                                    $taxes = $item['tax'];
0 ignored issues
show
Unused Code introduced by
The assignment to $taxes is dead and can be removed.
Loading history...
840
841
                                    if ( 'publish' === $this->status ) {
842
                                        $total_increase += $price;
843
                                    }
844
                                    break;
845
846
                                case 'remove':
847
                                    if ( 'publish' === $this->status ) {
848
                                        $total_decrease += $item['price'];
849
                                    }
850
                                    break;
851
                            }
852
                        }
853
                        break;
854
                    case 'fees':
855
                        if ( 'publish' !== $this->status ) {
856
                            break;
857
                        }
858
859
                        if ( empty( $this->pending[ $key ] ) ) {
860
                            break;
861
                        }
862
863
                        foreach ( $this->pending[ $key ] as $fee ) {
864
                            switch( $fee['action'] ) {
865
                                case 'add':
866
                                    $total_increase += $fee['amount'];
867
                                    break;
868
869
                                case 'remove':
870
                                    $total_decrease += $fee['amount'];
871
                                    break;
872
                            }
873
                        }
874
                        break;
875
                    case 'status':
876
                        $this->update_status( $this->status );
877
                        break;
878
                    case 'first_name':
879
                        $this->user_info['first_name'] = $this->first_name;
880
                        break;
881
                    case 'last_name':
882
                        $this->user_info['last_name'] = $this->last_name;
883
                        break;
884
                    case 'phone':
885
                        $this->user_info['phone'] = $this->phone;
886
                        break;
887
                    case 'address':
888
                        $this->user_info['address'] = $this->address;
889
                        break;
890
                    case 'city':
891
                        $this->user_info['city'] = $this->city;
892
                        break;
893
                    case 'country':
894
                        $this->user_info['country'] = $this->country;
895
                        break;
896
                    case 'state':
897
                        $this->user_info['state'] = $this->state;
898
                        break;
899
                    case 'zip':
900
                        $this->user_info['zip'] = $this->zip;
901
                        break;
902
                    case 'company':
903
                        $this->user_info['company'] = $this->company;
904
                        break;
905
                    case 'vat_number':
906
                        $this->user_info['vat_number'] = $this->vat_number;
907
                        
908
                        $vat_info = $wpi_session->get( 'user_vat_data' );
909
                        if ( $this->vat_number && !empty( $vat_info ) && isset( $vat_info['number'] ) && isset( $vat_info['valid'] ) && $vat_info['number'] == $this->vat_number ) {
910
                            $adddress_confirmed = isset( $vat_info['adddress_confirmed'] ) ? $vat_info['adddress_confirmed'] : false;
911
                            $this->update_meta( '_wpinv_adddress_confirmed', (bool)$adddress_confirmed );
912
                            $this->user_info['adddress_confirmed'] = (bool)$adddress_confirmed;
913
                            $this->adddress_confirmed = (bool)$adddress_confirmed;
0 ignored issues
show
Documentation Bug introduced by
The property $adddress_confirmed was declared of type integer, but (bool)$adddress_confirmed is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
914
                        }
915
    
916
                        break;
917
                    case 'vat_rate':
918
                        $this->user_info['vat_rate'] = $this->vat_rate;
919
                        break;
920
                    case 'adddress_confirmed':
921
                        $this->user_info['adddress_confirmed'] = $this->adddress_confirmed;
922
                        break;
923
                    case 'date':
924
                        $args = array(
925
                            'ID'        => $this->ID,
926
                            'post_date' => $this->date,
927
                            'edit_date' => true,
928
                        );
929
930
                        wp_update_post( $args );
931
                        break;
932
                    case 'due_date':
933
                        if ( empty( $this->due_date ) ) {
934
                            $this->due_date = 'none';
935
                        }
936
                        break;
937
                    case 'discounts':
938
                        if ( ! is_array( $this->discounts ) ) {
939
                            $this->discounts = explode( ',', $this->discounts );
940
                        }
941
942
                        $this->user_info['discount'] = implode( ',', $this->discounts );
943
                        break;
944
                    case 'parent_invoice':
945
                        $args = array(
946
                            'ID'          => $this->ID,
947
                            'post_parent' => $this->parent_invoice,
948
                        );
949
                        wp_update_post( $args );
950
                        break;
951
                    default:
952
                        do_action( 'wpinv_save', $this, $key );
953
                        break;
954
                }
955
            }
956
957
            $this->items    = array_values( $this->items );
958
959
            $this->pending      = array();
960
            $saved              = true;
961
        }
962
963
        $new_meta = array(
964
            'items'         => $this->items,
965
            'cart_details'  => $this->cart_details,
966
            'fees'          => $this->fees,
967
            'currency'      => $this->currency,
968
            'user_info'     => $this->user_info,
969
        );
970
        $this->payment_meta = array_merge( $this->payment_meta, $new_meta );
971
        $this->update_items();
972
973
        $this->save_special();
974
        do_action( 'wpinv_invoice_save', $this, $saved );
975
976
        if ( true === $saved || $setup ) {
977
            $this->setup_invoice( $this->ID );
978
        }
979
980
        $this->refresh_item_ids();
981
982
        return $saved;
983
    }
984
    
985
    public function add_fee( $args, $global = true ) {
986
        $default_args = array(
987
            'label'       => '',
988
            'amount'      => 0,
989
            'type'        => 'fee',
990
            'id'          => '',
991
            'no_tax'      => false,
992
            'item_id'     => 0,
993
        );
994
995
        $fee = wp_parse_args( $args, $default_args );
996
        
997
        if ( empty( $fee['label'] ) ) {
998
            return false;
999
        }
1000
        
1001
        $fee['id']  = sanitize_title( $fee['label'] );
1002
        
1003
        $this->fees[]               = $fee;
1004
        
1005
        $added_fee               = $fee;
1006
        $added_fee['action']     = 'add';
1007
        $this->pending['fees'][] = $added_fee;
1008
        reset( $this->fees );
1009
1010
        $this->increase_fees( $fee['amount'] );
1011
        return true;
1012
    }
1013
1014
    public function remove_fee( $key ) {
1015
        $removed = false;
1016
1017
        if ( is_numeric( $key ) ) {
1018
            $removed = $this->remove_fee_by( 'index', $key );
1019
        }
1020
1021
        return $removed;
1022
    }
1023
1024
    public function remove_fee_by( $key, $value, $global = false ) {
1025
        $allowed_fee_keys = apply_filters( 'wpinv_fee_keys', array(
1026
            'index', 'label', 'amount', 'type',
1027
        ) );
1028
1029
        if ( ! in_array( $key, $allowed_fee_keys ) ) {
1030
            return false;
1031
        }
1032
1033
        $removed = false;
1034
        if ( 'index' === $key && array_key_exists( $value, $this->fees ) ) {
1035
            $removed_fee             = $this->fees[ $value ];
1036
            $removed_fee['action']   = 'remove';
1037
            $this->pending['fees'][] = $removed_fee;
1038
1039
            $this->decrease_fees( $removed_fee['amount'] );
1040
1041
            unset( $this->fees[ $value ] );
1042
            $removed = true;
1043
        } else if ( 'index' !== $key ) {
1044
            foreach ( $this->fees as $index => $fee ) {
1045
                if ( isset( $fee[ $key ] ) && $fee[ $key ] == $value ) {
1046
                    $removed_fee             = $fee;
1047
                    $removed_fee['action']   = 'remove';
1048
                    $this->pending['fees'][] = $removed_fee;
1049
1050
                    $this->decrease_fees( $removed_fee['amount'] );
1051
1052
                    unset( $this->fees[ $index ] );
1053
                    $removed = true;
1054
1055
                    if ( false === $global ) {
1056
                        break;
1057
                    }
1058
                }
1059
            }
1060
        }
1061
1062
        if ( true === $removed ) {
1063
            $this->fees = array_values( $this->fees );
1064
        }
1065
1066
        return $removed;
1067
    }
1068
1069
    
1070
1071
    public function add_note( $note = '', $customer_type = false, $added_by_user = false, $system = false ) {
1072
        // Bail if no note specified
1073
        if( !$note ) {
1074
            return false;
1075
        }
1076
1077
        if ( empty( $this->ID ) )
1078
            return false;
1079
        
1080
        if ( ( ( is_user_logged_in() && wpinv_current_user_can_manage_invoicing() ) || $added_by_user ) && !$system ) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_user_logged_in() && ...d_by_user) && ! $system, Probably Intended Meaning: is_user_logged_in() && w...d_by_user && ! $system)
Loading history...
1081
            $user                 = get_user_by( 'id', get_current_user_id() );
1082
            $comment_author       = $user->display_name;
1083
            $comment_author_email = $user->user_email;
1084
        } else {
1085
            $comment_author       = 'System';
1086
            $comment_author_email = 'system@';
1087
            $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com';
1088
            $comment_author_email = sanitize_email( $comment_author_email );
1089
        }
1090
1091
        do_action( 'wpinv_pre_insert_invoice_note', $this->ID, $note, $customer_type );
1092
1093
        $note_id = wp_insert_comment( wp_filter_comment( array(
1094
            'comment_post_ID'      => $this->ID,
1095
            'comment_content'      => $note,
1096
            'comment_agent'        => 'WPInvoicing',
1097
            'user_id'              => is_admin() ? get_current_user_id() : 0,
1098
            'comment_date'         => current_time( 'mysql' ),
1099
            'comment_date_gmt'     => current_time( 'mysql', 1 ),
1100
            'comment_approved'     => 1,
1101
            'comment_parent'       => 0,
1102
            'comment_author'       => $comment_author,
1103
            'comment_author_IP'    => wpinv_get_ip(),
1104
            'comment_author_url'   => '',
1105
            'comment_author_email' => $comment_author_email,
1106
            'comment_type'         => 'wpinv_note'
1107
        ) ) );
1108
1109
        do_action( 'wpinv_insert_payment_note', $note_id, $this->ID, $note );
1110
        
1111
        if ( $customer_type ) {
1112
            add_comment_meta( $note_id, '_wpi_customer_note', 1 );
0 ignored issues
show
Bug introduced by
$note_id of type false is incompatible with the type integer expected by parameter $comment_id of add_comment_meta(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1112
            add_comment_meta( /** @scrutinizer ignore-type */ $note_id, '_wpi_customer_note', 1 );
Loading history...
1113
1114
            do_action( 'wpinv_new_customer_note', array( 'invoice_id' => $this->ID, 'user_note' => $note ) );
1115
        }
1116
1117
        return $note_id;
1118
    }
1119
1120
    private function increase_subtotal( $amount = 0.00 ) {
1121
        $amount          = (float) $amount;
1122
        $this->subtotal += $amount;
1123
        $this->subtotal  = wpinv_round_amount( $this->subtotal );
1124
1125
        $this->recalculate_total();
1126
    }
1127
1128
    private function decrease_subtotal( $amount = 0.00 ) {
1129
        $amount          = (float) $amount;
1130
        $this->subtotal -= $amount;
1131
        $this->subtotal  = wpinv_round_amount( $this->subtotal );
1132
1133
        if ( $this->subtotal < 0 ) {
1134
            $this->subtotal = 0;
1135
        }
1136
1137
        $this->recalculate_total();
1138
    }
1139
1140
    private function increase_fees( $amount = 0.00 ) {
1141
        $amount            = (float)$amount;
1142
        $this->fees_total += $amount;
1143
        $this->fees_total  = wpinv_round_amount( $this->fees_total );
1144
1145
        $this->recalculate_total();
1146
    }
1147
1148
    private function decrease_fees( $amount = 0.00 ) {
1149
        $amount            = (float) $amount;
1150
        $this->fees_total -= $amount;
1151
        $this->fees_total  = wpinv_round_amount( $this->fees_total );
1152
1153
        if ( $this->fees_total < 0 ) {
1154
            $this->fees_total = 0;
1155
        }
1156
1157
        $this->recalculate_total();
1158
    }
1159
1160
    public function recalculate_total() {
1161
        global $wpi_nosave;
1162
        
1163
        $this->total = $this->subtotal + $this->tax + $this->fees_total;
1164
        $this->total = wpinv_round_amount( $this->total );
1165
        
1166
        do_action( 'wpinv_invoice_recalculate_total', $this, $wpi_nosave );
1167
    }
1168
    
1169
    public function increase_tax( $amount = 0.00 ) {
1170
        $amount       = (float) $amount;
1171
        $this->tax   += $amount;
1172
1173
        $this->recalculate_total();
1174
    }
1175
1176
    public function decrease_tax( $amount = 0.00 ) {
1177
        $amount     = (float) $amount;
1178
        $this->tax -= $amount;
1179
1180
        if ( $this->tax < 0 ) {
1181
            $this->tax = 0;
1182
        }
1183
1184
        $this->recalculate_total();
1185
    }
1186
1187
    public function update_status( $new_status = false, $note = '', $manual = false ) {
1188
        $old_status = ! empty( $this->old_status ) ? $this->old_status : get_post_status( $this->ID );
1189
1190
        if ( $old_status === $new_status && in_array( $new_status, array_keys( wpinv_get_invoice_statuses( true ) ) ) ) {
1191
            return false; // Don't permit status changes that aren't changes
1192
        }
1193
1194
        $do_change = apply_filters( 'wpinv_should_update_invoice_status', true, $this->ID, $new_status, $old_status );
1195
        $updated = false;
1196
1197
        if ( $do_change ) {
1198
            do_action( 'wpinv_before_invoice_status_change', $this->ID, $new_status, $old_status );
1199
1200
            $update_post_data                   = array();
1201
            $update_post_data['ID']             = $this->ID;
1202
            $update_post_data['post_status']    = $new_status;
1203
            $update_post_data['edit_date']      = current_time( 'mysql', 0 );
1204
            $update_post_data['edit_date_gmt']  = current_time( 'mysql', 1 );
1205
            
1206
            $update_post_data = apply_filters( 'wpinv_update_invoice_status_fields', $update_post_data, $this->ID );
1207
1208
            $updated = wp_update_post( $update_post_data );     
1209
           
1210
            // Process any specific status functions
1211
            switch( $new_status ) {
1212
                case 'wpi-refunded':
1213
                    $this->process_refund();
1214
                    break;
1215
                case 'wpi-failed':
1216
                    $this->process_failure();
1217
                    break;
1218
                case 'wpi-pending':
1219
                    $this->process_pending();
1220
                    break;
1221
            }
1222
            
1223
            // Status was changed.
1224
            do_action( 'wpinv_status_' . $new_status, $this->ID, $old_status );
1225
            do_action( 'wpinv_status_' . $old_status . '_to_' . $new_status, $this->ID, $old_status );
0 ignored issues
show
Bug introduced by
Are you sure $old_status of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1225
            do_action( 'wpinv_status_' . /** @scrutinizer ignore-type */ $old_status . '_to_' . $new_status, $this->ID, $old_status );
Loading history...
1226
            do_action( 'wpinv_update_status', $this->ID, $new_status, $old_status );
1227
        }
1228
1229
        return $updated;
1230
    }
1231
1232
    public function refund() {
1233
        $this->old_status        = $this->status;
1234
        $this->status            = 'wpi-refunded';
1235
        $this->pending['status'] = $this->status;
1236
1237
        $this->save();
1238
    }
1239
1240
    public function update_meta( $meta_key = '', $meta_value = '', $prev_value = '' ) {
1241
        if ( empty( $meta_key ) ) {
1242
            return false;
1243
        }
1244
1245
        if ( $meta_key == 'key' || $meta_key == 'date' ) {
1246
            $current_meta = $this->get_meta();
1247
            $current_meta[ $meta_key ] = $meta_value;
1248
1249
            $meta_key     = '_wpinv_payment_meta';
1250
            $meta_value   = $current_meta;
1251
        }
1252
1253
        $key  = str_ireplace( '_wpinv_', '', $meta_key );
1254
        $this->$key = $meta_value;
1255
1256
        $special = array_keys( $this->get_special_fields() );
1257
        if ( in_array( $key, $special ) ) {
1258
            $this->save_special();
1259
        } else {
1260
            $meta_value = apply_filters( 'wpinv_update_payment_meta_' . $meta_key, $meta_value, $this->ID );
1261
        }
1262
1263
        return update_post_meta( $this->ID, $meta_key, $meta_value, $prev_value );
1264
    }
1265
1266
    private function process_refund() {
1267
        $process_refund = true;
1268
1269
        // If the payment was not in publish, don't decrement stats as they were never incremented
1270
        if ( 'publish' != $this->old_status || 'wpi-refunded' != $this->status ) {
1271
            $process_refund = false;
1272
        }
1273
1274
        // Allow extensions to filter for their own payment types, Example: Recurring Payments
1275
        $process_refund = apply_filters( 'wpinv_should_process_refund', $process_refund, $this );
1276
1277
        if ( false === $process_refund ) {
1278
            return;
1279
        }
1280
1281
        do_action( 'wpinv_pre_refund_invoice', $this );
1282
        
1283
        $decrease_store_earnings = apply_filters( 'wpinv_decrease_store_earnings_on_refund', true, $this );
0 ignored issues
show
Unused Code introduced by
The assignment to $decrease_store_earnings is dead and can be removed.
Loading history...
1284
        $decrease_customer_value = apply_filters( 'wpinv_decrease_customer_value_on_refund', true, $this );
0 ignored issues
show
Unused Code introduced by
The assignment to $decrease_customer_value is dead and can be removed.
Loading history...
1285
        $decrease_purchase_count = apply_filters( 'wpinv_decrease_customer_purchase_count_on_refund', true, $this );
0 ignored issues
show
Unused Code introduced by
The assignment to $decrease_purchase_count is dead and can be removed.
Loading history...
1286
        
1287
        do_action( 'wpinv_post_refund_invoice', $this );
1288
    }
1289
1290
    private function process_failure() {
1291
        $discounts = $this->discounts;
1292
        if ( empty( $discounts ) ) {
1293
            return;
1294
        }
1295
1296
        if ( ! is_array( $discounts ) ) {
0 ignored issues
show
introduced by
The condition is_array($discounts) is always true.
Loading history...
1297
            $discounts = array_map( 'trim', explode( ',', $discounts ) );
1298
        }
1299
1300
        foreach ( $discounts as $discount ) {
1301
            wpinv_decrease_discount_usage( $discount );
1302
        }
1303
    }
1304
    
1305
    private function process_pending() {
1306
        $process_pending = true;
1307
1308
        // If the payment was not in publish or revoked status, don't decrement stats as they were never incremented
1309
        if ( ( 'publish' != $this->old_status && 'revoked' != $this->old_status ) || 'wpi-pending' != $this->status ) {
1310
            $process_pending = false;
1311
        }
1312
1313
        // Allow extensions to filter for their own payment types, Example: Recurring Payments
1314
        $process_pending = apply_filters( 'wpinv_should_process_pending', $process_pending, $this );
1315
1316
        if ( false === $process_pending ) {
1317
            return;
1318
        }
1319
1320
        $decrease_store_earnings = apply_filters( 'wpinv_decrease_store_earnings_on_pending', true, $this );
0 ignored issues
show
Unused Code introduced by
The assignment to $decrease_store_earnings is dead and can be removed.
Loading history...
1321
        $decrease_customer_value = apply_filters( 'wpinv_decrease_customer_value_on_pending', true, $this );
0 ignored issues
show
Unused Code introduced by
The assignment to $decrease_customer_value is dead and can be removed.
Loading history...
1322
        $decrease_purchase_count = apply_filters( 'wpinv_decrease_customer_purchase_count_on_pending', true, $this );
0 ignored issues
show
Unused Code introduced by
The assignment to $decrease_purchase_count is dead and can be removed.
Loading history...
1323
1324
        $this->completed_date = '';
1325
        $this->update_meta( '_wpinv_completed_date', '' );
1326
    }
1327
    
1328
    // get data
1329
    public function get_meta( $meta_key = '_wpinv_payment_meta', $single = true ) {
1330
        $meta = get_post_meta( $this->ID, $meta_key, $single );
1331
1332
        if ( $meta_key === '_wpinv_payment_meta' ) {
1333
1334
            if(!is_array($meta)){$meta = array();} // we need this to be an array so make sure it is.
1335
1336
            if ( empty( $meta['key'] ) ) {
1337
                $meta['key'] = $this->key;
1338
            }
1339
1340
            if ( empty( $meta['date'] ) ) {
1341
                $meta['date'] = get_post_field( 'post_date', $this->ID );
1342
            }
1343
        }
1344
1345
        $meta = apply_filters( 'wpinv_get_invoice_meta_' . $meta_key, $meta, $this->ID );
1346
1347
        return apply_filters( 'wpinv_get_invoice_meta', $meta, $this->ID, $meta_key );
1348
    }
1349
    
1350
    public function get_description() {
1351
        $post = get_post( $this->ID );
1352
        
1353
        $description = !empty( $post ) ? $post->post_content : '';
1354
        return apply_filters( 'wpinv_get_description', $description, $this->ID, $this );
1355
    }
1356
    
1357
    public function get_status( $nicename = false ) {
1358
        if ( !$nicename ) {
1359
            $status = $this->status;
1360
        } else {
1361
            $status = $this->status_nicename;
1362
        }
1363
        
1364
        return apply_filters( 'wpinv_get_status', $status, $nicename, $this->ID, $this );
1365
    }
1366
    
1367
    public function get_cart_details() {
1368
        return apply_filters( 'wpinv_cart_details', $this->cart_details, $this->ID, $this );
1369
    }
1370
    
1371
    public function get_subtotal( $currency = false ) {
1372
        $subtotal = wpinv_round_amount( $this->subtotal );
1373
        
1374
        if ( $currency ) {
1375
            $subtotal = wpinv_price( wpinv_format_amount( $subtotal, NULL, !$currency ), $this->get_currency() );
1376
        }
1377
        
1378
        return apply_filters( 'wpinv_get_invoice_subtotal', $subtotal, $this->ID, $this, $currency );
1379
    }
1380
    
1381
    public function get_total( $currency = false ) {        
1382
        if ( $this->is_free_trial() ) {
1383
            $total = wpinv_round_amount( 0 );
1384
        } else {
1385
            $total = wpinv_round_amount( $this->total );
1386
        }
1387
        if ( $currency ) {
1388
            $total = wpinv_price( wpinv_format_amount( $total, NULL, !$currency ), $this->get_currency() );
1389
        }
1390
        
1391
        return apply_filters( 'wpinv_get_invoice_total', $total, $this->ID, $this, $currency );
1392
    }
1393
    
1394
    public function get_recurring_details( $field = '', $currency = false ) {        
1395
        $data                 = array();
1396
        $data['cart_details'] = $this->cart_details;
1397
        $data['subtotal']     = $this->get_subtotal();
1398
        $data['discount']     = $this->get_discount();
1399
        $data['tax']          = $this->get_tax();
1400
        $data['total']        = $this->get_total();
1401
    
1402
        if ( !empty( $this->cart_details ) && ( $this->is_parent() || $this->is_renewal() ) ) {
1403
            $is_free_trial = $this->is_free_trial();
1404
            $discounts = $this->get_discounts( true );
1405
            
1406
            if ( $is_free_trial || !empty( $discounts ) ) {
1407
                $first_use_only = false;
1408
                
1409
                if ( !empty( $discounts ) ) {
1410
                    foreach ( $discounts as $key => $code ) {
1411
                        if ( wpinv_discount_is_recurring( $code, true ) && !$this->is_renewal() ) {
1412
                            $first_use_only = true;
1413
                            break;
1414
                        }
1415
                    }
1416
                }
1417
                    
1418
                if ( !$first_use_only ) {
1419
                    $data['subtotal'] = wpinv_round_amount( $this->subtotal );
1420
                    $data['discount'] = wpinv_round_amount( $this->discount );
1421
                    $data['tax']      = wpinv_round_amount( $this->tax );
1422
                    $data['total']    = wpinv_round_amount( $this->total );
1423
                } else {
1424
                    $cart_subtotal   = 0;
1425
                    $cart_discount   = $this->discount;
1426
                    $cart_tax        = 0;
1427
1428
                    foreach ( $this->cart_details as $key => $item ) {
1429
                        $item_quantity  = $item['quantity'] > 0 ? absint( $item['quantity'] ) : 1;
1430
                        $item_subtotal  = !empty( $item['subtotal'] ) ? $item['subtotal'] : $item['item_price'] * $item_quantity;
1431
                        $item_discount  = 0;
1432
                        $item_tax       = $item_subtotal > 0 && !empty( $item['vat_rate'] ) ? ( $item_subtotal * 0.01 * (float)$item['vat_rate'] ) : 0;
1433
                        
1434
                        if ( wpinv_prices_include_tax() ) {
1435
                            $item_subtotal -= wpinv_round_amount( $item_tax );
1436
                        }
1437
                        
1438
                        $item_total     = $item_subtotal - $item_discount + $item_tax;
1439
                        // Do not allow totals to go negative
1440
                        if ( $item_total < 0 ) {
1441
                            $item_total = 0;
1442
                        }
1443
                        
1444
                        $cart_subtotal  += (float)($item_subtotal);
1445
                        $cart_discount  += (float)($item_discount);
1446
                        $cart_tax       += (float)($item_tax);
1447
                        
1448
                        $data['cart_details'][$key]['discount']   = wpinv_round_amount( $item_discount );
1449
                        $data['cart_details'][$key]['tax']        = wpinv_round_amount( $item_tax );
1450
                        $data['cart_details'][$key]['price']      = wpinv_round_amount( $item_total );
1451
                    }
1452
1453
	                $total = $data['subtotal'] - $data['discount'] + $data['tax'];
1454
	                if ( $total < 0 ) {
1455
		                $total = 0;
1456
	                }
1457
1458
                    $data['subtotal'] = wpinv_round_amount( $cart_subtotal );
1459
                    $data['discount'] = wpinv_round_amount( $cart_discount );
1460
                    $data['tax']      = wpinv_round_amount( $cart_tax );
1461
                    $data['total']    = wpinv_round_amount( $total );
1462
                }
1463
            }
1464
        }
1465
        
1466
        $data = apply_filters( 'wpinv_get_invoice_recurring_details', $data, $this, $field, $currency );
1467
1468
        if ( isset( $data[$field] ) ) {
1469
            return ( $currency ? wpinv_price( $data[$field], $this->get_currency() ) : $data[$field] );
1470
        }
1471
        
1472
        return $data;
1473
    }
1474
    
1475
    public function get_final_tax( $currency = false ) {        
1476
        $final_total = wpinv_round_amount( $this->tax );
1477
        if ( $currency ) {
1478
            $final_total = wpinv_price( wpinv_format_amount( $final_total, NULL, !$currency ), $this->get_currency() );
1479
        }
1480
        
1481
        return apply_filters( 'wpinv_get_invoice_final_total', $final_total, $this, $currency );
1482
    }
1483
    
1484
    public function get_discounts( $array = false ) {
1485
        $discounts = $this->discounts;
1486
        if ( $array && $discounts ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $discounts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1487
            $discounts = explode( ',', $discounts );
0 ignored issues
show
Bug introduced by
$discounts of type array is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1487
            $discounts = explode( ',', /** @scrutinizer ignore-type */ $discounts );
Loading history...
1488
        }
1489
        return apply_filters( 'wpinv_payment_discounts', $discounts, $this->ID, $this, $array );
1490
    }
1491
    
1492
    public function get_discount( $currency = false, $dash = false ) {
1493
        if ( !empty( $this->discounts ) ) {
1494
            global $ajax_cart_details;
1495
            $ajax_cart_details = $this->get_cart_details();
1496
            
1497
            if ( !empty( $ajax_cart_details ) && count( $ajax_cart_details ) == count( $this->items ) ) {
1498
                $cart_items = $ajax_cart_details;
1499
            } else {
1500
                $cart_items = $this->items;
1501
            }
1502
1503
            $this->discount = wpinv_get_cart_items_discount_amount( $cart_items , $this->discounts );
1504
        }
1505
        $discount   = wpinv_round_amount( $this->discount );
1506
        $dash       = $dash && $discount > 0 ? '&ndash;' : '';
1507
        
1508
        if ( $currency ) {
1509
            $discount = wpinv_price( wpinv_format_amount( $discount, NULL, !$currency ), $this->get_currency() );
1510
        }
1511
        
1512
        $discount   = $dash . $discount;
1513
        
1514
        return apply_filters( 'wpinv_get_invoice_discount', $discount, $this->ID, $this, $currency, $dash );
1515
    }
1516
    
1517
    public function get_discount_code() {
1518
        return $this->discount_code;
1519
    }
1520
1521
    // Checks if the invoice is taxable. Does not check if taxes are enabled on the site.
1522
    public function is_taxable() {
1523
        return (int) $this->disable_taxes === 0;
1524
    }
1525
1526
    public function get_tax( $currency = false ) {
1527
        $tax = wpinv_round_amount( $this->tax );
1528
1529
        if ( $currency ) {
1530
            $tax = wpinv_price( wpinv_format_amount( $tax, NULL, !$currency ), $this->get_currency() );
1531
        }
1532
1533
        if ( ! $this->is_taxable() ) {
1534
            $tax = wpinv_round_amount( 0.00 );
1535
        }
1536
1537
        return apply_filters( 'wpinv_get_invoice_tax', $tax, $this->ID, $this, $currency );
1538
    }
1539
    
1540
    public function get_fees( $type = 'all' ) {
1541
        $fees    = array();
1542
1543
        if ( ! empty( $this->fees ) && is_array( $this->fees ) ) {
1544
            foreach ( $this->fees as $fee ) {
1545
                if( 'all' != $type && ! empty( $fee['type'] ) && $type != $fee['type'] ) {
1546
                    continue;
1547
                }
1548
1549
                $fee['label'] = stripslashes( $fee['label'] );
1550
                $fee['amount_display'] = wpinv_price( $fee['amount'], $this->get_currency() );
1551
                $fees[]    = $fee;
1552
            }
1553
        }
1554
1555
        return apply_filters( 'wpinv_get_invoice_fees', $fees, $this->ID, $this );
1556
    }
1557
    
1558
    public function get_fees_total( $type = 'all' ) {
1559
        $fees_total = (float) 0.00;
1560
1561
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
1562
        if ( ! empty( $payment_fees ) ) {
1563
            foreach ( $payment_fees as $fee ) {
1564
                $fees_total += (float) $fee['amount'];
1565
            }
1566
        }
1567
1568
        return apply_filters( 'wpinv_get_invoice_fees_total', $fees_total, $this->ID, $this );
1569
1570
    }
1571
1572
    public function get_user_id() {
1573
        return apply_filters( 'wpinv_user_id', $this->user_id, $this->ID, $this );
1574
    }
1575
    
1576
    public function get_first_name() {
1577
        return apply_filters( 'wpinv_first_name', $this->first_name, $this->ID, $this );
1578
    }
1579
    
1580
    public function get_last_name() {
1581
        return apply_filters( 'wpinv_last_name', $this->last_name, $this->ID, $this );
1582
    }
1583
    
1584
    public function get_user_full_name() {
1585
        return apply_filters( 'wpinv_user_full_name', $this->full_name, $this->ID, $this );
1586
    }
1587
    
1588
    public function get_user_info() {
1589
        return apply_filters( 'wpinv_user_info', $this->user_info, $this->ID, $this );
1590
    }
1591
    
1592
    public function get_email() {
1593
        return apply_filters( 'wpinv_user_email', $this->email, $this->ID, $this );
1594
    }
1595
    
1596
    public function get_address() {
1597
        return apply_filters( 'wpinv_address', $this->address, $this->ID, $this );
1598
    }
1599
    
1600
    public function get_phone() {
1601
        return apply_filters( 'wpinv_phone', $this->phone, $this->ID, $this );
1602
    }
1603
    
1604
    public function get_number() {
1605
        return apply_filters( 'wpinv_number', $this->number, $this->ID, $this );
1606
    }
1607
    
1608
    public function get_items() {
1609
        return apply_filters( 'wpinv_payment_meta_items', $this->items, $this->ID, $this );
1610
    }
1611
    
1612
    public function get_key() {
1613
        return apply_filters( 'wpinv_key', $this->key, $this->ID, $this );
1614
    }
1615
    
1616
    public function get_transaction_id() {
1617
        return apply_filters( 'wpinv_get_invoice_transaction_id', $this->transaction_id, $this->ID, $this );
1618
    }
1619
    
1620
    public function get_gateway() {
1621
        return apply_filters( 'wpinv_gateway', $this->gateway, $this->ID, $this );
1622
    }
1623
    
1624
    public function get_gateway_title() {
1625
        $this->gateway_title = !empty( $this->gateway_title ) ? $this->gateway_title : wpinv_get_gateway_checkout_label( $this->gateway );
1626
        
1627
        return apply_filters( 'wpinv_gateway_title', $this->gateway_title, $this->ID, $this );
1628
    }
1629
    
1630
    public function get_currency() {
1631
        return apply_filters( 'wpinv_currency_code', $this->currency, $this->ID, $this );
1632
    }
1633
    
1634
    public function get_created_date() {
1635
        return apply_filters( 'wpinv_created_date', $this->date, $this->ID, $this );
1636
    }
1637
    
1638
    public function get_due_date( $display = false ) {
1639
        $due_date = apply_filters( 'wpinv_due_date', $this->due_date, $this->ID, $this );
1640
        
1641
        if ( !$display || empty( $due_date ) ) {
1642
            return $due_date;
1643
        }
1644
        
1645
        return date_i18n( get_option( 'date_format' ), strtotime( $due_date ) );
0 ignored issues
show
Bug introduced by
It seems like get_option('date_format') can also be of type false; however, parameter $format of date_i18n() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1645
        return date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $due_date ) );
Loading history...
1646
    }
1647
    
1648
    public function get_completed_date() {
1649
        return apply_filters( 'wpinv_completed_date', $this->completed_date, $this->ID, $this );
1650
    }
1651
    
1652
    public function get_invoice_date( $formatted = true ) {
1653
        $date_completed = $this->completed_date;
1654
        $invoice_date   = $date_completed != '' && $date_completed != '0000-00-00 00:00:00' ? $date_completed : '';
1655
        
1656
        if ( $invoice_date == '' ) {
1657
            $date_created   = $this->date;
1658
            $invoice_date   = $date_created != '' && $date_created != '0000-00-00 00:00:00' ? $date_created : '';
1659
        }
1660
        
1661
        if ( $formatted && $invoice_date ) {
1662
            $invoice_date   = date_i18n( get_option( 'date_format' ), strtotime( $invoice_date ) );
0 ignored issues
show
Bug introduced by
It seems like get_option('date_format') can also be of type false; however, parameter $format of date_i18n() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1662
            $invoice_date   = date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $invoice_date ) );
Loading history...
1663
        }
1664
1665
        return apply_filters( 'wpinv_get_invoice_date', $invoice_date, $formatted, $this->ID, $this );
1666
    }
1667
    
1668
    public function get_ip() {
1669
        return apply_filters( 'wpinv_user_ip', $this->ip, $this->ID, $this );
1670
    }
1671
        
1672
    public function has_status( $status ) {
1673
        return apply_filters( 'wpinv_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status ) ) || $this->get_status() === $status ? true : false, $this, $status );
1674
    }
1675
    
1676
    public function add_item( $item_id = 0, $args = array() ) {
1677
        global $wpi_current_id, $wpi_item_id;
1678
    
1679
        $item = new WPInv_Item( $item_id );
1680
1681
        // Bail if this post isn't a item
1682
        if( !$item || $item->post_type !== 'wpi_item' ) {
0 ignored issues
show
introduced by
$item is of type WPInv_Item, thus it always evaluated to true.
Loading history...
Bug Best Practice introduced by
The property post_type does not exist on WPInv_Item. Since you implemented __get, consider adding a @property annotation.
Loading history...
1683
            return false;
1684
        }
1685
        
1686
        $has_quantities = wpinv_item_quantities_enabled();
1687
1688
        // Set some defaults
1689
        $defaults = array(
1690
            'quantity'      => 1,
1691
            'id'            => false,
1692
            'name'          => $item->get_name(),
1693
            'item_price'    => false,
1694
            'custom_price'  => '',
1695
            'discount'      => 0,
1696
            'tax'           => 0.00,
1697
            'meta'          => array(),
1698
            'fees'          => array()
1699
        );
1700
1701
        $args = wp_parse_args( apply_filters( 'wpinv_add_item_args', $args, $item->ID ), $defaults );
1702
        $args['quantity']   = $has_quantities && $args['quantity'] > 0 ? absint( $args['quantity'] ) : 1;
1703
1704
        $wpi_current_id         = $this->ID;
1705
        $wpi_item_id            = $item->ID;
1706
        $discounts              = $this->get_discounts();
0 ignored issues
show
Unused Code introduced by
The assignment to $discounts is dead and can be removed.
Loading history...
1707
        
1708
        $_POST['wpinv_country'] = $this->country;
1709
        $_POST['wpinv_state']   = $this->state;
1710
        
1711
        $found_cart_key         = false;
1712
        
1713
        if ($has_quantities) {
1714
            $this->cart_details = !empty( $this->cart_details ) ? array_values( $this->cart_details ) : $this->cart_details;
1715
            
1716
            foreach ( $this->items as $key => $cart_item ) {
1717
                if ( (int)$item_id !== (int)$cart_item['id'] ) {
1718
                    continue;
1719
                }
1720
1721
                $this->items[ $key ]['quantity'] += $args['quantity'];
1722
                break;
1723
            }
1724
            
1725
            foreach ( $this->cart_details as $cart_key => $cart_item ) {
1726
                if ( $item_id != $cart_item['id'] ) {
1727
                    continue;
1728
                }
1729
1730
                $found_cart_key = $cart_key;
1731
                break;
1732
            }
1733
        }
1734
        
1735
        if ($has_quantities && $found_cart_key !== false) {
1736
            $cart_item          = $this->cart_details[$found_cart_key];
1737
            $item_price         = $cart_item['item_price'];
1738
            $quantity           = !empty( $cart_item['quantity'] ) ? $cart_item['quantity'] : 1;
1739
            $tax_rate           = !empty( $cart_item['vat_rate'] ) ? $cart_item['vat_rate'] : 0;
1740
            
1741
            $new_quantity       = $quantity + $args['quantity'];
1742
            $subtotal           = $item_price * $new_quantity;
1743
            
1744
            $args['quantity']   = $new_quantity;
1745
            $discount           = !empty( $args['discount'] ) ? $args['discount'] : 0;
1746
            $tax                = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1747
            
1748
            $discount_increased = $discount > 0 && $subtotal > 0 && $discount > (float)$cart_item['discount'] ? $discount - (float)$cart_item['discount'] : 0;
1749
            $tax_increased      = $tax > 0 && $subtotal > 0 && $tax > (float)$cart_item['tax'] ? $tax - (float)$cart_item['tax'] : 0;
1750
            // The total increase equals the number removed * the item_price
1751
            $total_increased    = wpinv_round_amount( $item_price );
1752
            
1753
            if ( wpinv_prices_include_tax() ) {
1754
                $subtotal -= wpinv_round_amount( $tax );
1755
            }
1756
1757
            $total              = $subtotal - $discount + $tax;
1758
1759
            // Do not allow totals to go negative
1760
            if( $total < 0 ) {
1761
                $total = 0;
1762
            }
1763
            
1764
            $cart_item['quantity']  = $new_quantity;
1765
            $cart_item['subtotal']  = $subtotal;
1766
            $cart_item['discount']  = $discount;
1767
            $cart_item['tax']       = $tax;
1768
            $cart_item['price']     = $total;
1769
            
1770
            $subtotal               = $total_increased - $discount_increased;
1771
            $tax                    = $tax_increased;
1772
            
1773
            $this->cart_details[$found_cart_key] = $cart_item;
1774
        } else {
1775
            // Set custom price.
1776
            if ( $args['custom_price'] !== '' ) {
1777
                $item_price = $args['custom_price'];
1778
            } else {
1779
                // Allow overriding the price
1780
                if ( false !== $args['item_price'] ) {
1781
                    $item_price = $args['item_price'];
1782
                } else {
1783
                    $item_price = wpinv_get_item_price( $item->ID );
1784
                }
1785
            }
1786
1787
            // Sanitizing the price here so we don't have a dozen calls later
1788
            $item_price = wpinv_sanitize_amount( $item_price );
1789
            $subtotal   = wpinv_round_amount( $item_price * $args['quantity'] );
1790
        
1791
            $discount   = !empty( $args['discount'] ) ? $args['discount'] : 0;
1792
            $tax_class  = !empty( $args['vat_class'] ) ? $args['vat_class'] : '';
1793
            $tax_rate   = !empty( $args['vat_rate'] ) ? $args['vat_rate'] : 0;
1794
            $tax        = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1795
1796
            // Setup the items meta item
1797
            $new_item = array(
1798
                'id'       => $item->ID,
1799
                'quantity' => $args['quantity'],
1800
            );
1801
1802
            $this->items[]  = $new_item;
1803
1804
            if ( wpinv_prices_include_tax() ) {
1805
                $subtotal -= wpinv_round_amount( $tax );
1806
            }
1807
1808
            $total      = $subtotal - $discount + $tax;
1809
1810
            // Do not allow totals to go negative
1811
            if( $total < 0 ) {
1812
                $total = 0;
1813
            }
1814
        
1815
            $this->cart_details[] = array(
1816
                'name'          => !empty($args['name']) ? $args['name'] : $item->get_name(),
1817
                'id'            => $item->ID,
1818
                'item_price'    => wpinv_round_amount( $item_price ),
1819
                'custom_price'  => ( $args['custom_price'] !== '' ? wpinv_round_amount( $args['custom_price'] ) : '' ),
1820
                'quantity'      => $args['quantity'],
1821
                'discount'      => $discount,
1822
                'subtotal'      => wpinv_round_amount( $subtotal ),
1823
                'tax'           => wpinv_round_amount( $tax ),
1824
                'price'         => wpinv_round_amount( $total ),
1825
                'vat_rate'      => $tax_rate,
1826
                'vat_class'     => $tax_class,
1827
                'meta'          => $args['meta'],
1828
                'fees'          => $args['fees'],
1829
            );
1830
   
1831
            $subtotal = $subtotal - $discount;
1832
        }
1833
        
1834
        $added_item = end( $this->cart_details );
1835
        $added_item['action']  = 'add';
1836
        
1837
        $this->pending['items'][] = $added_item;
1838
        
1839
        $this->increase_subtotal( $subtotal );
1840
        $this->increase_tax( $tax );
1841
1842
        return true;
1843
    }
1844
1845
    public function remove_item( $item_id, $args = array() ) {
1846
1847
        // Set some defaults
1848
        $defaults = array(
1849
            'quantity'      => 1,
1850
            'item_price'    => false,
1851
            'custom_price'  => '',
1852
            'cart_index'    => false,
1853
        );
1854
        $args = wp_parse_args( $args, $defaults );
0 ignored issues
show
Security Variable Injection introduced by
$args can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and Data is passed through wp_unslash(), and wp_unslash($_POST) is assigned to $data
    in includes/class-wpinv-ajax.php on line 755
  2. Data is passed through wpinv_clean(), and wpinv_clean($data[$address_field['name']]) is assigned to $address_fields
    in includes/class-wpinv-ajax.php on line 893
  3. wpinv_update_invoice() is called
    in includes/class-wpinv-ajax.php on line 941
  4. Enters via parameter $invoice_data
    in includes/wpinv-invoice-functions.php on line 276
  5. $invoice_data['cart_details'] is assigned to $cart_details
    in includes/wpinv-invoice-functions.php on line 351
  6. ! empty($cart_details['remove_items']) && is_array($cart_details['remove_items']) ? $cart_details['remove_items'] : array() is assigned to $remove_items
    in includes/wpinv-invoice-functions.php on line 352
  7. $remove_items is assigned to $item
    in includes/wpinv-invoice-functions.php on line 355
  8. ! empty($item['id']) ? $item['id'] : 0 is assigned to $item_id
    in includes/wpinv-invoice-functions.php on line 356
  9. array('id' => $item_id, 'quantity' => $quantity, 'cart_index' => $cart_index) is assigned to $args
    in includes/wpinv-invoice-functions.php on line 364
  10. WPInv_Invoice::remove_item() is called
    in includes/wpinv-invoice-functions.php on line 370
  11. Enters via parameter $args
    in includes/class-wpinv-invoice.php on line 1845

Used in variable context

  1. wp_parse_args() is called
    in includes/class-wpinv-invoice.php on line 1795
  2. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4371
  3. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4377
  4. Enters via parameter $string
    in wordpress/wp-includes/formatting.php on line 4888
  5. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 4889

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
1855
1856
        // Bail if this post isn't a item
1857
        if ( get_post_type( $item_id ) !== 'wpi_item' ) {
1858
            return false;
1859
        }
1860
        
1861
        $this->cart_details = !empty( $this->cart_details ) ? array_values( $this->cart_details ) : $this->cart_details;
1862
1863
        foreach ( $this->items as $key => $item ) {
1864
            if ( !empty($item['id']) && (int)$item_id !== (int)$item['id'] ) {
1865
                continue;
1866
            }
1867
1868
            if ( false !== $args['cart_index'] ) {
1869
                $cart_index = absint( $args['cart_index'] );
1870
                $cart_item  = ! empty( $this->cart_details[ $cart_index ] ) ? $this->cart_details[ $cart_index ] : false;
1871
1872
                if ( ! empty( $cart_item ) ) {
1873
                    // If the cart index item isn't the same item ID, don't remove it
1874
                    if ( !empty($cart_item['id']) && $cart_item['id'] != $item['id'] ) {
1875
                        continue;
1876
                    }
1877
                }
1878
            }
1879
1880
            $item_quantity = $this->items[ $key ]['quantity'];
1881
            if ( $item_quantity > $args['quantity'] ) {
1882
                $this->items[ $key ]['quantity'] -= $args['quantity'];
1883
                break;
1884
            } else {
1885
                unset( $this->items[ $key ] );
1886
                break;
1887
            }
1888
        }
1889
1890
        $found_cart_key = false;
1891
        if ( false === $args['cart_index'] ) {
1892
            foreach ( $this->cart_details as $cart_key => $item ) {
1893
                if ( $item_id != $item['id'] ) {
1894
                    continue;
1895
                }
1896
1897
                if ( false !== $args['item_price'] ) {
1898
                    if ( isset( $item['item_price'] ) && (float) $args['item_price'] != (float) $item['item_price'] ) {
1899
                        continue;
1900
                    }
1901
                }
1902
1903
                $found_cart_key = $cart_key;
1904
                break;
1905
            }
1906
        } else {
1907
            $cart_index = absint( $args['cart_index'] );
1908
1909
            if ( ! array_key_exists( $cart_index, $this->cart_details ) ) {
1910
                return false; // Invalid cart index passed.
1911
            }
1912
1913
            if ( (int) $this->cart_details[ $cart_index ]['id'] > 0 && (int) $this->cart_details[ $cart_index ]['id'] !== (int) $item_id ) {
1914
                return false; // We still need the proper Item ID to be sure.
1915
            }
1916
1917
            $found_cart_key = $cart_index;
1918
        }
1919
        
1920
        $cart_item  = $this->cart_details[$found_cart_key];
1921
        $quantity   = !empty( $cart_item['quantity'] ) ? $cart_item['quantity'] : 1;
1922
        
1923
        if ( count( $this->cart_details ) == 1 && ( $quantity - $args['quantity'] ) < 1 ) {
1924
            //return false; // Invoice must contain at least one item.
1925
        }
1926
        
1927
        $discounts  = $this->get_discounts();
0 ignored issues
show
Unused Code introduced by
The assignment to $discounts is dead and can be removed.
Loading history...
1928
        
1929
        if ( $quantity > $args['quantity'] ) {
1930
            $item_price         = $cart_item['item_price'];
1931
            $tax_rate           = !empty( $cart_item['vat_rate'] ) ? $cart_item['vat_rate'] : 0;
1932
            
1933
            $new_quantity       = max( $quantity - $args['quantity'], 1);
1934
            $subtotal           = $item_price * $new_quantity;
1935
            
1936
            $args['quantity']   = $new_quantity;
1937
            $discount           = !empty( $cart_item['discount'] ) ? $cart_item['discount'] : 0;
1938
            $tax                = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1939
            
1940
            $discount_decrease  = (float)$cart_item['discount'] > 0 && $quantity > 0 ? wpinv_round_amount( ( (float)$cart_item['discount'] / $quantity ) ) : 0;
1941
            $discount_decrease  = $discount > 0 && $subtotal > 0 && (float)$cart_item['discount'] > $discount ? (float)$cart_item['discount'] - $discount : $discount_decrease; 
1942
            $tax_decrease       = (float)$cart_item['tax'] > 0 && $quantity > 0 ? wpinv_round_amount( ( (float)$cart_item['tax'] / $quantity ) ) : 0;
1943
            $tax_decrease       = $tax > 0 && $subtotal > 0 && (float)$cart_item['tax'] > $tax ? (float)$cart_item['tax'] - $tax : $tax_decrease;
1944
            
1945
            // The total increase equals the number removed * the item_price
1946
            $total_decrease     = wpinv_round_amount( $item_price );
1947
            
1948
            if ( wpinv_prices_include_tax() ) {
1949
                $subtotal -= wpinv_round_amount( $tax );
1950
            }
1951
1952
            $total              = $subtotal - $discount + $tax;
1953
1954
            // Do not allow totals to go negative
1955
            if( $total < 0 ) {
1956
                $total = 0;
1957
            }
1958
            
1959
            $cart_item['quantity']  = $new_quantity;
1960
            $cart_item['subtotal']  = $subtotal;
1961
            $cart_item['discount']  = $discount;
1962
            $cart_item['tax']       = $tax;
1963
            $cart_item['price']     = $total;
1964
            
1965
            $added_item             = $cart_item;
1966
            $added_item['id']       = $item_id;
1967
            $added_item['price']    = $total_decrease;
1968
            $added_item['quantity'] = $args['quantity'];
1969
            
1970
            $subtotal_decrease      = $total_decrease - $discount_decrease;
1971
            
1972
            $this->cart_details[$found_cart_key] = $cart_item;
1973
            
1974
            $remove_item = end( $this->cart_details );
1975
        } else {
1976
            $item_price     = $cart_item['item_price'];
1977
            $discount       = !empty( $cart_item['discount'] ) ? $cart_item['discount'] : 0;
1978
            $tax            = !empty( $cart_item['tax'] ) ? $cart_item['tax'] : 0;
1979
        
1980
            $subtotal_decrease  = ( $item_price * $quantity ) - $discount;
1981
            $tax_decrease       = $tax;
1982
1983
            unset( $this->cart_details[$found_cart_key] );
1984
            
1985
            $remove_item             = $args;
1986
            $remove_item['id']       = $item_id;
1987
            $remove_item['price']    = $subtotal_decrease;
1988
            $remove_item['quantity'] = $args['quantity'];
1989
        }
1990
        
1991
        $remove_item['action']      = 'remove';
1992
        $this->pending['items'][]   = $remove_item;
1993
               
1994
        $this->decrease_subtotal( $subtotal_decrease );
1995
        $this->decrease_tax( $tax_decrease );
1996
        
1997
        return true;
1998
    }
1999
    
2000
    public function update_items($temp = false) {
2001
        global $wpinv_euvat, $wpi_current_id, $wpi_item_id, $wpi_nosave;
2002
2003
        if ( ! empty( $this->cart_details ) ) {
2004
            $wpi_nosave             = $temp;
2005
            $cart_subtotal          = 0;
2006
            $cart_discount          = 0;
2007
            $cart_tax               = 0;
2008
            $cart_details           = array();
2009
2010
            $_POST['wpinv_country'] = $this->country;
2011
            $_POST['wpinv_state']   = $this->state;
2012
2013
            foreach ( $this->cart_details as $key => $item ) {
2014
                $item_price = $item['item_price'];
2015
                $quantity   = wpinv_item_quantities_enabled() && $item['quantity'] > 0 ? absint( $item['quantity'] ) : 1;
2016
                $amount     = wpinv_round_amount( $item_price * $quantity );
0 ignored issues
show
Unused Code introduced by
The assignment to $amount is dead and can be removed.
Loading history...
2017
                $subtotal   = $item_price * $quantity;
2018
2019
                $wpi_current_id         = $this->ID;
2020
                $wpi_item_id            = $item['id'];
2021
2022
                $discount   = wpinv_get_cart_item_discount_amount( $item, $this->get_discounts() );
2023
2024
                $tax_rate   = wpinv_get_tax_rate( $this->country, $this->state, $wpi_item_id );
2025
                $tax_class  = $wpinv_euvat->get_item_class( $wpi_item_id );
2026
                $tax        = $item_price > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
2027
2028
                if ( ! $this->is_taxable() ) {
2029
                    $tax = 0;
2030
                }
2031
2032
                if ( wpinv_prices_include_tax() ) {
2033
                    $subtotal -= wpinv_round_amount( $tax );
2034
                }
2035
2036
                $total      = $subtotal - $discount + $tax;
2037
2038
                // Do not allow totals to go negative
2039
                if( $total < 0 ) {
2040
                    $total = 0;
2041
                }
2042
2043
                $cart_details[] = array(
2044
                    'id'          => $item['id'],
2045
                    'name'        => $item['name'],
2046
                    'item_price'  => wpinv_round_amount( $item_price ),
2047
                    'custom_price'=> ( isset( $item['custom_price'] ) ? $item['custom_price'] : '' ),
2048
                    'quantity'    => $quantity,
2049
                    'discount'    => $discount,
2050
                    'subtotal'    => wpinv_round_amount( $subtotal ),
2051
                    'tax'         => wpinv_round_amount( $tax ),
2052
                    'price'       => wpinv_round_amount( $total ),
2053
                    'vat_rate'    => $tax_rate,
2054
                    'vat_class'   => $tax_class,
2055
                    'meta'        => isset($item['meta']) ? $item['meta'] : array(),
2056
                    'fees'        => isset($item['fees']) ? $item['fees'] : array(),
2057
                );
2058
2059
                $cart_subtotal  += (float)($subtotal - $discount); // TODO
2060
                $cart_discount  += (float)($discount);
2061
                $cart_tax       += (float)($tax);
2062
            }
2063
            if ( $cart_subtotal < 0 ) {
2064
                $cart_subtotal = 0;
2065
            }
2066
            if ( $cart_tax < 0 ) {
2067
                $cart_tax = 0;
2068
            }
2069
            $this->subtotal = wpinv_round_amount( $cart_subtotal );
2070
            $this->tax      = wpinv_round_amount( $cart_tax );
2071
            $this->discount = wpinv_round_amount( $cart_discount );
2072
            
2073
            $this->recalculate_total();
2074
            
2075
            $this->cart_details = $cart_details;
2076
        }
2077
2078
        return $this;
2079
    }
2080
    
2081
    public function recalculate_totals($temp = false) {        
2082
        $this->update_items($temp);
2083
        $this->save( true );
2084
        
2085
        return $this;
2086
    }
2087
    
2088
    public function needs_payment() {
2089
        $valid_invoice_statuses = apply_filters( 'wpinv_valid_invoice_statuses_for_payment', array( 'wpi-pending' ), $this );
2090
2091
        if ( $this->has_status( $valid_invoice_statuses ) && ( $this->get_total() > 0 || $this->is_free_trial() || $this->is_free() || $this->is_initial_free() ) ) {
2092
            $needs_payment = true;
2093
        } else {
2094
            $needs_payment = false;
2095
        }
2096
2097
        return apply_filters( 'wpinv_needs_payment', $needs_payment, $this, $valid_invoice_statuses );
2098
    }
2099
    
2100
    public function get_checkout_payment_url( $with_key = false, $secret = false ) {
2101
        $pay_url = wpinv_get_checkout_uri();
2102
2103
        if ( is_ssl() ) {
2104
            $pay_url = str_replace( 'http:', 'https:', $pay_url );
2105
        }
2106
        
2107
        $key = $this->get_key();
2108
2109
        if ( $with_key ) {
2110
            $pay_url = add_query_arg( 'invoice_key', $key, $pay_url );
2111
        } else {
2112
            $pay_url = add_query_arg( array( 'wpi_action' => 'pay_for_invoice', 'invoice_key' => $key ), $pay_url );
2113
        }
2114
        
2115
        if ( $secret ) {
2116
            $pay_url = add_query_arg( array( '_wpipay' => md5( $this->get_user_id() . '::' . $this->get_email() . '::' . $key ) ), $pay_url );
2117
        }
2118
2119
        return apply_filters( 'wpinv_get_checkout_payment_url', $pay_url, $this, $with_key, $secret );
2120
    }
2121
    
2122
    public function get_view_url( $with_key = false ) {
2123
        $invoice_url = get_permalink( $this->ID );
2124
2125
        if ( $with_key ) {
2126
            $invoice_url = add_query_arg( 'invoice_key', $this->get_key(), $invoice_url );
2127
        }
2128
2129
        return apply_filters( 'wpinv_get_view_url', $invoice_url, $this, $with_key );
2130
    }
2131
    
2132
    public function generate_key( $string = '' ) {
2133
        $auth_key  = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
2134
        return strtolower( md5( $string . date( 'Y-m-d H:i:s' ) . $auth_key . uniqid( 'wpinv', true ) ) );  // Unique key
2135
    }
2136
    
2137
    public function is_recurring() {
2138
        if ( empty( $this->cart_details ) ) {
2139
            return false;
2140
        }
2141
        
2142
        $has_subscription = false;
2143
        foreach( $this->cart_details as $cart_item ) {
2144
            if ( !empty( $cart_item['id'] ) && wpinv_is_recurring_item( $cart_item['id'] )  ) {
2145
                $has_subscription = true;
2146
                break;
2147
            }
2148
        }
2149
        
2150
        if ( count( $this->cart_details ) > 1 ) {
2151
            $has_subscription = false;
2152
        }
2153
2154
        return apply_filters( 'wpinv_invoice_has_recurring_item', $has_subscription, $this->cart_details );
2155
    }
2156
2157
    public function is_free_trial() {
2158
        $is_free_trial = false;
2159
        
2160
        if ( $this->is_parent() && $item = $this->get_recurring( true ) ) {
2161
            if ( !empty( $item ) && $item->has_free_trial() ) {
2162
                $is_free_trial = true;
2163
            }
2164
        }
2165
2166
        return apply_filters( 'wpinv_invoice_is_free_trial', $is_free_trial, $this->cart_details, $this );
2167
    }
2168
2169
    public function is_initial_free() {
2170
        $is_initial_free = false;
2171
        
2172
        if ( ! ( (float)wpinv_round_amount( $this->get_total() ) > 0 ) && $this->is_parent() && $this->is_recurring() && ! $this->is_free_trial() && ! $this->is_free() ) {
2173
            $is_initial_free = true;
2174
        }
2175
2176
        return apply_filters( 'wpinv_invoice_is_initial_free', $is_initial_free, $this->cart_details );
2177
    }
2178
    
2179
    public function get_recurring( $object = false ) {
2180
        $item = NULL;
2181
        
2182
        if ( empty( $this->cart_details ) ) {
2183
            return $item;
2184
        }
2185
2186
        foreach( $this->cart_details as $cart_item ) {
2187
            if ( !empty( $cart_item['id'] ) && wpinv_is_recurring_item( $cart_item['id'] )  ) {
2188
                $item = $cart_item['id'];
2189
                break;
2190
            }
2191
        }
2192
2193
        if ( $object ) {
2194
            $item = $item ? new WPInv_Item( $item ) : NULL;
2195
            
2196
            apply_filters( 'wpinv_invoice_get_recurring_item', $item, $this );
2197
        }
2198
2199
        return apply_filters( 'wpinv_invoice_get_recurring_item_id', $item, $this );
2200
    }
2201
2202
    public function get_subscription_name() {
2203
        $item = $this->get_recurring( true );
2204
2205
        if ( empty( $item ) ) {
2206
            return NULL;
2207
        }
2208
2209
        if ( !($name = $item->get_name()) ) {
2210
            $name = $item->post_name;
2211
        }
2212
2213
        return apply_filters( 'wpinv_invoice_get_subscription_name', $name, $this );
2214
    }
2215
2216
    public function get_subscription_id() {
2217
        $subscription_id = $this->get_meta( '_wpinv_subscr_profile_id', true );
2218
2219
        if ( empty( $subscription_id ) && !empty( $this->parent_invoice ) ) {
2220
            $parent_invoice = wpinv_get_invoice( $this->parent_invoice );
2221
2222
            $subscription_id = $parent_invoice->get_meta( '_wpinv_subscr_profile_id', true );
2223
        }
2224
        
2225
        return $subscription_id;
2226
    }
2227
    
2228
    public function is_parent() {
2229
        $is_parent = empty( $this->parent_invoice ) ? true : false;
2230
2231
        return apply_filters( 'wpinv_invoice_is_parent', $is_parent, $this );
2232
    }
2233
    
2234
    public function is_renewal() {
2235
        $is_renewal = $this->parent_invoice && $this->parent_invoice != $this->ID ? true : false;
2236
2237
        return apply_filters( 'wpinv_invoice_is_renewal', $is_renewal, $this );
2238
    }
2239
    
2240
    public function get_parent_payment() {
2241
        $parent_payment = NULL;
2242
        
2243
        if ( $this->is_renewal() ) {
2244
            $parent_payment = wpinv_get_invoice( $this->parent_invoice );
2245
        }
2246
        
2247
        return $parent_payment;
2248
    }
2249
    
2250
    public function is_paid() {
2251
        $is_paid = $this->has_status( array( 'publish', 'wpi-processing', 'wpi-renewal' ) );
2252
2253
        return apply_filters( 'wpinv_invoice_is_paid', $is_paid, $this );
2254
    }
2255
2256
    /**
2257
     * Checks if this is a quote object.
2258
     * 
2259
     * @since 1.0.15
2260
     */
2261
    public function is_quote() {
2262
        return 'wpi_quote' === $this->post_type;
2263
    }
2264
    
2265
    public function is_refunded() {
2266
        $is_refunded = $this->has_status( array( 'wpi-refunded' ) );
2267
2268
        return apply_filters( 'wpinv_invoice_is_refunded', $is_refunded, $this );
2269
    }
2270
    
2271
    public function is_free() {
2272
        $is_free = false;
2273
        
2274
        if ( !( (float)wpinv_round_amount( $this->get_total() ) > 0 ) ) {
2275
            if ( $this->is_parent() && $this->is_recurring() ) {
2276
                $is_free = (float)wpinv_round_amount( $this->get_recurring_details( 'total' ) ) > 0 ? false : true;
2277
            } else {
2278
                $is_free = true;
2279
            }
2280
        }
2281
        
2282
        return apply_filters( 'wpinv_invoice_is_free', $is_free, $this );
2283
    }
2284
    
2285
    public function has_vat() {
2286
        global $wpinv_euvat, $wpi_country;
2287
        
2288
        $requires_vat = false;
2289
        
2290
        if ( $this->country ) {
2291
            $wpi_country        = $this->country;
2292
            
2293
            $requires_vat       = $wpinv_euvat->requires_vat( $requires_vat, $this->get_user_id(), $wpinv_euvat->invoice_has_digital_rule( $this ) );
2294
        }
2295
        
2296
        return apply_filters( 'wpinv_invoice_has_vat', $requires_vat, $this );
2297
    }
2298
2299
    public function refresh_item_ids() {
2300
        $item_ids = array();
2301
        
2302
        if ( ! empty( $this->cart_details ) ) {
2303
            foreach ( array_keys( $this->cart_details ) as $item ) {
2304
                if ( ! empty( $item['id'] ) ) {
2305
                    $item_ids[] = $item['id'];
2306
                }
2307
            }
2308
        }
2309
        
2310
        $item_ids = !empty( $item_ids ) ? implode( ',', array_unique( $item_ids ) ) : '';
2311
        
2312
        update_post_meta( $this->ID, '_wpinv_item_ids', $item_ids );
2313
    }
2314
    
2315
    public function get_invoice_quote_type( $post_id ) {
2316
        if ( empty( $post_id ) ) {
2317
            return '';
2318
        }
2319
2320
        $type = get_post_type( $post_id );
2321
2322
        if ( 'wpi_invoice' === $type ) {
2323
            $post_type = __('Invoice', 'invoicing');
2324
        } else{
2325
            $post_type = __('Quote', 'invoicing');
2326
        }
2327
2328
        return apply_filters('get_invoice_type_label', $post_type, $post_id);
2329
    }
2330
}
2331