Passed
Pull Request — master (#343)
by Brian
100:00
created

WPInv_Invoice::discount_first_payment_only()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 6
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 13
rs 9.6111
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
    /**
273
     * Retrieves an invoice key.
274
     */
275
    public function get( $key ) {
276
        if ( method_exists( $this, 'get_' . $key ) ) {
277
            $value = call_user_func( array( $this, 'get_' . $key ) );
278
        } else {
279
            $value = $this->$key;
280
        }
281
282
        return $value;
283
    }
284
285
     /**
286
     * Sets an invoice key.
287
     */
288
    public function set( $key, $value ) {
289
        $ignore = array( 'items', 'cart_details', 'fees', '_ID' );
290
291
        if ( $key === 'status' ) {
292
            $this->old_status = $this->status;
293
        }
294
295
        if ( ! in_array( $key, $ignore ) ) {
296
            $this->pending[ $key ] = $value;
297
        }
298
299
        if( '_ID' !== $key ) {
300
            $this->$key = $value;
301
        }
302
    }
303
304
    /**
305
     * Checks if an invoice key is set.
306
     */
307
    public function _isset( $name ) {
308
        if ( property_exists( $this, $name) ) {
309
            return false === empty( $this->$name );
310
        } else {
311
            return null;
312
        }
313
    }
314
315
    /**
316
     * @param int|WPInv_Invoice|WP_Post $invoice The invoice.
317
     */
318
    private function setup_invoice( $invoice ) {
319
        global $wpdb;
320
        $this->pending = array();
321
322
        if ( empty( $invoice ) ) {
323
            return false;
324
        }
325
326
        if ( is_a( $invoice, 'WPInv_Invoice' ) ) {
327
            foreach ( get_object_vars( $invoice ) as $prop => $value ) {
328
                $this->$prop = $value;
329
            }
330
            return true;
331
        }
332
333
        // Retrieve post object.
334
        $invoice      = get_post( $invoice );
335
336
        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...
337
            return false;
338
        }
339
340
        if( ! ( 'wpi_invoice' == $invoice->post_type OR 'wpi_quote' == $invoice->post_type ) ) {
341
            return false;
342
        }
343
344
        // Retrieve post data.
345
        $table = $wpdb->prefix . 'getpaid_invoices';
346
        $data  = $wpdb->get_row(
347
            $wpdb->prepare( "SELECT * FROM $table WHERE post_id=%d", $invoice->ID )
348
        );
349
350
        do_action( 'wpinv_pre_setup_invoice', $this, $invoice->ID, $data );
351
352
        // Primary Identifier
353
        $this->ID              = absint( $invoice->ID );
354
        $this->post_type       = $invoice->post_type;
355
356
        $this->date            = $invoice->post_date;
357
        $this->status          = $invoice->post_status;
358
359
        if ( 'future' == $this->status ) {
360
            $this->status = 'publish';
361
        }
362
363
        $this->post_status     = $this->status;
364
        $this->parent_invoice  = $invoice->post_parent;
365
        $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...
366
        $this->status_nicename = $this->setup_status_nicename( $invoice->post_status );
367
368
        $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...
369
        $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

369
        $this->email           = get_the_author_meta( 'email', /** @scrutinizer ignore-type */ $this->user_id );
Loading history...
370
        $this->currency        = wpinv_get_currency();
371
        $this->setup_invoice_data( $data );
372
373
        // Other Identifiers
374
        $this->title           = ! empty( $invoice->post_title ) ? $invoice->post_title : $this->number;
375
376
        // Allow extensions to add items to this object via hook
377
        do_action( 'wpinv_setup_invoice', $this, $invoice->ID, $data );
378
379
        return true;
380
    }
381
382
    /**
383
     * @param stdClass $data The invoice data.
384
     */
385
    private function setup_invoice_data( $data ) {
386
387
        if ( empty( $data ) ) {
388
            $this->number = $this->setup_invoice_number( $data );
389
            return;
390
        }
391
392
        $data = map_deep( $data, 'maybe_unserialize' );
393
394
        $this->payment_meta    = is_array( $data->custom_meta ) ? $data->custom_meta : array();
395
        $this->due_date        = $data->due_date;
396
        $this->completed_date  = $data->completed_date;
397
        $this->mode            = $data->mode;
398
399
        // Items
400
        $this->fees            = $this->setup_fees();
401
        $this->cart_details    = $this->setup_cart_details();
402
        $this->items           = ! empty( $this->payment_meta['items'] ) ? $this->payment_meta['items'] : array();
403
404
        // Currency Based
405
        $this->total           = $data->total;
406
        $this->disable_taxes   = (int) $data->disable_taxes;
407
        $this->tax             = $data->tax;
408
        $this->fees_total      = $data->fees_total;
409
        $this->subtotal        = $data->subtotal;
410
        $this->currency        = empty( $data->currency ) ? wpinv_get_currency() : $data->currency ;
411
412
        // Gateway based
413
        $this->gateway         = $data->gateway;
414
        $this->gateway_title   = $this->setup_gateway_title();
415
        $this->transaction_id  = $data->transaction_id;
416
417
        // User based
418
        $this->ip              = $data->user_ip;
419
        $this->user_info       = ! empty( $this->payment_meta['user_info'] ) ? $this->payment_meta['user_info'] : array();
420
421
        $this->first_name      = $data->first_name;
422
        $this->last_name       = $data->last_name;
423
        $this->company         = $data->company;
424
        $this->vat_number      = $data->vat_number;
425
        $this->vat_rate        = $data->vat_rate;
426
        $this->adddress_confirmed  = (int) $data->adddress_confirmed;
427
        $this->address         = $data->address;
428
        $this->city            = $data->city;
429
        $this->country         = $data->country;
430
        $this->state           = $data->state;
431
        $this->zip             = $data->zip;
432
        $this->phone           = ! empty( $this->user_info['phone'] ) ? $this->user_info['phone'] : '';
433
434
        $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...
435
        $this->discount        = $data->discount;
436
        $this->discount_code   = $data->discount_code;
437
438
        // Other Identifiers
439
        $this->key             = $data->key;
440
        $this->number          = $this->setup_invoice_number( $data );
441
442
        $this->full_name       = trim( $this->first_name . ' '. $this->last_name );
443
444
445
        return true;
446
    }
447
448
449
    /**
450
     * Sets up the status nice name.
451
     */
452
    private function setup_status_nicename( $status ) {
453
        $all_invoice_statuses  = wpinv_get_invoice_statuses( true, true, $this );
454
455
        if ( $this->is_quote() && class_exists( 'Wpinv_Quotes_Shared' ) ) {
456
            $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...
457
        }
458
        $status   = isset( $all_invoice_statuses[$status] ) ? $all_invoice_statuses[$status] : __( $status, 'invoicing' );
459
460
        return apply_filters( 'setup_status_nicename', $status );
461
    }
