Passed
Push — master ( 25dd78...14f7c3 )
by Brian
15:22 queued 10:50
created

WPInv_Invoice::get_transaction_id()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

}

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

803
            /** @scrutinizer ignore-call */ 
804
            $this->key = self::generate_key();
Loading history...
804
        }
805
806
        if ( empty( $this->ID ) ) {
807
            $invoice_id = $this->insert_invoice();
808
809
            if ( false === $invoice_id ) {
810
                $saved = false;
811
            } else {
812
                $this->ID = $invoice_id;
813
            }
814
        }
815
816
        // If we have something pending, let's save it
817
        if ( ! empty( $this->pending ) ) {
818
            $total_increase = 0;
819
            $total_decrease = 0;
820
821
            foreach ( $this->pending as $key => $value ) {
822
823
                switch( $key ) {
824
                    case 'items':
825
                        // Update totals for pending items
826
                        foreach ( $this->pending[ $key ] as $item ) {
827
                            switch( $item['action'] ) {
828
                                case 'add':
829
                                    $price = $item['price'];
830
                                    $taxes = $item['tax'];
0 ignored issues
show
Unused Code introduced by
The assignment to $taxes is dead and can be removed.
Loading history...
831
832
                                    if ( 'publish' === $this->status ) {
833
                                        $total_increase += $price;
834
                                    }
835
                                    break;
836
837
                                case 'remove':
838
                                    if ( 'publish' === $this->status ) {
839
                                        $total_decrease += $item['price'];
840
                                    }
841
                                    break;
842
                            }
843
                        }
844
                        break;
845
                    case 'fees':
846
                        if ( 'publish' !== $this->status ) {
847
                            break;
848
                        }
849
850
                        if ( empty( $this->pending[ $key ] ) ) {
851
                            break;
852
                        }
853
854
                        foreach ( $this->pending[ $key ] as $fee ) {
855
                            switch( $fee['action'] ) {
856
                                case 'add':
857
                                    $total_increase += $fee['amount'];
858
                                    break;
859
860
                                case 'remove':
861
                                    $total_decrease += $fee['amount'];
862
                                    break;
863
                            }
864
                        }
865
                        break;
866
                    case 'status':
867
                        $this->update_status( $this->status );
868
                        break;
869
                    case 'first_name':
870
                        $this->user_info['first_name'] = $this->first_name;
871
                        break;
872
                    case 'last_name':
873
                        $this->user_info['last_name'] = $this->last_name;
874
                        break;
875
                    case 'phone':
876
                        $this->user_info['phone'] = $this->phone;
877
                        break;
878
                    case 'address':
879
                        $this->user_info['address'] = $this->address;
880
                        break;
881
                    case 'city':
882
                        $this->user_info['city'] = $this->city;
883
                        break;
884
                    case 'country':
885
                        $this->user_info['country'] = $this->country;
886
                        break;
887
                    case 'state':
888
                        $this->user_info['state'] = $this->state;
889
                        break;
890
                    case 'zip':
891
                        $this->user_info['zip'] = $this->zip;
892
                        break;
893
                    case 'company':
894
                        $this->user_info['company'] = $this->company;
895
                        break;
896
                    case 'vat_number':
897
                        $this->user_info['vat_number'] = $this->vat_number;
898
                        
899
                        $vat_info = $wpi_session->get( 'user_vat_data' );
900
                        if ( $this->vat_number && !empty( $vat_info ) && isset( $vat_info['number'] ) && isset( $vat_info['valid'] ) && $vat_info['number'] == $this->vat_number ) {
901
                            $adddress_confirmed = isset( $vat_info['adddress_confirmed'] ) ? $vat_info['adddress_confirmed'] : false;
902
                            $this->update_meta( '_wpinv_adddress_confirmed', (bool)$adddress_confirmed );
903
                            $this->user_info['adddress_confirmed'] = (bool)$adddress_confirmed;
904
                            $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...
905
                        }
906
    
907
                        break;
908
                    case 'vat_rate':
909
                        $this->user_info['vat_rate'] = $this->vat_rate;
910
                        break;
911
                    case 'adddress_confirmed':
912
                        $this->user_info['adddress_confirmed'] = $this->adddress_confirmed;
913
                        break;
914
                    case 'date':
915
                        $args = array(
916
                            'ID'        => $this->ID,
917
                            'post_date' => $this->date,
918
                            'edit_date' => true,
919
                        );
920
921
                        wp_update_post( $args );
922
                        break;
923
                    case 'due_date':
924
                        if ( empty( $this->due_date ) ) {
925
                            $this->due_date = 'none';
926
                        }
927
                        break;
928
                    case 'discounts':
929
                        if ( ! is_array( $this->discounts ) ) {
930
                            $this->discounts = explode( ',', $this->discounts );
931
                        }
932
933
                        $this->user_info['discount'] = implode( ',', $this->discounts );
934
                        break;
935
                    case 'parent_invoice':
936
                        $args = array(
937
                            'ID'          => $this->ID,
938
                            'post_parent' => $this->parent_invoice,
939
                        );
940
                        wp_update_post( $args );
941
                        break;
942
                    default:
943
                        do_action( 'wpinv_save', $this, $key );
944
                        break;
945
                }
946
            }
947
948
            $this->items    = array_values( $this->items );
949
950
            $this->pending      = array();
951
            $saved              = true;
952
        }
