Passed
Pull Request — master (#343)
by Brian
86:37
created

WPInv_Invoice::get_recurring_details()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

}

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
456
        }
457
        $status   = isset( $all_invoice_statuses[$status] ) ? $all_invoice_statuses[$status] : __( $status, 'invoicing' );
458
459
        return apply_filters( 'setup_status_nicename', $status );
460
    }
461
462
    /**
463
     * Set's up the invoice number.
464
     */
465
    private function setup_invoice_number( $data ) {
466
467
        if ( ! empty( $data ) && ! empty( $data->number ) ) {
468
            return $data->number;
469
        }
470
471
        $number = $this->ID;
472
473
        if ( $this->status == 'auto-draft' && wpinv_sequential_number_active( $this->post_type ) ) {
474
            $next_number = wpinv_get_next_invoice_number( $this->post_type );
475
            $number      = $next_number;
476
        }
477
        
478
        return wpinv_format_invoice_number( $number, $this->post_type );
479
480
    }
481
482
    /**
483
     * Invoice's post name.
484
     */
485
    private function setup_post_name( $post = NULL ) {
486
        global $wpdb;
487
        
488
        $post_name = '';
489
490
        if ( !empty( $post ) ) {
491
            if( !empty( $post->post_name ) ) {
492
                $post_name = $post->post_name;
493
            } else if ( !empty( $post->ID ) ) {
494
                $post_name = wpinv_generate_post_name( $post->ID );
495
496
                $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) );
497
            }
498
        }
499
500
        $this->post_name = $post_name;
0 ignored issues
show
Bug Best Practice introduced by
The property post_name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
501
    }
502
503
    /**
504
     * Set's up the cart details.
505
     */
506
    public function setup_cart_details() {
507
        global $wpdb;
508
509
        $table =  $wpdb->prefix . 'getpaid_invoice_items';
510
        $items = $wpdb->get_results(
511
            $wpdb->prepare( "SELECT * FROM $table WHERE `post_id`=%d", $this->ID )
512
        );
513
514
        if ( empty( $items ) ) {
515
            return array();
516
        }
517
518
        $details = array();
519
520
        foreach ( $items as $item ) {
521
            $item = (array) $item;
522
            $details[] = array(
523
                'name'          => $item['item_name'],
524
                'id'            => $item['item_id'],
525
                'item_price'    => $item['item_price'],
526
                'custom_price'  => $item['custom_price'],
527
                'quantity'      => $item['quantity'],
528
                'discount'      => $item['discount'],
529
                'subtotal'      => $item['subtotal'],
530
                'tax'           => $item['tax'],
531
                'price'         => $item['price'],
532
                'vat_rate'      => $item['vat_rate'],
533
                'vat_class'     => $item['vat_class'],
534
                'meta'          => $item['meta'],
535
                'fees'          => $item['fees'],
536
            );
537
        }
538
539
        return map_deep( $details, 'maybe_unserialize' );
540
541
    }
542
543
    /**
544
     * Convert this to an array.
545
     */
546
    public function array_convert() {
547
        return get_object_vars( $this );
548
    }
549
    
550
    private function setup_fees() {
551
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
552
        return $payment_fees;
553
    }
554
555
    private function setup_gateway_title() {
556
        $gateway_title = wpinv_get_gateway_checkout_label( $this->gateway );
557
        return $gateway_title;
558
    }
559
    
560
    /**
561
     * Refreshes payment data.
562
     */
563
    private function refresh_payment_data() {
564
565
        $payment_data = array(
566
            'price'        => $this->total,
567
            'date'         => $this->date,
568
            'user_email'   => $this->email,
569
            'invoice_key'  => $this->key,
570
            'currency'     => $this->currency,
571
            'items'        => $this->items,
572
            'user_info' => array(
573
                'user_id'    => $this->user_id,
574
                'email'      => $this->email,
575
                'first_name' => $this->first_name,
576
                'last_name'  => $this->last_name,
577
                'address'    => $this->address,
578
                'phone'      => $this->phone,
579
                'city'       => $this->city,
580
                'country'    => $this->country,
581
                'state'      => $this->state,
582
                'zip'        => $this->zip,
583
                'company'    => $this->company,
584
                'vat_number' => $this->vat_number,
585
                'discount'   => $this->discounts,
586
            ),
587
            'cart_details' => $this->cart_details,
588
            'status'       => $this->status,
589
            'fees'         => $this->fees,
590
        );
591
592
        $this->payment_meta = array_merge( $this->payment_meta, $payment_data );
593
594
    }