462
463
    /**
464
     * Set's up the invoice number.
465
     */
466
    private function setup_invoice_number( $data ) {
467
468
        if ( ! empty( $data ) && ! empty( $data->number ) ) {
469
            return $data->number;
470
        }
471
472
        $number = $this->ID;
473
474
        if ( $this->status == 'auto-draft' && wpinv_sequential_number_active( $this->post_type ) ) {
475
            $next_number = wpinv_get_next_invoice_number( $this->post_type );
476
            $number      = $next_number;
477
        }
478
        
479
        return wpinv_format_invoice_number( $number, $this->post_type );
480
481
    }
482
483
    /**
484
     * Invoice's post name.
485
     */
486
    private function setup_post_name( $post = NULL ) {
487
        global $wpdb;
488
        
489
        $post_name = '';
490
491
        if ( !empty( $post ) ) {
492
            if( !empty( $post->post_name ) ) {
493
                $post_name = $post->post_name;
494
            } else if ( !empty( $post->ID ) ) {
495
                $post_name = wpinv_generate_post_name( $post->ID );
496
497
                $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) );
498
            }
499
        }
500
501
        $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...
502
    }
503
504
    /**
505
     * Set's up the cart details.
506
     */
507
    public function setup_cart_details() {
508
        global $wpdb;
509
510
        $table =  $wpdb->prefix . 'getpaid_invoice_items';
511
        $items = $wpdb->get_results(
512
            $wpdb->prepare( "SELECT * FROM $table WHERE `post_id`=%d", $this->ID )
513
        );
514
515
        if ( empty( $items ) ) {
516
            return array();
517
        }
518
519
        $details = array();
520
521
        foreach ( $items as $item ) {
522
            $item = (array) $item;
523
            $details[] = array(
524
                'name'          => $item['item_name'],
525
                'id'            => $item['item_id'],
526
                'item_price'    => $item['item_price'],
527
                'custom_price'  => $item['custom_price'],
528
                'quantity'      => $item['quantity'],
529
                'discount'      => $item['discount'],
530
                'subtotal'      => $item['subtotal'],
531
                'tax'           => $item['tax'],
532
                'price'         => $item['price'],
533
                'vat_rate'      => $item['vat_rate'],
534
                'vat_class'     => $item['vat_class'],
535
                'meta'          => $item['meta'],
536
                'fees'          => $item['fees'],
537
            );
538
        }
539
540
        return map_deep( $details, 'maybe_unserialize' );
541
542
    }
543
544
    /**
545
     * Convert this to an array.
546
     */
547
    public function array_convert() {
548
        return get_object_vars( $this );
549
    }
550
    
551
    private function setup_fees() {
552
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
553
        return $payment_fees;
554
    }
555
556
    private function setup_gateway_title() {
557
        $gateway_title = wpinv_get_gateway_checkout_label( $this->gateway );
558
        return $gateway_title;
559
    }
560
    
561
    /**
562
     * Refreshes payment data.
563
     */
564
    private function refresh_payment_data() {
565
566
        $payment_data = array(
567
            'price'        => $this->total,
568
            'date'         => $this->date,
569
            'user_email'   => $this->email,
570
            'invoice_key'  => $this->key,
571
            'currency'     => $this->currency,
572
            'items'        => $this->items,
573
            'user_info' => array(
574
                'user_id'    => $this->user_id,
575
                'email'      => $this->email,
576
                'first_name' => $this->first_name,
577
                'last_name'  => $this->last_name,
578
                'address'    => $this->address,
579
                'phone'      => $this->phone,
580
                'city'       => $this->city,
581
                'country'    => $this->country,
582
                'state'      => $this->state,
583
                'zip'        => $this->zip,
584
                'company'    => $this->company,
585
                'vat_number' => $this->vat_number,
586
                'discount'   => $this->discounts,
587
            ),
588
            'cart_details' => $this->cart_details,
589
            'status'       => $this->status,
590
            'fees'         => $this->fees,
591
        );
592
593
        $this->payment_meta = array_merge( $this->payment_meta, $payment_data );
594
595
    }
596
597
    private function insert_invoice() {
598
599
        if ( empty( $this->post_type ) ) {
600
            if ( !empty( $this->ID ) && $post_type = get_post_type( $this->ID ) ) {
601
                $this->post_type = $post_type;
602
            } else if ( !empty( $this->parent_invoice ) && $post_type = get_post_type( $this->parent_invoice ) ) {
603
                $this->post_type = $post_type;
604
            } else {
605
                $this->post_type = 'wpi_invoice';
606
            }
607
        }
608
609
        $invoice_number = $this->ID;
610
        if ( $number = $this->number ) {
611
            $invoice_number = $number;
612
        }
613
614
        if ( empty( $this->key ) ) {
615
            $this->key = $this->generate_key();
616
            $this->pending['key'] = $this->key;
617
        }
618
619
        if ( empty( $this->ip ) ) {
620
            $this->ip = wpinv_get_ip();
621
            $this->pending['ip'] = $this->ip;
622
        }
623
624
        $payment_data = array(
625
            'price'        => $this->total,
626
            'date'         => $this->date,
627
            'user_email'   => $this->email,
628
            'invoice_key'  => $this->key,
629
            'currency'     => $this->currency,
630
            'items'        => $this->items,
631
            'user_info' => array(
632
                'user_id'    => $this->user_id,
633
                'email'      => $this->email,
634
                'first_name' => $this->first_name,
635
                'last_name'  => $this->last_name,
636
                'address'    => $this->address,
637
                'phone'      => $this->phone,
638
                'city'       => $this->city,
639
                'country'    => $this->country,
640
                'state'      => $this->state,
641
                'zip'        => $this->zip,
642
                'company'    => $this->company,
643
                'vat_number' => $this->vat_number,
644
                'discount'   => $this->discounts,
645
            ),
646
            'cart_details' => $this->cart_details,
647
            'status'       => $this->status,
648
            'fees'         => $this->fees,
649
        );
650
651
        $post_data = array(
652
            'post_title'    => $invoice_number,
653
            'post_status'   => $this->status,
654
            'post_author'   => $this->user_id,
655
            'post_type'     => $this->post_type,
656
            'post_date'     => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? $this->date : current_time( 'mysql' ),
657
            'post_date_gmt' => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? get_gmt_from_date( $this->date ) : current_time( 'mysql', 1 ),
658
            'post_parent'   => $this->parent_invoice,
659
        );
660
        $args = apply_filters( 'wpinv_insert_invoice_args', $post_data, $this );
661
662
        // Create a blank invoice
663
        if ( !empty( $this->ID ) ) {
664
            $args['ID']         = $this->ID;
665
            $invoice_id = wp_update_post( $args, true );
666
        } else {
667
            $invoice_id = wp_insert_post( $args, true );
668
        }
669
670
        if ( is_wp_error( $invoice_id ) ) {
671
            return false;
672
        }
673
674
        if ( ! empty( $invoice_id ) ) {
675
            $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...
676
            $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...
677
678
            $this->payment_meta = array_merge( $this->payment_meta, $payment_data );
679
680
            if ( ! empty( $this->payment_meta['fees'] ) ) {
681
                $this->fees = array_merge( $this->fees, $this->payment_meta['fees'] );
682
                foreach( $this->fees as $fee ) {
683
                    $this->increase_fees( $fee['amount'] );
684
                }
685
            }
686
687
            $this->pending['payment_meta'] = $this->payment_meta;
688
            $this->save();
689
        }
690
691
        return $this->ID;
692
    }