953
954
        $new_meta = array(
955
            'items'         => $this->items,
956
            'cart_details'  => $this->cart_details,
957
            'fees'          => $this->fees,
958
            'currency'      => $this->currency,
959
            'user_info'     => $this->user_info,
960
        );
961
        $this->payment_meta = array_merge( $this->payment_meta, $new_meta );
962
        $this->update_items();
963
964
        $this->save_special();
965
        do_action( 'wpinv_invoice_save', $this, $saved );
966
967
        if ( true === $saved || $setup ) {
968
            $this->setup_invoice( $this->ID );
969
        }
970
971
        $this->refresh_item_ids();
972
973
        return $saved;
974
    }
975
    
976
    public function add_fee( $args, $global = true ) {
977
        $default_args = array(
978
            'label'       => '',
979
            'amount'      => 0,
980
            'type'        => 'fee',
981
            'id'          => '',
982
            'no_tax'      => false,
983
            'item_id'     => 0,
984
        );
985
986
        $fee = wp_parse_args( $args, $default_args );
987
        
988
        if ( empty( $fee['label'] ) ) {
989
            return false;
990
        }
991
        
992
        $fee['id']  = sanitize_title( $fee['label'] );
993
        
994
        $this->fees[]               = $fee;
995
        
996
        $added_fee               = $fee;
997
        $added_fee['action']     = 'add';
998
        $this->pending['fees'][] = $added_fee;
999
        reset( $this->fees );
1000
1001
        $this->increase_fees( $fee['amount'] );
1002
        return true;
1003
    }
1004
1005
    public function remove_fee( $key ) {
1006
        $removed = false;
1007
1008
        if ( is_numeric( $key ) ) {
1009
            $removed = $this->remove_fee_by( 'index', $key );
1010
        }
1011
1012
        return $removed;
1013
    }