595
596
    private function insert_invoice() {
597
598
        if ( empty( $this->post_type ) ) {
599
            if ( !empty( $this->ID ) && $post_type = get_post_type( $this->ID ) ) {
600
                $this->post_type = $post_type;
601
            } else if ( !empty( $this->parent_invoice ) && $post_type = get_post_type( $this->parent_invoice ) ) {
602
                $this->post_type = $post_type;
603
            } else {
604
                $this->post_type = 'wpi_invoice';
605
            }
606
        }
607
608
        $invoice_number = $this->ID;
609
        if ( $number = $this->number ) {
610
            $invoice_number = $number;
611
        }
612
613
        if ( empty( $this->key ) ) {
614
            $this->key = $this->generate_key();
615
            $this->pending['key'] = $this->key;
616
        }
617
618
        if ( empty( $this->ip ) ) {
619
            $this->ip = wpinv_get_ip();
620
            $this->pending['ip'] = $this->ip;
621
        }
622
623
        $payment_data = array(
624
            'price'        => $this->total,
625
            'date'         => $this->date,
626
            'user_email'   => $this->email,
627
            'invoice_key'  => $this->key,
628
            'currency'     => $this->currency,
629
            'items'        => $this->items,
630
            'user_info' => array(
631
                'user_id'    => $this->user_id,
632
                'email'      => $this->email,
633
                'first_name' => $this->first_name,
634
                'last_name'  => $this->last_name,
635
                'address'    => $this->address,
636
                'phone'      => $this->phone,
637
                'city'       => $this->city,
638
                'country'    => $this->country,
639
                'state'      => $this->state,
640
                'zip'        => $this->zip,
641
                'company'    => $this->company,
642
                'vat_number' => $this->vat_number,
643
                'discount'   => $this->discounts,
644
            ),
645
            'cart_details' => $this->cart_details,
646
            'status'       => $this->status,
647
            'fees'         => $this->fees,
648
        );
649
650
        $post_data = array(
651
            'post_title'    => $invoice_number,
652
            'post_status'   => $this->status,
653
            'post_author'   => $this->user_id,
654
            'post_type'     => $this->post_type,
655
            'post_date'     => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? $this->date : current_time( 'mysql' ),
656
            'post_date_gmt' => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? get_gmt_from_date( $this->date ) : current_time( 'mysql', 1 ),
657
            'post_parent'   => $this->parent_invoice,
658
        );
659
        $args = apply_filters( 'wpinv_insert_invoice_args', $post_data, $this );
660
661
        // Create a blank invoice
662
        if ( !empty( $this->ID ) ) {
663
            $args['ID']         = $this->ID;
664
            $invoice_id = wp_update_post( $args, true );
665
        } else {
666
            $invoice_id = wp_insert_post( $args, true );
667
        }
668
669
        if ( is_wp_error( $invoice_id ) ) {
670
            return false;
671
        }
672
673
        if ( ! empty( $invoice_id ) ) {
674
            $this->ID  = $invoice_id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $invoice_id can also be of type WP_Error. However, the property $ID is declared as type integer. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
675
            $this->_ID = $invoice_id;
0 ignored issues
show
Bug Best Practice introduced by
The property _ID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
676
677
            $this->payment_meta = array_merge( $this->payment_meta, $payment_data );
678
679
            if ( ! empty( $this->payment_meta['fees'] ) ) {
680
                $this->fees = array_merge( $this->fees, $this->payment_meta['fees'] );
681
                foreach( $this->fees as $fee ) {
682
                    $this->increase_fees( $fee['amount'] );
683
                }
684
            }
685
686
            $this->pending['payment_meta'] = $this->payment_meta;
687
            $this->save();
688
        }
689
690
        return $this->ID;
691
    }
692
693
    /**
694
     * Saves special fields in our custom table.
695
     */