693
694
    /**
695
     * Saves special fields in our custom table.
696
     */
697
    public function get_special_fields() {
698
699
        return array (
700
            'post_id'        => $this->ID,
701
            'number'         => $this->get_number(),
702
            'key'            => $this->get_key(),
703
            'type'           => str_replace( 'wpi_', '', $this->post_type ),
704
            'mode'           => $this->mode,
705
            'user_ip'        => $this->get_ip(),
706
            'first_name'     => $this->get_first_name(),
707
            'last_name'      => $this->get_last_name(),
708
            'address'        => $this->get_address(),
709
            'city'           => $this->city,
710
            'state'          => $this->state,
711
            'country'        => $this->country,
712
            'zip'            => $this->zip,
713
            'adddress_confirmed' => (int) $this->adddress_confirmed,
714
            'gateway'        => $this->get_gateway(),
715
            'transaction_id' => $this->get_transaction_id(),
716
            'currency'       => $this->get_currency(),
717
            'subtotal'       => $this->get_subtotal(),
718
            'tax'            => $this->get_tax(),
719
            'fees_total'     => $this->get_fees_total(),
720
            'total'          => $this->get_total(),
721
            'discount'       => $this->get_discount(),
722
            'discount_code'  => $this->get_discount_code(),
723
            'disable_taxes'  => $this->disable_taxes,
724
            'due_date'       => $this->get_due_date(),
725
            'completed_date' => $this->get_completed_date(),
726
            'company'        => $this->company,
727
            'vat_number'     => $this->vat_number,
728
            'vat_rate'       => $this->vat_rate,
729
            'custom_meta'    => $this->payment_meta
730
        );
731
732
    }
733
734
    /**
735
     * Saves special fields in our custom table.
736
     */
737
    public function save_special() {
738
        global $wpdb;
739
740
        $this->refresh_payment_data();
741
742
        $fields = $this->get_special_fields();
743
        $fields = array_map( 'maybe_serialize', $fields );
744
745
        $table =  $wpdb->prefix . 'getpaid_invoices';
746
747
        $id = (int) $this->ID;
748
749
        if ( empty( $id ) ) {
750
            return;
751
        }
752
753
        if ( $wpdb->get_var( "SELECT `post_id` FROM $table WHERE `post_id`=$id" ) ) {
754
755
            $wpdb->update( $table, $fields, array( 'post_id' => $id ) );
756
757
        } else {
758
759
            $wpdb->insert( $table, $fields );
760
761
        }
762
763
        $table =  $wpdb->prefix . 'getpaid_invoice_items';
764
        $wpdb->delete( $table, array( 'post_id' => $this->ID ) );
765
766
        foreach ( $this->get_cart_details() as $details ) {
767
            $fields = array(
768
                'post_id'          => $this->ID,
769
                'item_id'          => $details['id'],
770
                'item_name'        => $details['name'],
771
                'item_description' => empty( $details['meta']['description'] ) ? '' : $details['meta']['description'],
772
                'vat_rate'         => $details['vat_rate'],
773
                'vat_class'        => empty( $details['vat_class'] ) ? '_standard' : $details['vat_class'],
774
                'tax'              => $details['tax'],
775
                'item_price'       => $details['item_price'],
776
                'custom_price'     => $details['custom_price'],
777
                'quantity'         => $details['quantity'],
778
                'discount'         => $details['discount'],
779
                'subtotal'         => $details['subtotal'],
780
                'price'            => $details['price'],
781
                'meta'             => $details['meta'],
782
                'fees'             => $details['fees'],
783
            );
784
785
            $item_columns = array_keys ( $fields );
786
787
            foreach ( $fields as $key => $val ) {
788
                if ( is_null( $val ) ) {
789
                    $val = '';
790
                }
791
                $val = maybe_serialize( $val );
792
                $fields[ $key ] = $wpdb->prepare( '%s', $val );
793
            }
794
795
            $fields = implode( ', ', $fields );
796
            $item_rows[] = "($fields)";
797
        }
798
799
        $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 766. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
800
        $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 766. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
801
        $wpdb->query( "INSERT INTO $table ($item_columns) VALUES $item_rows" );
802
    }