1014
1015
    public function remove_fee_by( $key, $value, $global = false ) {
1016
        $allowed_fee_keys = apply_filters( 'wpinv_fee_keys', array(
1017
            'index', 'label', 'amount', 'type',
1018
        ) );
1019
1020
        if ( ! in_array( $key, $allowed_fee_keys ) ) {
1021
            return false;
1022
        }
1023
1024
        $removed = false;
1025
        if ( 'index' === $key && array_key_exists( $value, $this->fees ) ) {
1026
            $removed_fee             = $this->fees[ $value ];
1027
            $removed_fee['action']   = 'remove';
1028
            $this->pending['fees'][] = $removed_fee;
1029
1030
            $this->decrease_fees( $removed_fee['amount'] );
1031
1032
            unset( $this->fees[ $value ] );
1033
            $removed = true;
1034
        } else if ( 'index' !== $key ) {
1035
            foreach ( $this->fees as $index => $fee ) {
1036
                if ( isset( $fee[ $key ] ) && $fee[ $key ] == $value ) {
1037
                    $removed_fee             = $fee;
1038
                    $removed_fee['action']   = 'remove';
1039
                    $this->pending['fees'][] = $removed_fee;
1040
1041
                    $this->decrease_fees( $removed_fee['amount'] );
1042
1043
                    unset( $this->fees[ $index ] );
1044
                    $removed = true;
1045
1046
                    if ( false === $global ) {
1047
                        break;
1048
                    }
1049
                }
1050
            }
1051
        }
1052
1053
        if ( true === $removed ) {
1054
            $this->fees = array_values( $this->fees );
1055
        }
1056
1057
        return $removed;
1058
    }
1059
1060
    
1061
1062
    public function add_note( $note = '', $customer_type = false, $added_by_user = false, $system = false ) {
1063
        // Bail if no note specified
1064
        if( !$note ) {
1065
            return false;
1066
        }
1067
1068
        if ( empty( $this->ID ) )
1069
            return false;
1070
        
1071
        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...
1072
            $user                 = get_user_by( 'id', get_current_user_id() );
1073
            $comment_author       = $user->display_name;
1074
            $comment_author_email = $user->user_email;
1075
        } else {
1076
            $comment_author       = 'System';
1077
            $comment_author_email = 'system@';
1078
            $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com';
1079
            $comment_author_email = sanitize_email( $comment_author_email );
1080
        }
1081
1082
        do_action( 'wpinv_pre_insert_invoice_note', $this->ID, $note, $customer_type );
1083
1084
        $note_id = wp_insert_comment( wp_filter_comment( array(
1085
            'comment_post_ID'      => $this->ID,
1086
            'comment_content'      => $note,
1087
            'comment_agent'        => 'WPInvoicing',
1088
            'user_id'              => is_admin() ? get_current_user_id() : 0,
1089
            'comment_date'         => current_time( 'mysql' ),
1090
            'comment_date_gmt'     => current_time( 'mysql', 1 ),
1091
            'comment_approved'     => 1,
1092
            'comment_parent'       => 0,
1093
            'comment_author'       => $comment_author,
1094
            'comment_author_IP'    => wpinv_get_ip(),
1095
            'comment_author_url'   => '',
1096
            'comment_author_email' => $comment_author_email,
1097
            'comment_type'         => 'wpinv_note'
1098
        ) ) );
1099
1100
        do_action( 'wpinv_insert_payment_note', $note_id, $this->ID, $note );
1101
        
1102
        if ( $customer_type ) {
1103
            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

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

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

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

1641
        return date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $due_date ) );
Loading history...
1642
    }
1643
    
1644
    public function get_completed_date() {
1645
        return apply_filters( 'wpinv_completed_date', $this->completed_date, $this->ID, $this );
1646
    }
1647
    
1648
    public function get_invoice_date( $formatted = true ) {
1649
        $date_completed = $this->completed_date;
1650
        $invoice_date   = $date_completed != '' && $date_completed != '0000-00-00 00:00:00' ? $date_completed : '';
1651
        
1652
        if ( $invoice_date == '' ) {
1653
            $date_created   = $this->date;
1654
            $invoice_date   = $date_created != '' && $date_created != '0000-00-00 00:00:00' ? $date_created : '';
1655
        }
1656
        
1657
        if ( $formatted && $invoice_date ) {
1658
            $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

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

1 path for user data to reach this point

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

Used in variable context

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

General Strategies to prevent injection

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

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

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

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