696
    public function get_special_fields() {
697
698
        return array (
699
            'post_id'        => $this->ID,
700
            'number'         => $this->get_number(),
701
            'key'            => $this->get_key(),
702
            'type'           => str_replace( 'wpi_', '', $this->post_type ),
703
            'mode'           => $this->mode,
704
            'user_ip'        => $this->get_ip(),
705
            'first_name'     => $this->get_first_name(),
706
            'last_name'      => $this->get_last_name(),
707
            'address'        => $this->get_address(),
708
            'city'           => $this->city,
709
            'state'          => $this->state,
710
            'country'        => $this->country,
711
            'zip'            => $this->zip,
712
            'adddress_confirmed' => (int) $this->adddress_confirmed,
713
            'gateway'        => $this->get_gateway(),
714
            'transaction_id' => $this->get_transaction_id(),
715
            'currency'       => $this->get_currency(),
716
            'subtotal'       => $this->get_subtotal(),
717
            'tax'            => $this->get_tax(),
718
            'fees_total'     => $this->get_fees_total(),
719
            'total'          => $this->get_total(),
720
            'discount'       => $this->get_discount(),
721
            'discount_code'  => $this->get_discount_code(),
722
            'disable_taxes'  => $this->disable_taxes,
723
            'due_date'       => $this->get_due_date(),
724
            'completed_date' => $this->get_completed_date(),
725
            'company'        => $this->company,
726
            'vat_number'     => $this->vat_number,
727
            'vat_rate'       => $this->vat_rate,
728
            'custom_meta'    => $this->payment_meta
729
        );
730
731
    }
732
733
    /**
734
     * Saves special fields in our custom table.
735
     */
736
    public function save_special() {
737
        global $wpdb;
738
739
        $this->refresh_payment_data();
740
741
        $fields = $this->get_special_fields();
742
        $fields = array_map( 'maybe_serialize', $fields );
743
744
        $table =  $wpdb->prefix . 'getpaid_invoices';
745
746
        $id = (int) $this->ID;
747
748
        if ( empty( $id ) ) {
749
            return;
750
        }
751
752
        if ( $wpdb->get_var( "SELECT `post_id` FROM $table WHERE `post_id`=$id" ) ) {
753
754
            $wpdb->update( $table, $fields, array( 'post_id' => $id ) );
755
756
        } else {
757
758
            $wpdb->insert( $table, $fields );
759
760
        }
761
762
        $table =  $wpdb->prefix . 'getpaid_invoice_items';
763
        $wpdb->delete( $table, array( 'post_id' => $this->ID ) );
764
765
        foreach ( $this->get_cart_details() as $details ) {
766
            $fields = array(
767
                'post_id'          => $this->ID,
768
                'item_id'          => $details['id'],
769
                'item_name'        => $details['name'],
770
                'item_description' => empty( $details['meta']['description'] ) ? '' : $details['meta']['description'],
771
                'vat_rate'         => $details['vat_rate'],
772
                'vat_class'        => empty( $details['vat_class'] ) ? '_standard' : $details['vat_class'],
773
                'tax'              => $details['tax'],
774
                'item_price'       => $details['item_price'],
775
                'custom_price'     => $details['custom_price'],
776
                'quantity'         => $details['quantity'],
777
                'discount'         => $details['discount'],
778
                'subtotal'         => $details['subtotal'],
779
                'price'            => $details['price'],
780
                'meta'             => $details['meta'],
781
                'fees'             => $details['fees'],
782
            );
783
784
            $item_columns = array_keys ( $fields );
785
786
            foreach ( $fields as $key => $val ) {
787
                if ( is_null( $val ) ) {
788
                    $val = '';
789
                }
790
                $val = maybe_serialize( $val );
791
                $fields[ $key ] = $wpdb->prepare( '%s', $val );
792
            }
793
794
            $fields = implode( ', ', $fields );
795
            $item_rows[] = "($fields)";
796
        }
797
798
        $item_rows    = implode( ', ', $item_rows );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $item_rows seems to be defined by a foreach iteration on line 765. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
799
        $item_columns = implode( ', ', $item_columns );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $item_columns seems to be defined by a foreach iteration on line 765. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
800
        $wpdb->query( "INSERT INTO $table ($item_columns) VALUES $item_rows" );
801
    }