803
804
    public function save( $setup = false ) {
805
        global $wpi_session;
806
        
807
        $saved = false;
808
        if ( empty( $this->items ) ) {
809
            return $saved;
810
        }
811
812
        if ( empty( $this->key ) ) {
813
            $this->key = $this->generate_key();
814
        }
815
816
        if ( empty( $this->ID ) ) {
817
            $invoice_id = $this->insert_invoice();
818
819
            if ( false === $invoice_id ) {
820
                $saved = false;
821
            } else {
822
                $this->ID = $invoice_id;
823
            }
824
        }
825
826
        // If we have something pending, let's save it
827
        if ( ! empty( $this->pending ) ) {
828
            $total_increase = 0;
829
            $total_decrease = 0;
830
831
            foreach ( $this->pending as $key => $value ) {
832
833
                switch( $key ) {
834
                    case 'items':
835
                        // Update totals for pending items
836
                        foreach ( $this->pending[ $key ] as $item ) {
837
                            switch( $item['action'] ) {
838
                                case 'add':
839
                                    $price = $item['price'];
840
                                    $taxes = $item['tax'];
0 ignored issues
show
Unused Code introduced by
The assignment to $taxes is dead and can be removed.
Loading history...
841
842
                                    if ( 'publish' === $this->status ) {
843
                                        $total_increase += $price;
844
                                    }
845
                                    break;
846
847
                                case 'remove':
848
                                    if ( 'publish' === $this->status ) {
849
                                        $total_decrease += $item['price'];
850
                                    }
851
                                    break;
852
                            }
853
                        }
854
                        break;
855
                    case 'fees':
856
                        if ( 'publish' !== $this->status ) {
857
                            break;
858
                        }
859
860
                        if ( empty( $this->pending[ $key ] ) ) {
861
                            break;
862
                        }
863
864
                        foreach ( $this->pending[ $key ] as $fee ) {
865
                            switch( $fee['action'] ) {
866
                                case 'add':
867
                                    $total_increase += $fee['amount'];
868
                                    break;
869
870
                                case 'remove':
871
                                    $total_decrease += $fee['amount'];
872
                                    break;
873
                            }
874
                        }
875
                        break;
876
                    case 'status':
877
                        $this->update_status( $this->status );
878
                        break;
879
                    case 'first_name':
880
                        $this->user_info['first_name'] = $this->first_name;
881
                        break;
882
                    case 'last_name':
883
                        $this->user_info['last_name'] = $this->last_name;
884
                        break;
885
                    case 'phone':
886
                        $this->user_info['phone'] = $this->phone;
887
                        break;
888
                    case 'address':
889
                        $this->user_info['address'] = $this->address;
890
                        break;
891
                    case 'city':
892
                        $this->user_info['city'] = $this->city;
893
                        break;
894
                    case 'country':
895
                        $this->user_info['country'] = $this->country;
896
                        break;
897
                    case 'state':
898
                        $this->user_info['state'] = $this->state;
899
                        break;
900
                    case 'zip':
901
                        $this->user_info['zip'] = $this->zip;
902
                        break;
903
                    case 'company':
904
                        $this->user_info['company'] = $this->company;
905
                        break;
906
                    case 'vat_number':
907
                        $this->user_info['vat_number'] = $this->vat_number;
908
                        
909
                        $vat_info = $wpi_session->get( 'user_vat_data' );
910
                        if ( $this->vat_number && !empty( $vat_info ) && isset( $vat_info['number'] ) && isset( $vat_info['valid'] ) && $vat_info['number'] == $this->vat_number ) {
911
                            $adddress_confirmed = isset( $vat_info['adddress_confirmed'] ) ? $vat_info['adddress_confirmed'] : false;
912
                            $this->update_meta( '_wpinv_adddress_confirmed', (bool)$adddress_confirmed );
913
                            $this->user_info['adddress_confirmed'] = (bool)$adddress_confirmed;
914
                            $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...
915
                        }
916
    
917
                        break;
918
                    case 'vat_rate':
919
                        $this->user_info['vat_rate'] = $this->vat_rate;
920
                        break;
921
                    case 'adddress_confirmed':
922
                        $this->user_info['adddress_confirmed'] = $this->adddress_confirmed;
923
                        break;
924
                    case 'date':
925
                        $args = array(
926
                            'ID'        => $this->ID,
927
                            'post_date' => $this->date,
928
                            'edit_date' => true,
929
                        );
930
931
                        wp_update_post( $args );
932
                        break;
933
                    case 'due_date':
934
                        if ( empty( $this->due_date ) ) {
935
                            $this->due_date = 'none';
936
                        }
937
                        break;
938
                    case 'discounts':
939
                        if ( ! is_array( $this->discounts ) ) {
940
                            $this->discounts = explode( ',', $this->discounts );
941
                        }
942
943
                        $this->user_info['discount'] = implode( ',', $this->discounts );
944
                        break;
945
                    case 'parent_invoice':
946
                        $args = array(
947
                            'ID'          => $this->ID,
948
                            'post_parent' => $this->parent_invoice,
949
                        );
950
                        wp_update_post( $args );
951
                        break;
952
                    default:
953
                        do_action( 'wpinv_save', $this, $key );
954
                        break;
955
                }
956
            }
957
958
            $this->items    = array_values( $this->items );
959
960
            $this->pending      = array();
961
            $saved              = true;
962
        }
963
964
        $new_meta = array(
965
            'items'         => $this->items,
966
            'cart_details'  => $this->cart_details,
967
            'fees'          => $this->fees,
968
            'currency'      => $this->currency,
969
            'user_info'     => $this->user_info,
970
        );
971
        $this->payment_meta = array_merge( $this->payment_meta, $new_meta );
972
        $this->update_items();
973
974
        $this->save_special();
975
        do_action( 'wpinv_invoice_save', $this, $saved );
976
977
        if ( true === $saved || $setup ) {
978
            $this->setup_invoice( $this->ID );
979
        }
980
981
        $this->refresh_item_ids();
982
983
        return $saved;
984
    }
985
    
986
    public function add_fee( $args, $global = true ) {
987
        $default_args = array(
988
            'label'       => '',
989
            'amount'      => 0,
990
            'type'        => 'fee',
991
            'id'          => '',
992
            'no_tax'      => false,
993
            'item_id'     => 0,
994
        );
995
996
        $fee = wp_parse_args( $args, $default_args );
997
        
998
        if ( empty( $fee['label'] ) ) {
999
            return false;
1000
        }
1001
        
1002
        $fee['id']  = sanitize_title( $fee['label'] );
1003
        
1004
        $this->fees[]               = $fee;
1005
        
1006
        $added_fee               = $fee;
1007
        $added_fee['action']     = 'add';
1008
        $this->pending['fees'][] = $added_fee;
1009
        reset( $this->fees );
1010
1011
        $this->increase_fees( $fee['amount'] );
1012
        return true;
1013
    }
1014
1015
    public function remove_fee( $key ) {
1016
        $removed = false;
1017
1018
        if ( is_numeric( $key ) ) {
1019
            $removed = $this->remove_fee_by( 'index', $key );
1020
        }
1021
1022
        return $removed;
1023
    }
1024
1025
    public function remove_fee_by( $key, $value, $global = false ) {
1026
        $allowed_fee_keys = apply_filters( 'wpinv_fee_keys', array(
1027
            'index', 'label', 'amount', 'type',
1028
        ) );
1029
1030
        if ( ! in_array( $key, $allowed_fee_keys ) ) {
1031
            return false;
1032
        }
1033
1034
        $removed = false;
1035
        if ( 'index' === $key && array_key_exists( $value, $this->fees ) ) {
1036
            $removed_fee             = $this->fees[ $value ];
1037
            $removed_fee['action']   = 'remove';
1038
            $this->pending['fees'][] = $removed_fee;
1039
1040
            $this->decrease_fees( $removed_fee['amount'] );
1041
1042
            unset( $this->fees[ $value ] );
1043
            $removed = true;
1044
        } else if ( 'index' !== $key ) {
1045
            foreach ( $this->fees as $index => $fee ) {
1046
                if ( isset( $fee[ $key ] ) && $fee[ $key ] == $value ) {
1047
                    $removed_fee             = $fee;
1048
                    $removed_fee['action']   = 'remove';
1049
                    $this->pending['fees'][] = $removed_fee;
1050
1051
                    $this->decrease_fees( $removed_fee['amount'] );
1052
1053
                    unset( $this->fees[ $index ] );
1054
                    $removed = true;
1055
1056
                    if ( false === $global ) {
1057
                        break;
1058
                    }
1059
                }
1060
            }
1061
        }
1062
1063
        if ( true === $removed ) {
1064
            $this->fees = array_values( $this->fees );
1065
        }
1066
1067
        return $removed;
1068
    }
1069
1070
    
1071
1072
    public function add_note( $note = '', $customer_type = false, $added_by_user = false, $system = false ) {
1073
        // Bail if no note specified
1074
        if( !$note ) {
1075
            return false;
1076
        }
1077
1078
        if ( empty( $this->ID ) )
1079
            return false;
1080
        
1081
        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...
1082
            $user                 = get_user_by( 'id', get_current_user_id() );
1083
            $comment_author       = $user->display_name;
1084
            $comment_author_email = $user->user_email;
1085
        } else {
1086
            $comment_author       = 'System';
1087
            $comment_author_email = 'system@';
1088
            $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com';
1089
            $comment_author_email = sanitize_email( $comment_author_email );
1090
        }
1091
1092
        do_action( 'wpinv_pre_insert_invoice_note', $this->ID, $note, $customer_type );
1093
1094
        $note_id = wp_insert_comment( wp_filter_comment( array(
1095
            'comment_post_ID'      => $this->ID,
1096
            'comment_content'      => $note,
1097
            'comment_agent'        => 'WPInvoicing',
1098
            'user_id'              => is_admin() ? get_current_user_id() : 0,
1099
            'comment_date'         => current_time( 'mysql' ),
1100
            'comment_date_gmt'     => current_time( 'mysql', 1 ),
1101
            'comment_approved'     => 1,
1102
            'comment_parent'       => 0,
1103
            'comment_author'       => $comment_author,
1104
            'comment_author_IP'    => wpinv_get_ip(),
1105
            'comment_author_url'   => '',
1106
            'comment_author_email' => $comment_author_email,
1107
            'comment_type'         => 'wpinv_note'
1108
        ) ) );
1109
1110
        do_action( 'wpinv_insert_payment_note', $note_id, $this->ID, $note );
1111
        
1112
        if ( $customer_type ) {
1113
            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

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

1226
            do_action( 'wpinv_status_' . /** @scrutinizer ignore-type */ $old_status . '_to_' . $new_status, $this->ID, $old_status );
Loading history...
1227
            do_action( 'wpinv_update_status', $this->ID, $new_status, $old_status );
1228
        }
1229
1230
        return $updated;
1231
    }
1232
1233
    public function refund() {
1234
        $this->old_status        = $this->status;
1235
        $this->status            = 'wpi-refunded';
1236
        $this->pending['status'] = $this->status;
1237
1238
        $this->save();
1239
    }
1240
1241
    public function update_meta( $meta_key = '', $meta_value = '', $prev_value = '' ) {
1242
        if ( empty( $meta_key ) ) {
1243
            return false;
1244
        }
1245
1246
        if ( $meta_key == 'key' || $meta_key == 'date' ) {
1247
            $current_meta = $this->get_meta();
1248
            $current_meta[ $meta_key ] = $meta_value;
1249
1250
            $meta_key     = '_wpinv_payment_meta';
1251
            $meta_value   = $current_meta;
1252
        }
1253
1254
        $key  = str_ireplace( '_wpinv_', '', $meta_key );
1255
        $this->$key = $meta_value;
1256
1257
        $special = array_keys( $this->get_special_fields() );
1258
        if ( in_array( $key, $special ) ) {
1259
            $this->save_special();
1260
        } else {
1261
            $meta_value = apply_filters( 'wpinv_update_payment_meta_' . $meta_key, $meta_value, $this->ID );
1262
        }
1263
1264
        return update_post_meta( $this->ID, $meta_key, $meta_value, $prev_value );
1265
    }
1266
1267
    private function process_refund() {
1268
        $process_refund = true;
1269
1270
        // If the payment was not in publish, don't decrement stats as they were never incremented
1271
        if ( 'publish' != $this->old_status || 'wpi-refunded' != $this->status ) {
1272
            $process_refund = false;
1273
        }
1274
1275
        // Allow extensions to filter for their own payment types, Example: Recurring Payments
1276
        $process_refund = apply_filters( 'wpinv_should_process_refund', $process_refund, $this );
1277
1278
        if ( false === $process_refund ) {
1279
            return;
1280
        }
1281
1282
        do_action( 'wpinv_pre_refund_invoice', $this );
1283
        
1284
        $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...
1285
        $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...
1286
        $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...
1287
        
1288
        do_action( 'wpinv_post_refund_invoice', $this );
1289
    }
1290
1291
    private function process_failure() {
1292
        $discounts = $this->discounts;
1293
        if ( empty( $discounts ) ) {
1294
            return;
1295
        }
1296
1297
        if ( ! is_array( $discounts ) ) {
0 ignored issues
show
introduced by
The condition is_array($discounts) is always true.
Loading history...
1298
            $discounts = array_map( 'trim', explode( ',', $discounts ) );
1299
        }
1300
1301
        foreach ( $discounts as $discount ) {
1302
            wpinv_decrease_discount_usage( $discount );
1303
        }
1304
    }
1305
    
1306
    private function process_pending() {
1307
        $process_pending = true;
1308
1309
        // If the payment was not in publish or revoked status, don't decrement stats as they were never incremented
1310
        if ( ( 'publish' != $this->old_status && 'revoked' != $this->old_status ) || 'wpi-pending' != $this->status ) {
1311
            $process_pending = false;
1312
        }
1313
1314
        // Allow extensions to filter for their own payment types, Example: Recurring Payments
1315
        $process_pending = apply_filters( 'wpinv_should_process_pending', $process_pending, $this );
1316
1317
        if ( false === $process_pending ) {
1318
            return;
1319
        }
1320
1321
        $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...
1322
        $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...
1323
        $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...
1324
1325
        $this->completed_date = '';
1326
        $this->update_meta( '_wpinv_completed_date', '' );
1327
    }
1328
    
1329
    // get data
1330
    public function get_meta( $meta_key = '_wpinv_payment_meta', $single = true ) {
1331
        $meta = get_post_meta( $this->ID, $meta_key, $single );
1332
1333
        if ( $meta_key === '_wpinv_payment_meta' ) {
1334
1335
            if(!is_array($meta)){$meta = array();} // we need this to be an array so make sure it is.
1336
1337
            if ( empty( $meta['key'] ) ) {
1338
                $meta['key'] = $this->key;
1339
            }
1340
1341
            if ( empty( $meta['date'] ) ) {
1342
                $meta['date'] = get_post_field( 'post_date', $this->ID );
1343
            }
1344
        }
1345
1346
        $meta = apply_filters( 'wpinv_get_invoice_meta_' . $meta_key, $meta, $this->ID );
1347
1348
        return apply_filters( 'wpinv_get_invoice_meta', $meta, $this->ID, $meta_key );
1349
    }