802
803
    public function save( $setup = false ) {
804
        global $wpi_session;
805
        
806
        $saved = false;
807
        if ( empty( $this->items ) ) {
808
            return $saved;
809
        }
810
811
        if ( empty( $this->key ) ) {
812
            $this->key = $this->generate_key();
813
        }
814
815
        if ( empty( $this->ID ) ) {
816
            $invoice_id = $this->insert_invoice();
817
818
            if ( false === $invoice_id ) {
819
                $saved = false;
820
            } else {
821
                $this->ID = $invoice_id;
822
            }
823
        }
824
825
        // If we have something pending, let's save it
826
        if ( ! empty( $this->pending ) ) {
827
            $total_increase = 0;
828
            $total_decrease = 0;
829
830
            foreach ( $this->pending as $key => $value ) {
831
832
                switch( $key ) {
833
                    case 'items':
834
                        // Update totals for pending items
835
                        foreach ( $this->pending[ $key ] as $item ) {
836
                            switch( $item['action'] ) {
837
                                case 'add':
838
                                    $price = $item['price'];
839
                                    $taxes = $item['tax'];
0 ignored issues
show
Unused Code introduced by
The assignment to $taxes is dead and can be removed.
Loading history...
840
841
                                    if ( 'publish' === $this->status ) {
842
                                        $total_increase += $price;
843
                                    }
844
                                    break;
845
846
                                case 'remove':
847
                                    if ( 'publish' === $this->status ) {
848
                                        $total_decrease += $item['price'];
849
                                    }
850
                                    break;
851
                            }
852
                        }
853
                        break;
854
                    case 'fees':
855
                        if ( 'publish' !== $this->status ) {
856
                            break;
857
                        }
858
859
                        if ( empty( $this->pending[ $key ] ) ) {
860
                            break;
861
                        }
862
863
                        foreach ( $this->pending[ $key ] as $fee ) {
864
                            switch( $fee['action'] ) {
865
                                case 'add':
866
                                    $total_increase += $fee['amount'];
867
                                    break;
868
869
                                case 'remove':
870
                                    $total_decrease += $fee['amount'];
871
                                    break;
872
                            }
873
                        }
874
                        break;
875
                    case 'status':
876
                        $this->update_status( $this->status );
877
                        break;
878
                    case 'first_name':
879
                        $this->user_info['first_name'] = $this->first_name;
880
                        break;
881
                    case 'last_name':
882
                        $this->user_info['last_name'] = $this->last_name;
883
                        break;
884
                    case 'phone':
885
                        $this->user_info['phone'] = $this->phone;
886
                        break;
887
                    case 'address':
888
                        $this->user_info['address'] = $this->address;
889
                        break;
890
                    case 'city':
891
                        $this->user_info['city'] = $this->city;
892
                        break;
893
                    case 'country':
894
                        $this->user_info['country'] = $this->country;
895
                        break;
896
                    case 'state':
897
                        $this->user_info['state'] = $this->state;
898
                        break;
899
                    case 'zip':
900
                        $this->user_info['zip'] = $this->zip;
901
                        break;
902
                    case 'company':
903
                        $this->user_info['company'] = $this->company;
904
                        break;
905
                    case 'vat_number':
906
                        $this->user_info['vat_number'] = $this->vat_number;
907
                        
908
                        $vat_info = $wpi_session->get( 'user_vat_data' );
909
                        if ( $this->vat_number && !empty( $vat_info ) && isset( $vat_info['number'] ) && isset( $vat_info['valid'] ) && $vat_info['number'] == $this->vat_number ) {
910
                            $adddress_confirmed = isset( $vat_info['adddress_confirmed'] ) ? $vat_info['adddress_confirmed'] : false;
911
                            $this->update_meta( '_wpinv_adddress_confirmed', (bool)$adddress_confirmed );
912
                            $this->user_info['adddress_confirmed'] = (bool)$adddress_confirmed;
913
                            $this->adddress_confirmed = (bool)$adddress_confirmed;
0 ignored issues
show
Documentation Bug introduced by
The property $adddress_confirmed was declared of type integer, but (bool)$adddress_confirmed is of type boolean. Maybe add a type cast?

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

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

$answer = 42;

$correct = false;

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

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

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

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

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

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

1604
        return date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $due_date ) );
Loading history...
1605
    }
1606
    
1607
    public function get_completed_date() {
1608
        return apply_filters( 'wpinv_completed_date', $this->completed_date, $this->ID, $this );
1609
    }
1610
    
1611
    public function get_invoice_date( $formatted = true ) {
1612
        $date_completed = $this->completed_date;
1613
        $invoice_date   = $date_completed != '' && $date_completed != '0000-00-00 00:00:00' ? $date_completed : '';
1614
        
1615
        if ( $invoice_date == '' ) {
1616
            $date_created   = $this->date;
1617
            $invoice_date   = $date_created != '' && $date_created != '0000-00-00 00:00:00' ? $date_created : '';
1618
        }
1619
        
1620
        if ( $formatted && $invoice_date ) {
1621
            $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

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

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