1350
    
1351
    public function get_description() {
1352
        $post = get_post( $this->ID );
1353
        
1354
        $description = !empty( $post ) ? $post->post_content : '';
1355
        return apply_filters( 'wpinv_get_description', $description, $this->ID, $this );
1356
    }
1357
    
1358
    public function get_status( $nicename = false ) {
1359
        if ( !$nicename ) {
1360
            $status = $this->status;
1361
        } else {
1362
            $status = $this->status_nicename;
1363
        }
1364
        
1365
        return apply_filters( 'wpinv_get_status', $status, $nicename, $this->ID, $this );
1366
    }
1367
    
1368
    public function get_cart_details() {
1369
        return apply_filters( 'wpinv_cart_details', $this->cart_details, $this->ID, $this );
1370
    }
1371
    
1372
    public function get_subtotal( $currency = false ) {
1373
        $subtotal = wpinv_round_amount( $this->subtotal );
1374
        
1375
        if ( $currency ) {
1376
            $subtotal = wpinv_price( wpinv_format_amount( $subtotal, NULL, !$currency ), $this->get_currency() );
1377
        }
1378
        
1379
        return apply_filters( 'wpinv_get_invoice_subtotal', $subtotal, $this->ID, $this, $currency );
1380
    }
1381
    
1382
    public function get_total( $currency = false ) {        
1383
        if ( $this->is_free_trial() ) {
1384
            $total = wpinv_round_amount( 0 );
1385
        } else {
1386
            $total = wpinv_round_amount( $this->total );
1387
        }
1388
        if ( $currency ) {
1389
            $total = wpinv_price( wpinv_format_amount( $total, NULL, !$currency ), $this->get_currency() );
1390
        }
1391
        
1392
        return apply_filters( 'wpinv_get_invoice_total', $total, $this->ID, $this, $currency );
1393
    }
1394
1395
    /**
1396
     * Returns recurring payment details.
1397
     */
1398
    public function get_recurring_details( $field = '', $currency = false ) {        
1399
        $data                 = array();
1400
        $data['cart_details'] = $this->cart_details;
1401
        $data['subtotal']     = $this->get_subtotal();
1402
        $data['discount']     = $this->get_discount();
1403
        $data['tax']          = $this->get_tax();
1404
        $data['total']        = $this->get_total();
1405
1406
        if ( $this->is_parent() || $this->is_renewal() ) {
1407
1408
            // Use the parent to calculate recurring details.
1409
            if ( $this->is_renewal() ){
1410
                $parent = $this->get_parent_payment();
1411
            } else {
1412
                $parent = $this;
1413
            }
1414
1415
            if ( empty( $parent ) ) {
1416
                $parent = $this;
1417
            }
1418
1419
            // Subtotal.
1420
            $data['subtotal'] = wpinv_round_amount( $parent->subtotal );
1421
            $data['tax']      = wpinv_round_amount( $parent->tax );
1422
            $data['discount'] = wpinv_round_amount( $parent->discount );
1423
1424
            if ( $data['discount'] > 0 && $parent->discount_first_payment_only() ) {
1425
                $data['discount'] = wpinv_round_amount( 0 );
1426
            }
1427
1428
            $data['total'] = wpinv_round_amount( $data['subtotal'] + $data['tax'] - $data['discount'] );
1429
1430
        }
1431
        
1432
        $data = apply_filters( 'wpinv_get_invoice_recurring_details', $data, $this, $field, $currency );
1433
1434
        if ( $data['total'] < 0 ) {
1435
            $data['total'] = 0;
1436
        }
1437
1438
        if ( isset( $data[$field] ) ) {
1439
            return ( $currency ? wpinv_price( $data[$field], $this->get_currency() ) : $data[$field] );
1440
        }
1441
        
1442
        return $data;
1443
    }
1444
    
1445
    public function get_final_tax( $currency = false ) {        
1446
        $final_total = wpinv_round_amount( $this->tax );
1447
        if ( $currency ) {
1448
            $final_total = wpinv_price( wpinv_format_amount( $final_total, NULL, !$currency ), $this->get_currency() );
1449
        }
1450
        
1451
        return apply_filters( 'wpinv_get_invoice_final_total', $final_total, $this, $currency );
1452
    }
1453
    
1454
    public function get_discounts( $array = false ) {
1455
        $discounts = $this->discounts;
1456
        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...
1457
            $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

1457
            $discounts = explode( ',', /** @scrutinizer ignore-type */ $discounts );
Loading history...
1458
        }
1459
        return apply_filters( 'wpinv_payment_discounts', $discounts, $this->ID, $this, $array );
1460
    }
1461
    
1462
    public function get_discount( $currency = false, $dash = false ) {
1463
        if ( !empty( $this->discounts ) ) {
1464
            global $ajax_cart_details;
1465
            $ajax_cart_details = $this->get_cart_details();
1466
            
1467
            if ( !empty( $ajax_cart_details ) && count( $ajax_cart_details ) == count( $this->items ) ) {
1468
                $cart_items = $ajax_cart_details;
1469
            } else {
1470
                $cart_items = $this->items;
1471
            }
1472
1473
            $this->discount = wpinv_get_cart_items_discount_amount( $cart_items , $this->discounts );
1474
        }
1475
        $discount   = wpinv_round_amount( $this->discount );
1476
        $dash       = $dash && $discount > 0 ? '&ndash;' : '';
1477
        
1478
        if ( $currency ) {
1479
            $discount = wpinv_price( wpinv_format_amount( $discount, NULL, !$currency ), $this->get_currency() );
1480
        }
1481
        
1482
        $discount   = $dash . $discount;
1483
        
1484
        return apply_filters( 'wpinv_get_invoice_discount', $discount, $this->ID, $this, $currency, $dash );
1485
    }
1486
    
1487
    public function get_discount_code() {
1488
        return $this->discount_code;
1489
    }
1490
1491
    // Checks if the invoice is taxable. Does not check if taxes are enabled on the site.
1492
    public function is_taxable() {
1493
        return (int) $this->disable_taxes === 0;
1494
    }
1495
1496
    public function get_tax( $currency = false ) {
1497
        $tax = wpinv_round_amount( $this->tax );
1498
1499
        if ( $currency ) {
1500
            $tax = wpinv_price( wpinv_format_amount( $tax, NULL, !$currency ), $this->get_currency() );
1501
        }
1502
1503
        if ( ! $this->is_taxable() ) {
1504
            $tax = wpinv_round_amount( 0.00 );
1505
        }
1506
1507
        return apply_filters( 'wpinv_get_invoice_tax', $tax, $this->ID, $this, $currency );
1508
    }
1509
    
1510
    public function get_fees( $type = 'all' ) {
1511
        $fees    = array();
1512
1513
        if ( ! empty( $this->fees ) && is_array( $this->fees ) ) {
1514
            foreach ( $this->fees as $fee ) {
1515
                if( 'all' != $type && ! empty( $fee['type'] ) && $type != $fee['type'] ) {
1516
                    continue;
1517
                }
1518
1519
                $fee['label'] = stripslashes( $fee['label'] );
1520
                $fee['amount_display'] = wpinv_price( $fee['amount'], $this->get_currency() );
1521
                $fees[]    = $fee;
1522
            }
1523
        }
1524
1525
        return apply_filters( 'wpinv_get_invoice_fees', $fees, $this->ID, $this );
1526
    }
1527
    
1528
    public function get_fees_total( $type = 'all' ) {
1529
        $fees_total = (float) 0.00;
1530
1531
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
1532
        if ( ! empty( $payment_fees ) ) {
1533
            foreach ( $payment_fees as $fee ) {
1534
                $fees_total += (float) $fee['amount'];
1535
            }
1536
        }
1537
1538
        return apply_filters( 'wpinv_get_invoice_fees_total', $fees_total, $this->ID, $this );
1539
1540
    }
1541
1542
    public function get_user_id() {
1543
        return apply_filters( 'wpinv_user_id', $this->user_id, $this->ID, $this );
1544
    }
1545
    
1546
    public function get_first_name() {
1547
        return apply_filters( 'wpinv_first_name', $this->first_name, $this->ID, $this );
1548
    }
1549
    
1550
    public function get_last_name() {
1551
        return apply_filters( 'wpinv_last_name', $this->last_name, $this->ID, $this );
1552
    }
1553
    
1554
    public function get_user_full_name() {
1555
        return apply_filters( 'wpinv_user_full_name', $this->full_name, $this->ID, $this );
1556
    }
1557
    
1558
    public function get_user_info() {
1559
        return apply_filters( 'wpinv_user_info', $this->user_info, $this->ID, $this );
1560
    }
1561
    
1562
    public function get_email() {
1563
        return apply_filters( 'wpinv_user_email', $this->email, $this->ID, $this );
1564
    }
1565
    
1566
    public function get_address() {
1567
        return apply_filters( 'wpinv_address', $this->address, $this->ID, $this );
1568
    }
1569
    
1570
    public function get_phone() {
1571
        return apply_filters( 'wpinv_phone', $this->phone, $this->ID, $this );
1572
    }
1573
    
1574
    public function get_number() {
1575
        return apply_filters( 'wpinv_number', $this->number, $this->ID, $this );
1576
    }
1577
    
1578
    public function get_items() {
1579
        return apply_filters( 'wpinv_payment_meta_items', $this->items, $this->ID, $this );
1580
    }
1581
    
1582
    public function get_key() {
1583
        return apply_filters( 'wpinv_key', $this->key, $this->ID, $this );
1584
    }
1585
    
1586
    public function get_transaction_id() {
1587
        return apply_filters( 'wpinv_get_invoice_transaction_id', $this->transaction_id, $this->ID, $this );
1588
    }
1589
    
1590
    public function get_gateway() {
1591
        return apply_filters( 'wpinv_gateway', $this->gateway, $this->ID, $this );
1592
    }
1593
    
1594
    public function get_gateway_title() {
1595
        $this->gateway_title = !empty( $this->gateway_title ) ? $this->gateway_title : wpinv_get_gateway_checkout_label( $this->gateway );
1596
        
1597
        return apply_filters( 'wpinv_gateway_title', $this->gateway_title, $this->ID, $this );
1598
    }
1599
    
1600
    public function get_currency() {
1601
        return apply_filters( 'wpinv_currency_code', $this->currency, $this->ID, $this );
1602
    }
1603
    
1604
    public function get_created_date() {
1605
        return apply_filters( 'wpinv_created_date', $this->date, $this->ID, $this );
1606
    }
1607
    
1608
    public function get_due_date( $display = false ) {
1609
        $due_date = apply_filters( 'wpinv_due_date', $this->due_date, $this->ID, $this );
1610
        
1611
        if ( !$display || empty( $due_date ) ) {
1612
            return $due_date;
1613
        }
1614
        
1615
        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

1615
        return date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $due_date ) );
Loading history...
1616
    }
1617
    
1618
    public function get_completed_date() {
1619
        return apply_filters( 'wpinv_completed_date', $this->completed_date, $this->ID, $this );
1620
    }
1621
    
1622
    public function get_invoice_date( $formatted = true ) {
1623
        $date_completed = $this->completed_date;
1624
        $invoice_date   = $date_completed != '' && $date_completed != '0000-00-00 00:00:00' ? $date_completed : '';
1625
        
1626
        if ( $invoice_date == '' ) {
1627
            $date_created   = $this->date;
1628
            $invoice_date   = $date_created != '' && $date_created != '0000-00-00 00:00:00' ? $date_created : '';
1629
        }
1630
        
1631
        if ( $formatted && $invoice_date ) {
1632
            $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

1632
            $invoice_date   = date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $invoice_date ) );
Loading history...
1633
        }
1634
1635
        return apply_filters( 'wpinv_get_invoice_date', $invoice_date, $formatted, $this->ID, $this );
1636
    }
1637
    
1638
    public function get_ip() {
1639
        return apply_filters( 'wpinv_user_ip', $this->ip, $this->ID, $this );
1640
    }
1641
        
1642
    public function has_status( $status ) {
1643
        return apply_filters( 'wpinv_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status ) ) || $this->get_status() === $status ? true : false, $this, $status );
1644
    }
1645
    
1646
    public function add_item( $item_id = 0, $args = array() ) {
1647
        global $wpi_current_id, $wpi_item_id;
1648
    
1649
        $item = new WPInv_Item( $item_id );
1650
1651
        // Bail if this post isn't a item
1652
        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...
1653
            return false;
1654
        }
1655
        
1656
        $has_quantities = wpinv_item_quantities_enabled();
1657
1658
        // Set some defaults
1659
        $defaults = array(
1660
            'quantity'      => 1,
1661
            'id'            => false,
1662
            'name'          => $item->get_name(),
1663
            'item_price'    => false,
1664
            'custom_price'  => '',
1665
            'discount'      => 0,
1666
            'tax'           => 0.00,
1667
            'meta'          => array(),
1668
            'fees'          => array()
1669
        );
1670
1671
        $args = wp_parse_args( apply_filters( 'wpinv_add_item_args', $args, $item->ID ), $defaults );
1672
        $args['quantity']   = $has_quantities && $args['quantity'] > 0 ? absint( $args['quantity'] ) : 1;
1673
1674
        $wpi_current_id         = $this->ID;
1675
        $wpi_item_id            = $item->ID;
1676
        $discounts              = $this->get_discounts();
0 ignored issues
show
Unused Code introduced by
The assignment to $discounts is dead and can be removed.
Loading history...
1677
        
1678
        $_POST['wpinv_country'] = $this->country;
1679
        $_POST['wpinv_state']   = $this->state;
1680
        
1681
        $found_cart_key         = false;
1682
        
1683
        if ($has_quantities) {
1684
            $this->cart_details = !empty( $this->cart_details ) ? array_values( $this->cart_details ) : $this->cart_details;
1685
            
1686
            foreach ( $this->items as $key => $cart_item ) {
1687
                if ( (int)$item_id !== (int)$cart_item['id'] ) {
1688
                    continue;
1689
                }
1690
1691
                $this->items[ $key ]['quantity'] += $args['quantity'];
1692
                break;
1693
            }
1694
            
1695
            foreach ( $this->cart_details as $cart_key => $cart_item ) {
1696
                if ( $item_id != $cart_item['id'] ) {
1697
                    continue;
1698
                }
1699
1700
                $found_cart_key = $cart_key;
1701
                break;
1702
            }
1703
        }
1704
        
1705
        if ($has_quantities && $found_cart_key !== false) {
1706
            $cart_item          = $this->cart_details[$found_cart_key];
1707
            $item_price         = $cart_item['item_price'];
1708
            $quantity           = !empty( $cart_item['quantity'] ) ? $cart_item['quantity'] : 1;
1709
            $tax_rate           = !empty( $cart_item['vat_rate'] ) ? $cart_item['vat_rate'] : 0;
1710
            
1711
            $new_quantity       = $quantity + $args['quantity'];
1712
            $subtotal           = $item_price * $new_quantity;
1713
            
1714
            $args['quantity']   = $new_quantity;
1715
            $discount           = !empty( $args['discount'] ) ? $args['discount'] : 0;
1716
            $tax                = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1717
            
1718
            $discount_increased = $discount > 0 && $subtotal > 0 && $discount > (float)$cart_item['discount'] ? $discount - (float)$cart_item['discount'] : 0;
1719
            $tax_increased      = $tax > 0 && $subtotal > 0 && $tax > (float)$cart_item['tax'] ? $tax - (float)$cart_item['tax'] : 0;
1720
            // The total increase equals the number removed * the item_price
1721
            $total_increased    = wpinv_round_amount( $item_price );
1722
            
1723
            if ( wpinv_prices_include_tax() ) {
1724
                $subtotal -= wpinv_round_amount( $tax );
1725
            }
1726
1727
            $total              = $subtotal - $discount + $tax;
1728
1729
            // Do not allow totals to go negative
1730
            if( $total < 0 ) {
1731
                $total = 0;
1732
            }
1733
            
1734
            $cart_item['quantity']  = $new_quantity;
1735
            $cart_item['subtotal']  = $subtotal;
1736
            $cart_item['discount']  = $discount;
1737
            $cart_item['tax']       = $tax;
1738
            $cart_item['price']     = $total;
1739
            
1740
            $subtotal               = $total_increased - $discount_increased;
1741
            $tax                    = $tax_increased;
1742
            
1743
            $this->cart_details[$found_cart_key] = $cart_item;
1744
        } else {
1745
            // Set custom price.
1746
            if ( $args['custom_price'] !== '' ) {
1747
                $item_price = $args['custom_price'];
1748
            } else {
1749
                // Allow overriding the price
1750
                if ( false !== $args['item_price'] ) {
1751
                    $item_price = $args['item_price'];
1752
                } else {
1753
                    $item_price = wpinv_get_item_price( $item->ID );
1754
                }
1755
            }
1756
1757
            // Sanitizing the price here so we don't have a dozen calls later
1758
            $item_price = wpinv_sanitize_amount( $item_price );
1759
            $subtotal   = wpinv_round_amount( $item_price * $args['quantity'] );
1760
        
1761
            $discount   = !empty( $args['discount'] ) ? $args['discount'] : 0;
1762
            $tax_class  = !empty( $args['vat_class'] ) ? $args['vat_class'] : '';
1763
            $tax_rate   = !empty( $args['vat_rate'] ) ? $args['vat_rate'] : 0;
1764
            $tax        = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1765
1766
            // Setup the items meta item
1767
            $new_item = array(
1768
                'id'       => $item->ID,
1769
                'quantity' => $args['quantity'],
1770
            );
1771
1772
            $this->items[]  = $new_item;
1773
1774
            if ( wpinv_prices_include_tax() ) {
1775
                $subtotal -= wpinv_round_amount( $tax );
1776
            }
1777
1778
            $total      = $subtotal - $discount + $tax;
1779
1780
            // Do not allow totals to go negative
1781
            if( $total < 0 ) {
1782
                $total = 0;
1783
            }
1784
        
1785
            $this->cart_details[] = array(
1786
                'name'          => !empty($args['name']) ? $args['name'] : $item->get_name(),
1787
                'id'            => $item->ID,
1788
                'item_price'    => wpinv_round_amount( $item_price ),
1789
                'custom_price'  => ( $args['custom_price'] !== '' ? wpinv_round_amount( $args['custom_price'] ) : '' ),
1790
                'quantity'      => $args['quantity'],
1791
                'discount'      => $discount,
1792
                'subtotal'      => wpinv_round_amount( $subtotal ),
1793
                'tax'           => wpinv_round_amount( $tax ),
1794
                'price'         => wpinv_round_amount( $total ),
1795
                'vat_rate'      => $tax_rate,
1796
                'vat_class'     => $tax_class,
1797
                'meta'          => $args['meta'],
1798
                'fees'          => $args['fees'],
1799
            );
1800
   
1801
            $subtotal = $subtotal - $discount;
1802
        }
1803
        
1804
        $added_item = end( $this->cart_details );
1805
        $added_item['action']  = 'add';
1806
        
1807
        $this->pending['items'][] = $added_item;
1808
        
1809
        $this->increase_subtotal( $subtotal );
1810
        $this->increase_tax( $tax );
1811
1812
        return true;
1813
    }
1814
1815
    public function remove_item( $item_id, $args = array() ) {
1816
1817
        // Set some defaults
1818
        $defaults = array(
1819
            'quantity'      => 1,
1820
            'item_price'    => false,
1821
            'custom_price'  => '',
1822
            'cart_index'    => false,
1823
        );
1824
        $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 758
  2. array($data['discount']) is assigned to $address_fields
    in includes/class-wpinv-ajax.php on line 956
  3. wpinv_update_invoice() is called
    in includes/class-wpinv-ajax.php on line 976
  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 1815

Used in variable context

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