Passed
Pull Request — master (#270)
by Brian
08:38
created

WPInv_Invoice::has_non_recurring_discount()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 9
nc 5
nop 0
dl 0
loc 20
rs 9.2222
c 0
b 0
f 0
1
<?php
2
/**
3
 * Contains functions related to Invoicing plugin.
4
 *
5
 * @since 1.0.0
6
 * @package Invoicing
7
 */
8
 
9
// MUST have WordPress.
10
if ( !defined( 'WPINC' ) ) {
11
    exit( 'Do NOT access this file directly: ' . basename( __FILE__ ) );
12
}
13
14
final class WPInv_Invoice {
15
    public $ID  = 0;
16
    public $title;
17
    public $post_type;
18
    
19
    public $pending;
20
    public $items = array();
21
    public $user_info = array();
22
    public $payment_meta = array();
23
    
24
    public $new = false;
25
    public $number = '';
26
    public $mode = 'live';
27
    public $key = '';
28
    public $total = 0.00;
29
    public $subtotal = 0;
30
    public $tax = 0;
31
    public $fees = array();
32
    public $fees_total = 0;
33
    public $discounts = '';
34
    public $discount = 0;
35
    public $discount_code = 0;
36
    public $date = '';
37
    public $due_date = '';
38
    public $completed_date = '';
39
    public $status      = 'wpi-pending';
40
    public $post_status = 'wpi-pending';
41
    public $old_status = '';
42
    public $status_nicename = '';
43
    public $user_id = 0;
44
    public $first_name = '';
45
    public $last_name = '';
46
    public $email = '';
47
    public $phone = '';
48
    public $address = '';
49
    public $city = '';
50
    public $country = '';
51
    public $state = '';
52
    public $zip = '';
53
    public $transaction_id = '';
54
    public $ip = '';
55
    public $gateway = '';
56
    public $gateway_title = '';
57
    public $currency = '';
58
    public $cart_details = array();
59
    
60
    public $company = '';
61
    public $vat_number = '';
62
    public $vat_rate = '';
63
    public $adddress_confirmed = '';
64
    
65
    public $full_name = '';
66
    public $parent_invoice = 0;
67
    
68
    public function __construct( $invoice_id = false ) {
69
        if( empty( $invoice_id ) ) {
70
            return false;
71
        }
72
73
        $this->setup_invoice( $invoice_id );
74
    }
75
76
    public function get( $key ) {
77
        if ( method_exists( $this, 'get_' . $key ) ) {
78
            $value = call_user_func( array( $this, 'get_' . $key ) );
79
        } else {
80
            $value = $this->$key;
81
        }
82
83
        return $value;
84
    }
85
86
    public function set( $key, $value ) {
87
        $ignore = array( 'items', 'cart_details', 'fees', '_ID' );
88
89
        if ( $key === 'status' ) {
90
            $this->old_status = $this->status;
91
        }
92
93
        if ( ! in_array( $key, $ignore ) ) {
94
            $this->pending[ $key ] = $value;
95
        }
96
97
        if( '_ID' !== $key ) {
98
            $this->$key = $value;
99
        }
100
    }
101
102
    public function _isset( $name ) {
103
        if ( property_exists( $this, $name) ) {
104
            return false === empty( $this->$name );
105
        } else {
106
            return null;
107
        }
108
    }
109
110
    private function setup_invoice( $invoice_id ) {
111
        $this->pending = array();
112
113
        if ( empty( $invoice_id ) ) {
114
            return false;
115
        }
116
117
        $invoice = get_post( $invoice_id );
118
119
        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...
120
            return false;
121
        }
122
123
        if( !('wpi_invoice' == $invoice->post_type OR 'wpi_quote' == $invoice->post_type) ) {
124
            return false;
125
        }
126
127
        do_action( 'wpinv_pre_setup_invoice', $this, $invoice_id );
128
        
129
        // Primary Identifier
130
        $this->ID              = absint( $invoice_id );
131
        $this->post_type       = $invoice->post_type;
132
        
133
        // We have a payment, get the generic payment_meta item to reduce calls to it
134
        $this->payment_meta    = $this->get_meta();
135
        $this->date            = $invoice->post_date;
136
        $this->due_date        = $this->setup_due_date();
137
        $this->completed_date  = $this->setup_completed_date();
138
        $this->status          = $invoice->post_status;
139
        $this->post_status     = $this->status;
140
        $this->mode            = $this->setup_mode();
141
        $this->parent_invoice  = $invoice->post_parent;
142
        $this->post_name       = $this->setup_post_name( $invoice );
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...
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...
143
        $this->status_nicename = $this->setup_status_nicename($invoice->post_status);
144
145
        // Items
146
        $this->fees            = $this->setup_fees();
147
        $this->cart_details    = $this->setup_cart_details();
148
        $this->items           = $this->setup_items();
149
150
        // Currency Based
151
        $this->total           = $this->setup_total();
152
        $this->tax             = $this->setup_tax();
153
        $this->fees_total      = $this->get_fees_total();
154
        $this->subtotal        = $this->setup_subtotal();
155
        $this->currency        = $this->setup_currency();
156
        
157
        // Gateway based
158
        $this->gateway         = $this->setup_gateway();
159
        $this->gateway_title   = $this->setup_gateway_title();
160
        $this->transaction_id  = $this->setup_transaction_id();
161
        
162
        // User based
163
        $this->ip              = $this->setup_ip();
164
        $this->user_id         = !empty( $invoice->post_author ) ? $invoice->post_author : get_current_user_id();///$this->setup_user_id();
165
        $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

165
        $this->email           = get_the_author_meta( 'email', /** @scrutinizer ignore-type */ $this->user_id );
Loading history...
166
        
167
        $this->user_info       = $this->setup_user_info();
168
                
169
        $this->first_name      = $this->user_info['first_name'];
170
        $this->last_name       = $this->user_info['last_name'];
171
        $this->company         = $this->user_info['company'];
172
        $this->vat_number      = $this->user_info['vat_number'];
173
        $this->vat_rate        = $this->user_info['vat_rate'];
174
        $this->adddress_confirmed  = $this->user_info['adddress_confirmed'];
175
        $this->address         = $this->user_info['address'];
176
        $this->city            = $this->user_info['city'];
177
        $this->country         = $this->user_info['country'];
178
        $this->state           = $this->user_info['state'];
179
        $this->zip             = $this->user_info['zip'];
180
        $this->phone           = $this->user_info['phone'];
181
        
182
        $this->discounts       = $this->user_info['discount'];
183
            $this->discount        = $this->setup_discount();
184
            $this->discount_code   = $this->setup_discount_code();
185
186
        // Other Identifiers
187
        $this->key             = $this->setup_invoice_key();
188
        $this->number          = $this->setup_invoice_number();
189
        $this->title           = !empty( $invoice->post_title ) ? $invoice->post_title : $this->number;
190
        
191
        $this->full_name       = trim( $this->first_name . ' '. $this->last_name );
192
        
193
        // Allow extensions to add items to this object via hook
194
        do_action( 'wpinv_setup_invoice', $this, $invoice_id );
195
196
        return true;
197
    }
198
    
199
    private function setup_status_nicename( $status ) {
200
        $all_invoice_statuses  = wpinv_get_invoice_statuses( true, true, $this );
201
202
        if ( $this->is_quote() && class_exists( 'Wpinv_Quotes_Shared' ) ) {
203
            $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...
204
        }
205
        $status   = isset( $all_invoice_statuses[$status] ) ? $all_invoice_statuses[$status] : __( $status, 'invoicing' );
206
207
        return apply_filters( 'setup_status_nicename', $status );
208
    }
209
    
210
    private function setup_post_name( $post = NULL ) {
211
        global $wpdb;
212
        
213
        $post_name = '';
214
        
215
        if ( !empty( $post ) ) {
216
            if( !empty( $post->post_name ) ) {
217
                $post_name = $post->post_name;
218
            } else if ( !empty( $post->ID ) ) {
219
                $post_name = wpinv_generate_post_name( $post->ID );
220
221
                $wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) );
222
            }
223
        }
224
225
        $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...
226
    }
227
    
228
    private function setup_due_date() {
229
        $due_date = $this->get_meta( '_wpinv_due_date' );
230
        
231
        if ( empty( $due_date ) ) {
232
            $overdue_time = strtotime( $this->date ) + ( DAY_IN_SECONDS * absint( wpinv_get_option( 'overdue_days' ) ) );
233
            $due_date = date_i18n( 'Y-m-d', $overdue_time );
234
        } else if ( $due_date == 'none' ) {
235
            $due_date = '';
236
        }
237
        
238
        return $due_date;
239
    }
240
    
241
    private function setup_completed_date() {
242
        $invoice = get_post( $this->ID );
243
244
        if ( 'wpi-pending' == $invoice->post_status || 'preapproved' == $invoice->post_status ) {
245
            return false; // This invoice was never paid
246
        }
247
248
        $date = ( $date = $this->get_meta( '_wpinv_completed_date', true ) ) ? $date : $invoice->modified_date;
249
250
        return $date;
251
    }
252
    
253
    private function setup_cart_details() {
254
        $cart_details = isset( $this->payment_meta['cart_details'] ) ? maybe_unserialize( $this->payment_meta['cart_details'] ) : array();
255
        return $cart_details;
256
    }
257
    
258
    public function array_convert() {
259
        return get_object_vars( $this );
260
    }
261
    
262
    private function setup_items() {
263
        $items = isset( $this->payment_meta['items'] ) ? maybe_unserialize( $this->payment_meta['items'] ) : array();
264
        return $items;
265
    }
266
    
267
    private function setup_fees() {
268
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
269
        return $payment_fees;
270
    }
271
        
272
    private function setup_currency() {
273
        $currency = isset( $this->payment_meta['currency'] ) ? $this->payment_meta['currency'] : apply_filters( 'wpinv_currency_default', wpinv_get_currency(), $this );
274
        return $currency;
275
    }
276
    
277
    private function setup_discount() {
278
        //$discount = $this->get_meta( '_wpinv_discount', true );
279
        $discount = (float)$this->subtotal - ( (float)$this->total - (float)$this->tax - (float)$this->fees_total );
280
        if ( $discount < 0 ) {
281
            $discount = 0;
282
        }
283
        $discount = wpinv_round_amount( $discount );
284
        
285
        return $discount;
286
    }
287
    
288
    private function setup_discount_code() {
289
        $discount_code = !empty( $this->discounts ) ? $this->discounts : $this->get_meta( '_wpinv_discount_code', true );
290
        return $discount_code;
291
    }
292
    
293
    private function setup_tax() {
294
        $tax = $this->get_meta( '_wpinv_tax', true );
295
296
        // We don't have tax as it's own meta and no meta was passed
297
        if ( '' === $tax ) {            
298
            $tax = isset( $this->payment_meta['tax'] ) ? $this->payment_meta['tax'] : 0;
299
        }
300
        
301
        if ( $tax < 0 ) {
302
            $tax = 0;
303
        }
304
305
        return $tax;
306
    }
307
308
    private function setup_subtotal() {
309
        $subtotal     = 0;
310
        $cart_details = $this->cart_details;
311
312
        if ( is_array( $cart_details ) ) {
0 ignored issues
show
introduced by
The condition is_array($cart_details) is always true.
Loading history...
313
            foreach ( $cart_details as $item ) {
314
                if ( isset( $item['subtotal'] ) ) {
315
                    $subtotal += $item['subtotal'];
316
                }
317
            }
318
        } else {
319
            $subtotal  = $this->total;
320
            $tax       = wpinv_use_taxes() ? $this->tax : 0;
321
            $subtotal -= $tax;
322
        }
323
324
        return $subtotal;
325
    }
326
    
327
    private function setup_discounts() {
328
        $discounts = ! empty( $this->payment_meta['user_info']['discount'] ) ? $this->payment_meta['user_info']['discount'] : array();
329
        return $discounts;
330
    }
331
    
332
    private function setup_total() {
333
        $amount = $this->get_meta( '_wpinv_total', true );
334
335
        if ( empty( $amount ) && '0.00' != $amount ) {
336
            $meta   = $this->get_meta( '_wpinv_payment_meta', true );
337
            $meta   = maybe_unserialize( $meta );
0 ignored issues
show
Bug introduced by
It seems like $meta can also be of type array and array; however, parameter $original of maybe_unserialize() 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

337
            $meta   = maybe_unserialize( /** @scrutinizer ignore-type */ $meta );
Loading history...
338
339
            if ( isset( $meta['amount'] ) ) {
340
                $amount = $meta['amount'];
341
            }
342
        }
343
344
        if($amount < 0){
345
            $amount = 0;
346
        }
347
348
        return $amount;
349
    }
350
    
351
    private function setup_mode() {
352
        return $this->get_meta( '_wpinv_mode' );
353
    }
354
355
    private function setup_gateway() {
356
        $gateway = $this->get_meta( '_wpinv_gateway' );
357
        
358
        if ( empty( $gateway ) && 'publish' === $this->status ) {
359
            $gateway = 'manual';
360
        }
361
        
362
        return $gateway;
363
    }
364
    
365
    private function setup_gateway_title() {
366
        $gateway_title = wpinv_get_gateway_checkout_label( $this->gateway );
367
        return $gateway_title;
368
    }
369
370
    private function setup_transaction_id() {
371
        $transaction_id = $this->get_meta( '_wpinv_transaction_id' );
372
373
        if ( empty( $transaction_id ) || (int) $transaction_id === (int) $this->ID ) {
374
            $gateway        = $this->gateway;
375
            $transaction_id = apply_filters( 'wpinv_get_invoice_transaction_id-' . $gateway, $this->ID );
376
        }
377
378
        return $transaction_id;
379
    }
380
381
    private function setup_ip() {
382
        $ip = $this->get_meta( '_wpinv_user_ip' );
383
        return $ip;
384
    }
385
386
    ///private function setup_user_id() {
387
        ///$user_id = $this->get_meta( '_wpinv_user_id' );
388
        ///return $user_id;
389
    ///}
390
        
391
    private function setup_first_name() {
392
        $first_name = $this->get_meta( '_wpinv_first_name' );
393
        return $first_name;
394
    }
395
    
396
    private function setup_last_name() {
397
        $last_name = $this->get_meta( '_wpinv_last_name' );
398
        return $last_name;
399
    }
400
    
401
    private function setup_company() {
402
        $company = $this->get_meta( '_wpinv_company' );
403
        return $company;
404
    }
405
    
406
    private function setup_vat_number() {
407
        $vat_number = $this->get_meta( '_wpinv_vat_number' );
408
        return $vat_number;
409
    }
410
    
411
    private function setup_vat_rate() {
412
        $vat_rate = $this->get_meta( '_wpinv_vat_rate' );
413
        return $vat_rate;
414
    }
415
    
416
    private function setup_adddress_confirmed() {
417
        $adddress_confirmed = $this->get_meta( '_wpinv_adddress_confirmed' );
418
        return $adddress_confirmed;
419
    }
420
    
421
    private function setup_phone() {
422
        $phone = $this->get_meta( '_wpinv_phone' );
423
        return $phone;
424
    }
425
    
426
    private function setup_address() {
427
        $address = $this->get_meta( '_wpinv_address', true );
428
        return $address;
429
    }
430
    
431
    private function setup_city() {
432
        $city = $this->get_meta( '_wpinv_city', true );
433
        return $city;
434
    }
435
    
436
    private function setup_country() {
437
        $country = $this->get_meta( '_wpinv_country', true );
438
        return $country;
439
    }
440
    
441
    private function setup_state() {
442
        $state = $this->get_meta( '_wpinv_state', true );
443
        return $state;
444
    }
445
    
446
    private function setup_zip() {
447
        $zip = $this->get_meta( '_wpinv_zip', true );
448
        return $zip;
449
    }
450
451
    private function setup_user_info() {
452
        $defaults = array(
453
            'user_id'        => $this->user_id,
454
            'first_name'     => $this->first_name,
455
            'last_name'      => $this->last_name,
456
            'email'          => get_the_author_meta( 'email', $this->user_id ),
457
            'phone'          => $this->phone,
458
            'address'        => $this->address,
459
            'city'           => $this->city,
460
            'country'        => $this->country,
461
            'state'          => $this->state,
462
            'zip'            => $this->zip,
463
            'company'        => $this->company,
464
            'vat_number'     => $this->vat_number,
465
            'vat_rate'       => $this->vat_rate,
466
            'adddress_confirmed' => $this->adddress_confirmed,
467
            'discount'       => $this->discounts,
468
        );
469
        
470
        $user_info = array();
471
        if ( isset( $this->payment_meta['user_info'] ) ) {
472
            $user_info = maybe_unserialize( $this->payment_meta['user_info'] );
473
            
474
            if ( !empty( $user_info ) && isset( $user_info['user_id'] ) && $post = get_post( $this->ID ) ) {
475
                $this->user_id = $post->post_author;
476
                $this->email = get_the_author_meta( 'email', $this->user_id );
0 ignored issues
show
Bug introduced by
$this->user_id of type string is incompatible with the type false|integer expected by parameter $user_id of get_the_author_meta(). ( Ignorable by Annotation )

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

476
                $this->email = get_the_author_meta( 'email', /** @scrutinizer ignore-type */ $this->user_id );
Loading history...
477
                
478
                $user_info['user_id'] = $this->user_id;
479
                $user_info['email'] = $this->email;
480
                $this->payment_meta['user_id'] = $this->user_id;
481
                $this->payment_meta['email'] = $this->email;
482
            }
483
        }
484
        
485
        $user_info    = wp_parse_args( $user_info, $defaults );
486
        
487
        // Get the user, but only if it's been created
488
        $user = get_userdata( $this->user_id );
489
        
490
        if ( !empty( $user ) && $user->ID > 0 ) {
491
            if ( empty( $user_info ) ) {
492
                $user_info = array(
493
                    'user_id'    => $user->ID,
494
                    'first_name' => $user->first_name,
495
                    'last_name'  => $user->last_name,
496
                    'email'      => $user->user_email,
497
                    'discount'   => '',
498
                );
499
            } else {
500
                foreach ( $user_info as $key => $value ) {
501
                    if ( ! empty( $value ) ) {
502
                        continue;
503
                    }
504
505
                    switch( $key ) {
506
                        case 'user_id':
507
                            $user_info[ $key ] = $user->ID;
508
                            break;
509
                        case 'first_name':
510
                            $user_info[ $key ] = $user->first_name;
511
                            break;
512
                        case 'last_name':
513
                            $user_info[ $key ] = $user->last_name;
514
                            break;
515
                        case 'email':
516
                            $user_info[ $key ] = $user->user_email;
517
                            break;
518
                    }
519
                }
520
            }
521
        }
522
523
        return $user_info;
524
    }
525
526
    private function setup_invoice_key() {
527
        $key = $this->get_meta( '_wpinv_key', true );
528
        
529
        return $key;
530
    }
531
532
    private function setup_invoice_number() {
533
        $number = $this->get_meta( '_wpinv_number', true );
534
535
        if ( !$number ) {
536
            $number = $this->ID;
537
538
            if ( $this->status == 'auto-draft' ) {
539
                if ( wpinv_sequential_number_active( $this->post_type ) ) {
540
                    $next_number = wpinv_get_next_invoice_number( $this->post_type );
541
                    $number      = $next_number;
542
                }
543
            }
544
            
545
            $number = wpinv_format_invoice_number( $number, $this->post_type );
546
        }
547
548
        return $number;
549
    }
550
    
551
    private function insert_invoice() {
552
        global $wpdb;
553
554
        if ( empty( $this->post_type ) ) {
555
            if ( !empty( $this->ID ) && $post_type = get_post_type( $this->ID ) ) {
556
                $this->post_type = $post_type;
557
            } else if ( !empty( $this->parent_invoice ) && $post_type = get_post_type( $this->parent_invoice ) ) {
558
                $this->post_type = $post_type;
559
            } else {
560
                $this->post_type = 'wpi_invoice';
561
            }
562
        }
563
564
        $invoice_number = $this->ID;
565
        if ( $number = $this->get_meta( '_wpinv_number', true ) ) {
566
            $invoice_number = $number;
567
        }
568
569
        if ( empty( $this->key ) ) {
570
            $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

570
            /** @scrutinizer ignore-call */ 
571
            $this->key = self::generate_key();
Loading history...
571
            $this->pending['key'] = $this->key;
572
        }
573
574
        if ( empty( $this->ip ) ) {
575
            $this->ip = wpinv_get_ip();
576
            $this->pending['ip'] = $this->ip;
577
        }
578
        
579
        $payment_data = array(
580
            'price'        => $this->total,
581
            'date'         => $this->date,
582
            'user_email'   => $this->email,
583
            'invoice_key'  => $this->key,
584
            'currency'     => $this->currency,
585
            'items'        => $this->items,
586
            'user_info' => array(
587
                'user_id'    => $this->user_id,
588
                'email'      => $this->email,
589
                'first_name' => $this->first_name,
590
                'last_name'  => $this->last_name,
591
                'address'    => $this->address,
592
                'phone'      => $this->phone,
593
                'city'       => $this->city,
594
                'country'    => $this->country,
595
                'state'      => $this->state,
596
                'zip'        => $this->zip,
597
                'company'    => $this->company,
598
                'vat_number' => $this->vat_number,
599
                'discount'   => $this->discounts,
600
            ),
601
            'cart_details' => $this->cart_details,
602
            'status'       => $this->status,
603
            'fees'         => $this->fees,
604
        );
605
606
        $post_data = array(
607
                        'post_title'    => $invoice_number,
608
                        'post_status'   => $this->status,
609
                        'post_author'   => $this->user_id,
610
                        'post_type'     => $this->post_type,
611
                        'post_date'     => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? $this->date : current_time( 'mysql' ),
612
                        'post_date_gmt' => ! empty( $this->date ) && $this->date != '0000-00-00 00:00:00' ? get_gmt_from_date( $this->date ) : current_time( 'mysql', 1 ),
613
                        'post_parent'   => $this->parent_invoice,
614
                    );
615
        $args = apply_filters( 'wpinv_insert_invoice_args', $post_data, $this );
616
617
        // Create a blank invoice
618
        if ( !empty( $this->ID ) ) {
619
            $args['ID']         = $this->ID;
620
621
            $invoice_id = wp_update_post( $args, true );
622
        } else {
623
            $invoice_id = wp_insert_post( $args, true );
624
        }
625
626
        if ( is_wp_error( $invoice_id ) ) {
627
            return false;
628
        }
629
630
        if ( !empty( $invoice_id ) ) {
631
            $this->ID  = $invoice_id;
632
            $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...
633
634
            $this->payment_meta = apply_filters( 'wpinv_payment_meta', $this->payment_meta, $payment_data );
635
            if ( ! empty( $this->payment_meta['fees'] ) ) {
636
                $this->fees = array_merge( $this->fees, $this->payment_meta['fees'] );
637
                foreach( $this->fees as $fee ) {
638
                    $this->increase_fees( $fee['amount'] );
639
                }
640
            }
641
642
            $this->update_meta( '_wpinv_payment_meta', $this->payment_meta );            
643
            $this->new = true;
644
        }
645
646
        return $this->ID;
647
    }
648
649
    public function save( $setup = false ) {
650
        global $wpi_session;
651
        
652
        $saved = false;
653
        if ( empty( $this->items ) ) {
654
            return $saved; // Don't save empty invoice.
655
        }
656
        
657
        if ( empty( $this->key ) ) {
658
            $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

658
            /** @scrutinizer ignore-call */ 
659
            $this->key = self::generate_key();
Loading history...
659
            $this->pending['key'] = $this->key;
660
        }
661
        
662
        if ( empty( $this->ID ) ) {
663
            $invoice_id = $this->insert_invoice();
664
665
            if ( false === $invoice_id ) {
666
                $saved = false;
667
            } else {
668
                $this->ID = $invoice_id;
669
            }
670
        }
671
672
        // If we have something pending, let's save it
673
        if ( !empty( $this->pending ) ) {
674
            $total_increase = 0;
675
            $total_decrease = 0;
676
677
            foreach ( $this->pending as $key => $value ) {
678
                switch( $key ) {
679
                    case 'items':
680
                        // Update totals for pending items
681
                        foreach ( $this->pending[ $key ] as $item ) {
682
                            switch( $item['action'] ) {
683
                                case 'add':
684
                                    $price = $item['price'];
685
                                    $taxes = $item['tax'];
0 ignored issues
show
Unused Code introduced by
The assignment to $taxes is dead and can be removed.
Loading history...
686
687
                                    if ( 'publish' === $this->status ) {
688
                                        $total_increase += $price;
689
                                    }
690
                                    break;
691
692
                                case 'remove':
693
                                    if ( 'publish' === $this->status ) {
694
                                        $total_decrease += $item['price'];
695
                                    }
696
                                    break;
697
                            }
698
                        }
699
                        break;
700
                    case 'fees':
701
                        if ( 'publish' !== $this->status ) {
702
                            break;
703
                        }
704
705
                        if ( empty( $this->pending[ $key ] ) ) {
706
                            break;
707
                        }
708
709
                        foreach ( $this->pending[ $key ] as $fee ) {
710
                            switch( $fee['action'] ) {
711
                                case 'add':
712
                                    $total_increase += $fee['amount'];
713
                                    break;
714
715
                                case 'remove':
716
                                    $total_decrease += $fee['amount'];
717
                                    break;
718
                            }
719
                        }
720
                        break;
721
                    case 'status':
722
                        $this->update_status( $this->status );
723
                        break;
724
                    case 'gateway':
725
                        $this->update_meta( '_wpinv_gateway', $this->gateway );
726
                        break;
727
                    case 'mode':
728
                        $this->update_meta( '_wpinv_mode', $this->mode );
729
                        break;
730
                    case 'transaction_id':
731
                        $this->update_meta( '_wpinv_transaction_id', $this->transaction_id );
732
                        break;
733
                    case 'ip':
734
                        $this->update_meta( '_wpinv_user_ip', $this->ip );
735
                        break;
736
                    ///case 'user_id':
737
                        ///$this->update_meta( '_wpinv_user_id', $this->user_id );
738
                        ///$this->user_info['user_id'] = $this->user_id;
739
                        ///break;
740
                    case 'first_name':
741
                        $this->update_meta( '_wpinv_first_name', $this->first_name );
742
                        $this->user_info['first_name'] = $this->first_name;
743
                        break;
744
                    case 'last_name':
745
                        $this->update_meta( '_wpinv_last_name', $this->last_name );
746
                        $this->user_info['last_name'] = $this->last_name;
747
                        break;
748
                    case 'phone':
749
                        $this->update_meta( '_wpinv_phone', $this->phone );
750
                        $this->user_info['phone'] = $this->phone;
751
                        break;
752
                    case 'address':
753
                        $this->update_meta( '_wpinv_address', $this->address );
754
                        $this->user_info['address'] = $this->address;
755
                        break;
756
                    case 'city':
757
                        $this->update_meta( '_wpinv_city', $this->city );
758
                        $this->user_info['city'] = $this->city;
759
                        break;
760
                    case 'country':
761
                        $this->update_meta( '_wpinv_country', $this->country );
762
                        $this->user_info['country'] = $this->country;
763
                        break;
764
                    case 'state':
765
                        $this->update_meta( '_wpinv_state', $this->state );
766
                        $this->user_info['state'] = $this->state;
767
                        break;
768
                    case 'zip':
769
                        $this->update_meta( '_wpinv_zip', $this->zip );
770
                        $this->user_info['zip'] = $this->zip;
771
                        break;
772
                    case 'company':
773
                        $this->update_meta( '_wpinv_company', $this->company );
774
                        $this->user_info['company'] = $this->company;
775
                        break;
776
                    case 'vat_number':
777
                        $this->update_meta( '_wpinv_vat_number', $this->vat_number );
778
                        $this->user_info['vat_number'] = $this->vat_number;
779
                        
780
                        $vat_info = $wpi_session->get( 'user_vat_data' );
781
                        if ( $this->vat_number && !empty( $vat_info ) && isset( $vat_info['number'] ) && isset( $vat_info['valid'] ) && $vat_info['number'] == $this->vat_number ) {
782
                            $adddress_confirmed = isset( $vat_info['adddress_confirmed'] ) ? $vat_info['adddress_confirmed'] : false;
783
                            $this->update_meta( '_wpinv_adddress_confirmed', (bool)$adddress_confirmed );
784
                            $this->user_info['adddress_confirmed'] = (bool)$adddress_confirmed;
785
                        }
786
    
787
                        break;
788
                    case 'vat_rate':
789
                        $this->update_meta( '_wpinv_vat_rate', $this->vat_rate );
790
                        $this->user_info['vat_rate'] = $this->vat_rate;
791
                        break;
792
                    case 'adddress_confirmed':
793
                        $this->update_meta( '_wpinv_adddress_confirmed', $this->adddress_confirmed );
794
                        $this->user_info['adddress_confirmed'] = $this->adddress_confirmed;
795
                        break;
796
                    
797
                    case 'key':
798
                        $this->update_meta( '_wpinv_key', $this->key );
799
                        break;
800
                    case 'date':
801
                        $args = array(
802
                            'ID'        => $this->ID,
803
                            'post_date' => $this->date,
804
                            'edit_date' => true,
805
                        );
806
807
                        wp_update_post( $args );
808
                        break;
809
                    case 'due_date':
810
                        if ( empty( $this->due_date ) ) {
811
                            $this->due_date = 'none';
812
                        }
813
                        
814
                        $this->update_meta( '_wpinv_due_date', $this->due_date );
815
                        break;
816
                    case 'completed_date':
817
                        $this->update_meta( '_wpinv_completed_date', $this->completed_date );
818
                        break;
819
                    case 'discounts':
820
                        if ( ! is_array( $this->discounts ) ) {
821
                            $this->discounts = explode( ',', $this->discounts );
822
                        }
823
824
                        $this->user_info['discount'] = implode( ',', $this->discounts );
825
                        break;
826
                    case 'discount':
827
                        $this->update_meta( '_wpinv_discount', wpinv_round_amount( $this->discount ) );
828
                        break;
829
                    case 'discount_code':
830
                        $this->update_meta( '_wpinv_discount_code', $this->discount_code );
831
                        break;
832
                    case 'parent_invoice':
833
                        $args = array(
834
                            'ID'          => $this->ID,
835
                            'post_parent' => $this->parent_invoice,
836
                        );
837
                        wp_update_post( $args );
838
                        break;
839
                    default:
840
                        do_action( 'wpinv_save', $this, $key );
841
                        break;
842
                }
843
            }
844
845
            $this->update_meta( '_wpinv_subtotal', wpinv_round_amount( $this->subtotal ) );
846
            $this->update_meta( '_wpinv_total', wpinv_round_amount( $this->total ) );
847
            $this->update_meta( '_wpinv_tax', wpinv_round_amount( $this->tax ) );
848
            
849
            $this->items    = array_values( $this->items );
850
            
851
            $new_meta = array(
852
                'items'         => $this->items,
853
                'cart_details'  => $this->cart_details,
854
                'fees'          => $this->fees,
855
                'currency'      => $this->currency,
856
                'user_info'     => $this->user_info,
857
            );
858
            
859
            $meta        = $this->get_meta();
860
            $merged_meta = array_merge( $meta, $new_meta );
861
862
            // Only save the payment meta if it's changed
863
            if ( md5( serialize( $meta ) ) !== md5( serialize( $merged_meta) ) ) {
864
                $updated     = $this->update_meta( '_wpinv_payment_meta', $merged_meta );
865
                if ( false !== $updated ) {
866
                    $saved = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $saved is dead and can be removed.
Loading history...
867
                }
868
            }
869
870
            $this->pending = array();
871
            $saved         = true;
872
        } else {
873
            $this->update_meta( '_wpinv_subtotal', wpinv_round_amount( $this->subtotal ) );
874
            $this->update_meta( '_wpinv_total', wpinv_round_amount( $this->total ) );
875
            $this->update_meta( '_wpinv_tax', wpinv_round_amount( $this->tax ) );
876
        }
877
        
878
        do_action( 'wpinv_invoice_save', $this, $saved );
879
880
        if ( true === $saved || $setup ) {
881
            $this->setup_invoice( $this->ID );
882
        }
883
        
884
        $this->refresh_item_ids();
885
        
886
        return $saved;
887
    }
888
    
889
    public function add_fee( $args, $global = true ) {
890
        $default_args = array(
891
            'label'       => '',
892
            'amount'      => 0,
893
            'type'        => 'fee',
894
            'id'          => '',
895
            'no_tax'      => false,
896
            'item_id'     => 0,
897
        );
898
899
        $fee = wp_parse_args( $args, $default_args );
900
        
901
        if ( empty( $fee['label'] ) ) {
902
            return false;
903
        }
904
        
905
        $fee['id']  = sanitize_title( $fee['label'] );
906
        
907
        $this->fees[]               = $fee;
908
        
909
        $added_fee               = $fee;
910
        $added_fee['action']     = 'add';
911
        $this->pending['fees'][] = $added_fee;
912
        reset( $this->fees );
913
914
        $this->increase_fees( $fee['amount'] );
915
        return true;
916
    }
917
918
    public function remove_fee( $key ) {
919
        $removed = false;
920
921
        if ( is_numeric( $key ) ) {
922
            $removed = $this->remove_fee_by( 'index', $key );
923
        }
924
925
        return $removed;
926
    }
927
928
    public function remove_fee_by( $key, $value, $global = false ) {
929
        $allowed_fee_keys = apply_filters( 'wpinv_fee_keys', array(
930
            'index', 'label', 'amount', 'type',
931
        ) );
932
933
        if ( ! in_array( $key, $allowed_fee_keys ) ) {
934
            return false;
935
        }
936
937
        $removed = false;
938
        if ( 'index' === $key && array_key_exists( $value, $this->fees ) ) {
939
            $removed_fee             = $this->fees[ $value ];
940
            $removed_fee['action']   = 'remove';
941
            $this->pending['fees'][] = $removed_fee;
942
943
            $this->decrease_fees( $removed_fee['amount'] );
944
945
            unset( $this->fees[ $value ] );
946
            $removed = true;
947
        } else if ( 'index' !== $key ) {
948
            foreach ( $this->fees as $index => $fee ) {
949
                if ( isset( $fee[ $key ] ) && $fee[ $key ] == $value ) {
950
                    $removed_fee             = $fee;
951
                    $removed_fee['action']   = 'remove';
952
                    $this->pending['fees'][] = $removed_fee;
953
954
                    $this->decrease_fees( $removed_fee['amount'] );
955
956
                    unset( $this->fees[ $index ] );
957
                    $removed = true;
958
959
                    if ( false === $global ) {
960
                        break;
961
                    }
962
                }
963
            }
964
        }
965
966
        if ( true === $removed ) {
967
            $this->fees = array_values( $this->fees );
968
        }
969
970
        return $removed;
971
    }
972
973
    
974
975
    public function add_note( $note = '', $customer_type = false, $added_by_user = false, $system = false ) {
976
        // Bail if no note specified
977
        if( !$note ) {
978
            return false;
979
        }
980
981
        if ( empty( $this->ID ) )
982
            return false;
983
        
984
        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...
985
            $user                 = get_user_by( 'id', get_current_user_id() );
986
            $comment_author       = $user->display_name;
987
            $comment_author_email = $user->user_email;
988
        } else {
989
            $comment_author       = 'System';
990
            $comment_author_email = 'system@';
991
            $comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com';
992
            $comment_author_email = sanitize_email( $comment_author_email );
993
        }
994
995
        do_action( 'wpinv_pre_insert_invoice_note', $this->ID, $note, $customer_type );
996
997
        $note_id = wp_insert_comment( wp_filter_comment( array(
998
            'comment_post_ID'      => $this->ID,
999
            'comment_content'      => $note,
1000
            'comment_agent'        => 'WPInvoicing',
1001
            'user_id'              => is_admin() ? get_current_user_id() : 0,
1002
            'comment_date'         => current_time( 'mysql' ),
1003
            'comment_date_gmt'     => current_time( 'mysql', 1 ),
1004
            'comment_approved'     => 1,
1005
            'comment_parent'       => 0,
1006
            'comment_author'       => $comment_author,
1007
            'comment_author_IP'    => wpinv_get_ip(),
1008
            'comment_author_url'   => '',
1009
            'comment_author_email' => $comment_author_email,
1010
            'comment_type'         => 'wpinv_note'
1011
        ) ) );
1012
1013
        do_action( 'wpinv_insert_payment_note', $note_id, $this->ID, $note );
1014
        
1015
        if ( $customer_type ) {
1016
            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

1016
            add_comment_meta( /** @scrutinizer ignore-type */ $note_id, '_wpi_customer_note', 1 );
Loading history...
1017
1018
            do_action( 'wpinv_new_customer_note', array( 'invoice_id' => $this->ID, 'user_note' => $note ) );
1019
        }
1020
1021
        return $note_id;
1022
    }
1023
1024
    private function increase_subtotal( $amount = 0.00 ) {
1025
        $amount          = (float) $amount;
1026
        $this->subtotal += $amount;
1027
        $this->subtotal  = wpinv_round_amount( $this->subtotal );
1028
1029
        $this->recalculate_total();
1030
    }
1031
1032
    private function decrease_subtotal( $amount = 0.00 ) {
1033
        $amount          = (float) $amount;
1034
        $this->subtotal -= $amount;
1035
        $this->subtotal  = wpinv_round_amount( $this->subtotal );
1036
1037
        if ( $this->subtotal < 0 ) {
1038
            $this->subtotal = 0;
1039
        }
1040
1041
        $this->recalculate_total();
1042
    }
1043
1044
    private function increase_fees( $amount = 0.00 ) {
1045
        $amount            = (float)$amount;
1046
        $this->fees_total += $amount;
1047
        $this->fees_total  = wpinv_round_amount( $this->fees_total );
1048
1049
        $this->recalculate_total();
1050
    }
1051
1052
    private function decrease_fees( $amount = 0.00 ) {
1053
        $amount            = (float) $amount;
1054
        $this->fees_total -= $amount;
1055
        $this->fees_total  = wpinv_round_amount( $this->fees_total );
1056
1057
        if ( $this->fees_total < 0 ) {
1058
            $this->fees_total = 0;
1059
        }
1060
1061
        $this->recalculate_total();
1062
    }
1063
1064
    public function recalculate_total() {
1065
        global $wpi_nosave;
1066
        
1067
        $this->total = $this->subtotal + $this->tax + $this->fees_total;
1068
        $this->total = wpinv_round_amount( $this->total );
1069
        
1070
        do_action( 'wpinv_invoice_recalculate_total', $this, $wpi_nosave );
1071
    }
1072
    
1073
    public function increase_tax( $amount = 0.00 ) {
1074
        $amount       = (float) $amount;
1075
        $this->tax   += $amount;
1076
1077
        $this->recalculate_total();
1078
    }
1079
1080
    public function decrease_tax( $amount = 0.00 ) {
1081
        $amount     = (float) $amount;
1082
        $this->tax -= $amount;
1083
1084
        if ( $this->tax < 0 ) {
1085
            $this->tax = 0;
1086
        }
1087
1088
        $this->recalculate_total();
1089
    }
1090
1091
    public function update_status( $new_status = false, $note = '', $manual = false ) {
1092
        $old_status = ! empty( $this->old_status ) ? $this->old_status : get_post_status( $this->ID );
1093
1094
        if ( $old_status === $new_status && in_array( $new_status, array_keys( wpinv_get_invoice_statuses( true ) ) ) ) {
1095
            return false; // Don't permit status changes that aren't changes
1096
        }
1097
1098
        $do_change = apply_filters( 'wpinv_should_update_invoice_status', true, $this->ID, $new_status, $old_status );
1099
        $updated = false;
1100
1101
        if ( $do_change ) {
1102
            do_action( 'wpinv_before_invoice_status_change', $this->ID, $new_status, $old_status );
1103
1104
            $update_post_data                   = array();
1105
            $update_post_data['ID']             = $this->ID;
1106
            $update_post_data['post_status']    = $new_status;
1107
            $update_post_data['edit_date']      = current_time( 'mysql', 0 );
1108
            $update_post_data['edit_date_gmt']  = current_time( 'mysql', 1 );
1109
            
1110
            $update_post_data = apply_filters( 'wpinv_update_invoice_status_fields', $update_post_data, $this->ID );
1111
1112
            $updated = wp_update_post( $update_post_data );     
1113
           
1114
            // Process any specific status functions
1115
            switch( $new_status ) {
1116
                case 'wpi-refunded':
1117
                    $this->process_refund();
1118
                    break;
1119
                case 'wpi-failed':
1120
                    $this->process_failure();
1121
                    break;
1122
                case 'wpi-pending':
1123
                    $this->process_pending();
1124
                    break;
1125
            }
1126
            
1127
            // Status was changed.
1128
            do_action( 'wpinv_status_' . $new_status, $this->ID, $old_status );
1129
            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

1129
            do_action( 'wpinv_status_' . /** @scrutinizer ignore-type */ $old_status . '_to_' . $new_status, $this->ID, $old_status );
Loading history...
1130
            do_action( 'wpinv_update_status', $this->ID, $new_status, $old_status );
1131
        }
1132
1133
        return $updated;
1134
    }
1135
1136
    public function refund() {
1137
        $this->old_status        = $this->status;
1138
        $this->status            = 'wpi-refunded';
1139
        $this->pending['status'] = $this->status;
1140
1141
        $this->save();
1142
    }
1143
1144
    public function update_meta( $meta_key = '', $meta_value = '', $prev_value = '' ) {
1145
        if ( empty( $meta_key ) ) {
1146
            return false;
1147
        }
1148
1149
        if ( $meta_key == 'key' || $meta_key == 'date' ) {
1150
            $current_meta = $this->get_meta();
1151
            $current_meta[ $meta_key ] = $meta_value;
1152
1153
            $meta_key     = '_wpinv_payment_meta';
1154
            $meta_value   = $current_meta;
1155
        }
1156
1157
        $meta_value = apply_filters( 'wpinv_update_payment_meta_' . $meta_key, $meta_value, $this->ID );
1158
        
1159
        // Do not update created date on invoice marked as paid.
1160
        /*if ( $meta_key == '_wpinv_completed_date' && !empty( $meta_value ) ) {
1161
            $args = array(
1162
                'ID'                => $this->ID,
1163
                'post_date'         => $meta_value,
1164
                'edit_date'         => true,
1165
                'post_date_gmt'     => get_gmt_from_date( $meta_value ),
1166
                'post_modified'     => $meta_value,
1167
                'post_modified_gmt' => get_gmt_from_date( $meta_value )
1168
            );
1169
            wp_update_post( $args );
1170
        }*/
1171
        
1172
        return update_post_meta( $this->ID, $meta_key, $meta_value, $prev_value );
1173
    }
1174
1175
    private function process_refund() {
1176
        $process_refund = true;
1177
1178
        // If the payment was not in publish, don't decrement stats as they were never incremented
1179
        if ( 'publish' != $this->old_status || 'wpi-refunded' != $this->status ) {
1180
            $process_refund = false;
1181
        }
1182
1183
        // Allow extensions to filter for their own payment types, Example: Recurring Payments
1184
        $process_refund = apply_filters( 'wpinv_should_process_refund', $process_refund, $this );
1185
1186
        if ( false === $process_refund ) {
1187
            return;
1188
        }
1189
1190
        do_action( 'wpinv_pre_refund_invoice', $this );
1191
        
1192
        $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...
1193
        $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...
1194
        $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...
1195
        
1196
        do_action( 'wpinv_post_refund_invoice', $this );
1197
    }
1198
1199
    private function process_failure() {
1200
        $discounts = $this->discounts;
1201
        if ( empty( $discounts ) ) {
1202
            return;
1203
        }
1204
1205
        if ( ! is_array( $discounts ) ) {
0 ignored issues
show
introduced by
The condition is_array($discounts) is always false.
Loading history...
1206
            $discounts = array_map( 'trim', explode( ',', $discounts ) );
1207
        }
1208
1209
        foreach ( $discounts as $discount ) {
1210
            wpinv_decrease_discount_usage( $discount );
1211
        }
1212
    }
1213
    
1214
    private function process_pending() {
1215
        $process_pending = true;
1216
1217
        // If the payment was not in publish or revoked status, don't decrement stats as they were never incremented
1218
        if ( ( 'publish' != $this->old_status && 'revoked' != $this->old_status ) || 'wpi-pending' != $this->status ) {
1219
            $process_pending = false;
1220
        }
1221
1222
        // Allow extensions to filter for their own payment types, Example: Recurring Payments
1223
        $process_pending = apply_filters( 'wpinv_should_process_pending', $process_pending, $this );
1224
1225
        if ( false === $process_pending ) {
1226
            return;
1227
        }
1228
1229
        $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...
1230
        $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...
1231
        $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...
1232
1233
        $this->completed_date = '';
1234
        $this->update_meta( '_wpinv_completed_date', '' );
1235
    }
1236
    
1237
    // get data
1238
    public function get_meta( $meta_key = '_wpinv_payment_meta', $single = true ) {
1239
        $meta = get_post_meta( $this->ID, $meta_key, $single );
1240
1241
        if ( $meta_key === '_wpinv_payment_meta' ) {
1242
1243
            if(!is_array($meta)){$meta = array();} // we need this to be an array so make sure it is.
1244
1245
            if ( empty( $meta['key'] ) ) {
1246
                $meta['key'] = $this->setup_invoice_key();
1247
            }
1248
1249
            if ( empty( $meta['date'] ) ) {
1250
                $meta['date'] = get_post_field( 'post_date', $this->ID );
1251
            }
1252
        }
1253
1254
        $meta = apply_filters( 'wpinv_get_invoice_meta_' . $meta_key, $meta, $this->ID );
1255
1256
        return apply_filters( 'wpinv_get_invoice_meta', $meta, $this->ID, $meta_key );
1257
    }
1258
    
1259
    public function get_description() {
1260
        $post = get_post( $this->ID );
1261
        
1262
        $description = !empty( $post ) ? $post->post_content : '';
1263
        return apply_filters( 'wpinv_get_description', $description, $this->ID, $this );
1264
    }
1265
    
1266
    public function get_status( $nicename = false ) {
1267
        if ( !$nicename ) {
1268
            $status = $this->status;
1269
        } else {
1270
            $status = $this->status_nicename;
1271
        }
1272
        
1273
        return apply_filters( 'wpinv_get_status', $status, $nicename, $this->ID, $this );
1274
    }
1275
    
1276
    public function get_cart_details() {
1277
        return apply_filters( 'wpinv_cart_details', $this->cart_details, $this->ID, $this );
1278
    }
1279
    
1280
    public function get_subtotal( $currency = false ) {
1281
        $subtotal = wpinv_round_amount( $this->subtotal );
1282
        
1283
        if ( $currency ) {
1284
            $subtotal = wpinv_price( wpinv_format_amount( $subtotal, NULL, !$currency ), $this->get_currency() );
1285
        }
1286
        
1287
        return apply_filters( 'wpinv_get_invoice_subtotal', $subtotal, $this->ID, $this, $currency );
1288
    }
1289
    
1290
    public function get_total( $currency = false ) {        
1291
        if ( $this->is_free_trial() ) {
1292
            $total = wpinv_round_amount( 0 );
1293
        } else {
1294
            $total = wpinv_round_amount( $this->total );
1295
        }
1296
        if ( $currency ) {
1297
            $total = wpinv_price( wpinv_format_amount( $total, NULL, !$currency ), $this->get_currency() );
1298
        }
1299
        
1300
        return apply_filters( 'wpinv_get_invoice_total', $total, $this->ID, $this, $currency );
1301
    }
1302
    
1303
    public function get_recurring_details( $field = '', $currency = false ) {        
1304
        $data                 = array();
1305
        $data['cart_details'] = $this->cart_details;
1306
        $data['subtotal']     = $this->get_subtotal();
1307
        $data['discount']     = $this->get_discount();
1308
        $data['tax']          = $this->get_tax();
1309
        $data['total']        = $this->get_total();
1310
1311
        if ( !empty( $this->cart_details ) && ( $this->is_parent() || $this->is_renewal() ) ) {
1312
            $is_free_trial = $this->is_free_trial();
1313
            $discounts = $this->get_discounts( true );
1314
            
1315
            if ( $is_free_trial || !empty( $discounts ) ) {
1316
                $first_use_only = false;
1317
                
1318
                if ( !empty( $discounts ) ) {
1319
                    foreach ( $discounts as $key => $code ) {
1320
                        if ( wpinv_discount_is_recurring( $code, true ) && ! $this->is_renewal() ) {
1321
                            $first_use_only = true;
1322
                            break;
1323
                        }
1324
                    }
1325
                }
1326
                    
1327
                if ( !$first_use_only ) {
1328
                    $data['subtotal'] = wpinv_round_amount( $this->subtotal );
1329
                    $data['discount'] = wpinv_round_amount( $this->discount );
1330
                    $data['tax']      = wpinv_round_amount( $this->tax );
1331
                    $data['total']    = wpinv_round_amount( $this->total );
1332
                } else {
1333
                    $cart_subtotal   = 0;
1334
                    $cart_discount   = $this->discount;
1335
                    $cart_tax        = 0;
1336
1337
                    foreach ( $this->cart_details as $key => $item ) {
1338
                        $item_quantity  = $item['quantity'] > 0 ? absint( $item['quantity'] ) : 1;
1339
                        $item_subtotal  = !empty( $item['subtotal'] ) ? $item['subtotal'] : $item['item_price'] * $item_quantity;
1340
                        $item_discount  = 0;
1341
                        $item_tax       = $item_subtotal > 0 && !empty( $item['vat_rate'] ) ? ( $item_subtotal * 0.01 * (float)$item['vat_rate'] ) : 0;
1342
                        
1343
                        if ( wpinv_prices_include_tax() ) {
1344
                            $item_subtotal -= wpinv_round_amount( $item_tax );
1345
                        }
1346
                        
1347
                        $item_total     = $item_subtotal - $item_discount + $item_tax;
1348
                        // Do not allow totals to go negative
1349
                        if ( $item_total < 0 ) {
1350
                            $item_total = 0;
1351
                        }
1352
                        
1353
                        $cart_subtotal  += (float)($item_subtotal);
1354
                        $cart_discount  += (float)($item_discount);
1355
                        $cart_tax       += (float)($item_tax);
1356
                        
1357
                        $data['cart_details'][$key]['discount']   = wpinv_round_amount( $item_discount );
1358
                        $data['cart_details'][$key]['tax']        = wpinv_round_amount( $item_tax );
1359
                        $data['cart_details'][$key]['price']      = wpinv_round_amount( $item_total );
1360
                    }
1361
1362
	                $total = $data['subtotal'] - $data['discount'] + $data['tax'];
1363
	                if ( $total < 0 ) {
1364
		                $total = 0;
1365
	                }
1366
1367
                    $data['subtotal'] = wpinv_round_amount( $cart_subtotal );
1368
                    $data['discount'] = wpinv_round_amount( $cart_discount );
1369
                    $data['tax']      = wpinv_round_amount( $cart_tax );
1370
                    $data['total']    = wpinv_round_amount( $total );
1371
                }
1372
            }
1373
        }
1374
1375
        $data = apply_filters( 'wpinv_get_invoice_recurring_details', $data, $this, $field, $currency );
1376
1377
        if ( isset( $data[$field] ) ) {
1378
            return ( $currency ? wpinv_price( $data[$field], $this->get_currency() ) : $data[$field] );
1379
        }
1380
        
1381
        return $data;
1382
    }
1383
1384
    /**
1385
     * Checks if the invoice has a non-recurring discount.
1386
     * 
1387
     * This is usefull for example when the customer uses a 100% discount
1388
     * that is only valid for the first year of a subscription.
1389
     */
1390
    public function has_non_recurring_discount() {
1391
1392
        // If the invoice is made up of free items, abort
1393
        if ( ! $this->get_subtotal() > 0 ) {
1394
            return false;
1395
        }
1396
1397
        // Ensure there are discounts
1398
        $discounts = $this->get_discounts( true );
1399
        if ( empty( $discounts ) ) {
1400
            return false;
1401
        }
1402
1403
        foreach ( array_values( $discounts ) as $discount_code ) {
0 ignored issues
show
Bug introduced by
It seems like $discounts can also be of type string; however, parameter $input of array_values() does only seem to accept array, 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

1403
        foreach ( array_values( /** @scrutinizer ignore-type */ $discounts ) as $discount_code ) {
Loading history...
1404
            if ( ! wpinv_discount_is_recurring( null, $discount_code ) && wpinv_get_discount_amount( $discount_code ) > 0 ) {
1405
                return true;
1406
            }
1407
        }
1408
        
1409
        return false;
1410
    }
1411
    
1412
    public function get_final_tax( $currency = false ) {        
1413
        $final_total = wpinv_round_amount( $this->tax );
1414
        if ( $currency ) {
1415
            $final_total = wpinv_price( wpinv_format_amount( $final_total, NULL, !$currency ), $this->get_currency() );
1416
        }
1417
        
1418
        return apply_filters( 'wpinv_get_invoice_final_total', $final_total, $this, $currency );
1419
    }
1420
    
1421
    public function get_discounts( $array = false ) {
1422
        $discounts = $this->discounts;
1423
        if ( $array && $discounts ) {
1424
            $discounts = explode( ',', $discounts );
1425
        }
1426
        return apply_filters( 'wpinv_payment_discounts', $discounts, $this->ID, $this, $array );
1427
    }
1428
    
1429
    public function get_discount( $currency = false, $dash = false ) {
1430
        if ( !empty( $this->discounts ) ) {
1431
            global $ajax_cart_details;
1432
            $ajax_cart_details = $this->get_cart_details();
1433
            
1434
            if ( !empty( $ajax_cart_details ) && count( $ajax_cart_details ) == count( $this->items ) ) {
1435
                $cart_items = $ajax_cart_details;
1436
            } else {
1437
                $cart_items = $this->items;
1438
            }
1439
1440
            $this->discount = wpinv_get_cart_items_discount_amount( $cart_items , $this->discounts );
1441
        }
1442
        $discount   = wpinv_round_amount( $this->discount );
1443
        $dash       = $dash && $discount > 0 ? '&ndash;' : '';
1444
        
1445
        if ( $currency ) {
1446
            $discount = wpinv_price( wpinv_format_amount( $discount, NULL, !$currency ), $this->get_currency() );
1447
        }
1448
        
1449
        $discount   = $dash . $discount;
1450
        
1451
        return apply_filters( 'wpinv_get_invoice_discount', $discount, $this->ID, $this, $currency, $dash );
1452
    }
1453
    
1454
    public function get_discount_code() {
1455
        return $this->discount_code;
1456
    }
1457
    
1458
    public function get_tax( $currency = false ) {
1459
        $tax = wpinv_round_amount( $this->tax );
1460
        
1461
        if ( $currency ) {
1462
            $tax = wpinv_price( wpinv_format_amount( $tax, NULL, !$currency ), $this->get_currency() );
1463
        }
1464
        
1465
        return apply_filters( 'wpinv_get_invoice_tax', $tax, $this->ID, $this, $currency );
1466
    }
1467
    
1468
    public function get_fees( $type = 'all' ) {
1469
        $fees    = array();
1470
1471
        if ( ! empty( $this->fees ) && is_array( $this->fees ) ) {
1472
            foreach ( $this->fees as $fee ) {
1473
                if( 'all' != $type && ! empty( $fee['type'] ) && $type != $fee['type'] ) {
1474
                    continue;
1475
                }
1476
1477
                $fee['label'] = stripslashes( $fee['label'] );
1478
                $fee['amount_display'] = wpinv_price( $fee['amount'], $this->get_currency() );
1479
                $fees[]    = $fee;
1480
            }
1481
        }
1482
1483
        return apply_filters( 'wpinv_get_invoice_fees', $fees, $this->ID, $this );
1484
    }
1485
    
1486
    public function get_fees_total( $type = 'all' ) {
1487
        $fees_total = (float) 0.00;
1488
1489
        $payment_fees = isset( $this->payment_meta['fees'] ) ? $this->payment_meta['fees'] : array();
1490
        if ( ! empty( $payment_fees ) ) {
1491
            foreach ( $payment_fees as $fee ) {
1492
                $fees_total += (float) $fee['amount'];
1493
            }
1494
        }
1495
1496
        return apply_filters( 'wpinv_get_invoice_fees_total', $fees_total, $this->ID, $this );
1497
        /*
1498
        $fees = $this->get_fees( $type );
1499
1500
        $fees_total = 0;
1501
        if ( ! empty( $fees ) && is_array( $fees ) ) {
1502
            foreach ( $fees as $fee_id => $fee ) {
1503
                if( 'all' != $type && !empty( $fee['type'] ) && $type != $fee['type'] ) {
1504
                    continue;
1505
                }
1506
1507
                $fees_total += $fee['amount'];
1508
            }
1509
        }
1510
1511
        return apply_filters( 'wpinv_get_invoice_fees_total', $fees_total, $this->ID, $this );
1512
        */
1513
    }
1514
1515
    public function get_user_id() {
1516
        return apply_filters( 'wpinv_user_id', $this->user_id, $this->ID, $this );
1517
    }
1518
    
1519
    public function get_first_name() {
1520
        return apply_filters( 'wpinv_first_name', $this->first_name, $this->ID, $this );
1521
    }
1522
    
1523
    public function get_last_name() {
1524
        return apply_filters( 'wpinv_last_name', $this->last_name, $this->ID, $this );
1525
    }
1526
    
1527
    public function get_user_full_name() {
1528
        return apply_filters( 'wpinv_user_full_name', $this->full_name, $this->ID, $this );
1529
    }
1530
    
1531
    public function get_user_info() {
1532
        return apply_filters( 'wpinv_user_info', $this->user_info, $this->ID, $this );
1533
    }
1534
    
1535
    public function get_email() {
1536
        return apply_filters( 'wpinv_user_email', $this->email, $this->ID, $this );
1537
    }
1538
    
1539
    public function get_address() {
1540
        return apply_filters( 'wpinv_address', $this->address, $this->ID, $this );
1541
    }
1542
    
1543
    public function get_phone() {
1544
        return apply_filters( 'wpinv_phone', $this->phone, $this->ID, $this );
1545
    }
1546
    
1547
    public function get_number() {
1548
        return apply_filters( 'wpinv_number', $this->number, $this->ID, $this );
1549
    }
1550
    
1551
    public function get_items() {
1552
        return apply_filters( 'wpinv_payment_meta_items', $this->items, $this->ID, $this );
1553
    }
1554
    
1555
    public function get_key() {
1556
        return apply_filters( 'wpinv_key', $this->key, $this->ID, $this );
1557
    }
1558
    
1559
    public function get_transaction_id() {
1560
        return apply_filters( 'wpinv_get_invoice_transaction_id', $this->transaction_id, $this->ID, $this );
1561
    }
1562
    
1563
    public function get_gateway() {
1564
        return apply_filters( 'wpinv_gateway', $this->gateway, $this->ID, $this );
1565
    }
1566
    
1567
    public function get_gateway_title() {
1568
        $this->gateway_title = !empty( $this->gateway_title ) ? $this->gateway_title : wpinv_get_gateway_checkout_label( $this->gateway );
1569
        
1570
        return apply_filters( 'wpinv_gateway_title', $this->gateway_title, $this->ID, $this );
1571
    }
1572
    
1573
    public function get_currency() {
1574
        return apply_filters( 'wpinv_currency_code', $this->currency, $this->ID, $this );
1575
    }
1576
    
1577
    public function get_created_date() {
1578
        return apply_filters( 'wpinv_created_date', $this->date, $this->ID, $this );
1579
    }
1580
    
1581
    public function get_due_date( $display = false ) {
1582
        $due_date = apply_filters( 'wpinv_due_date', $this->due_date, $this->ID, $this );
1583
        
1584
        if ( !$display || empty( $due_date ) ) {
1585
            return $due_date;
1586
        }
1587
        
1588
        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

1588
        return date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $due_date ) );
Loading history...
1589
    }
1590
    
1591
    public function get_completed_date() {
1592
        return apply_filters( 'wpinv_completed_date', $this->completed_date, $this->ID, $this );
1593
    }
1594
    
1595
    public function get_invoice_date( $formatted = true ) {
1596
        $date_completed = $this->completed_date;
1597
        $invoice_date   = $date_completed != '' && $date_completed != '0000-00-00 00:00:00' ? $date_completed : '';
1598
        
1599
        if ( $invoice_date == '' ) {
1600
            $date_created   = $this->date;
1601
            $invoice_date   = $date_created != '' && $date_created != '0000-00-00 00:00:00' ? $date_created : '';
1602
        }
1603
        
1604
        if ( $formatted && $invoice_date ) {
1605
            $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

1605
            $invoice_date   = date_i18n( /** @scrutinizer ignore-type */ get_option( 'date_format' ), strtotime( $invoice_date ) );
Loading history...
1606
        }
1607
1608
        return apply_filters( 'wpinv_get_invoice_date', $invoice_date, $formatted, $this->ID, $this );
1609
    }
1610
    
1611
    public function get_ip() {
1612
        return apply_filters( 'wpinv_user_ip', $this->ip, $this->ID, $this );
1613
    }
1614
        
1615
    public function has_status( $status ) {
1616
        return apply_filters( 'wpinv_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status ) ) || $this->get_status() === $status ? true : false, $this, $status );
1617
    }
1618
    
1619
    public function add_item( $item_id = 0, $args = array() ) {
1620
        global $wpi_current_id, $wpi_item_id;
1621
        
1622
        $item = new WPInv_Item( $item_id );
1623
1624
        // Bail if this post isn't a item
1625
        if( !$item || $item->post_type !== 'wpi_item' ) {
0 ignored issues
show
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...
introduced by
$item is of type WPInv_Item, thus it always evaluated to true.
Loading history...
1626
            return false;
1627
        }
1628
        
1629
        $has_quantities = wpinv_item_quantities_enabled();
1630
1631
        // Set some defaults
1632
        $defaults = array(
1633
            'quantity'      => 1,
1634
            'id'            => false,
1635
            'name'          => $item->get_name(),
1636
            'item_price'    => false,
1637
            'custom_price'  => '',
1638
            'discount'      => 0,
1639
            'tax'           => 0.00,
1640
            'meta'          => array(),
1641
            'fees'          => array()
1642
        );
1643
1644
        $args = wp_parse_args( apply_filters( 'wpinv_add_item_args', $args, $item->ID ), $defaults );
1645
        $args['quantity']   = $has_quantities && $args['quantity'] > 0 ? absint( $args['quantity'] ) : 1;
1646
1647
        $wpi_current_id         = $this->ID;
1648
        $wpi_item_id            = $item->ID;
1649
        $discounts              = $this->get_discounts();
0 ignored issues
show
Unused Code introduced by
The assignment to $discounts is dead and can be removed.
Loading history...
1650
        
1651
        $_POST['wpinv_country'] = $this->country;
1652
        $_POST['wpinv_state']   = $this->state;
1653
        
1654
        $found_cart_key         = false;
1655
        
1656
        if ($has_quantities) {
1657
            $this->cart_details = !empty( $this->cart_details ) ? array_values( $this->cart_details ) : $this->cart_details;
1658
            
1659
            foreach ( $this->items as $key => $cart_item ) {
1660
                if ( (int)$item_id !== (int)$cart_item['id'] ) {
1661
                    continue;
1662
                }
1663
1664
                $this->items[ $key ]['quantity'] += $args['quantity'];
1665
                break;
1666
            }
1667
            
1668
            foreach ( $this->cart_details as $cart_key => $cart_item ) {
1669
                if ( $item_id != $cart_item['id'] ) {
1670
                    continue;
1671
                }
1672
1673
                $found_cart_key = $cart_key;
1674
                break;
1675
            }
1676
        }
1677
        
1678
        if ($has_quantities && $found_cart_key !== false) {
1679
            $cart_item          = $this->cart_details[$found_cart_key];
1680
            $item_price         = $cart_item['item_price'];
1681
            $quantity           = !empty( $cart_item['quantity'] ) ? $cart_item['quantity'] : 1;
1682
            $tax_rate           = !empty( $cart_item['vat_rate'] ) ? $cart_item['vat_rate'] : 0;
1683
            
1684
            $new_quantity       = $quantity + $args['quantity'];
1685
            $subtotal           = $item_price * $new_quantity;
1686
            
1687
            $args['quantity']   = $new_quantity;
1688
            $discount           = !empty( $args['discount'] ) ? $args['discount'] : 0;
1689
            $tax                = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1690
            
1691
            $discount_increased = $discount > 0 && $subtotal > 0 && $discount > (float)$cart_item['discount'] ? $discount - (float)$cart_item['discount'] : 0;
1692
            $tax_increased      = $tax > 0 && $subtotal > 0 && $tax > (float)$cart_item['tax'] ? $tax - (float)$cart_item['tax'] : 0;
1693
            // The total increase equals the number removed * the item_price
1694
            $total_increased    = wpinv_round_amount( $item_price );
1695
            
1696
            if ( wpinv_prices_include_tax() ) {
1697
                $subtotal -= wpinv_round_amount( $tax );
1698
            }
1699
1700
            $total              = $subtotal - $discount + $tax;
1701
1702
            // Do not allow totals to go negative
1703
            if( $total < 0 ) {
1704
                $total = 0;
1705
            }
1706
            
1707
            $cart_item['quantity']  = $new_quantity;
1708
            $cart_item['subtotal']  = $subtotal;
1709
            $cart_item['discount']  = $discount;
1710
            $cart_item['tax']       = $tax;
1711
            $cart_item['price']     = $total;
1712
            
1713
            $subtotal               = $total_increased - $discount_increased;
1714
            $tax                    = $tax_increased;
1715
            
1716
            $this->cart_details[$found_cart_key] = $cart_item;
1717
        } else {
1718
            // Set custom price.
1719
            if ( $args['custom_price'] !== '' ) {
1720
                $item_price = $args['custom_price'];
1721
            } else {
1722
                // Allow overriding the price
1723
                if ( false !== $args['item_price'] ) {
1724
                    $item_price = $args['item_price'];
1725
                } else {
1726
                    $item_price = wpinv_get_item_price( $item->ID );
1727
                }
1728
            }
1729
1730
            // Sanitizing the price here so we don't have a dozen calls later
1731
            $item_price = wpinv_sanitize_amount( $item_price );
1732
            $subtotal   = wpinv_round_amount( $item_price * $args['quantity'] );
1733
        
1734
            $discount   = !empty( $args['discount'] ) ? $args['discount'] : 0;
1735
            $tax_class  = !empty( $args['vat_class'] ) ? $args['vat_class'] : '';
1736
            $tax_rate   = !empty( $args['vat_rate'] ) ? $args['vat_rate'] : 0;
1737
            $tax        = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1738
1739
            // Setup the items meta item
1740
            $new_item = array(
1741
                'id'       => $item->ID,
1742
                'quantity' => $args['quantity'],
1743
            );
1744
1745
            $this->items[]  = $new_item;
1746
1747
            if ( wpinv_prices_include_tax() ) {
1748
                $subtotal -= wpinv_round_amount( $tax );
1749
            }
1750
1751
            $total      = $subtotal - $discount + $tax;
1752
1753
            // Do not allow totals to go negative
1754
            if( $total < 0 ) {
1755
                $total = 0;
1756
            }
1757
        
1758
            $this->cart_details[] = array(
1759
                'name'          => !empty($args['name']) ? $args['name'] : $item->get_name(),
1760
                'id'            => $item->ID,
1761
                'item_price'    => wpinv_round_amount( $item_price ),
1762
                'custom_price'  => ( $args['custom_price'] !== '' ? wpinv_round_amount( $args['custom_price'] ) : '' ),
1763
                'quantity'      => $args['quantity'],
1764
                'discount'      => $discount,
1765
                'subtotal'      => wpinv_round_amount( $subtotal ),
1766
                'tax'           => wpinv_round_amount( $tax ),
1767
                'price'         => wpinv_round_amount( $total ),
1768
                'vat_rate'      => $tax_rate,
1769
                'vat_class'     => $tax_class,
1770
                'meta'          => $args['meta'],
1771
                'fees'          => $args['fees'],
1772
            );
1773
                        
1774
            $subtotal = $subtotal - $discount;
1775
        }
1776
        
1777
        $added_item = end( $this->cart_details );
1778
        $added_item['action']  = 'add';
1779
        
1780
        $this->pending['items'][] = $added_item;
1781
        
1782
        $this->increase_subtotal( $subtotal );
1783
        $this->increase_tax( $tax );
1784
1785
        return true;
1786
    }
1787
    
1788
    public function remove_item( $item_id, $args = array() ) {
1789
        // Set some defaults
1790
        $defaults = array(
1791
            'quantity'      => 1,
1792
            'item_price'    => false,
1793
            'custom_price'  => '',
1794
            'cart_index'    => false,
1795
        );
1796
        $args = wp_parse_args( $args, $defaults );
1797
1798
        // Bail if this post isn't a item
1799
        if ( get_post_type( $item_id ) !== 'wpi_item' ) {
1800
            return false;
1801
        }
1802
        
1803
        $this->cart_details = !empty( $this->cart_details ) ? array_values( $this->cart_details ) : $this->cart_details;
1804
1805
        foreach ( $this->items as $key => $item ) {
1806
            if ( !empty($item['id']) && (int)$item_id !== (int)$item['id'] ) {
1807
                continue;
1808
            }
1809
1810
            if ( false !== $args['cart_index'] ) {
1811
                $cart_index = absint( $args['cart_index'] );
1812
                $cart_item  = ! empty( $this->cart_details[ $cart_index ] ) ? $this->cart_details[ $cart_index ] : false;
1813
1814
                if ( ! empty( $cart_item ) ) {
1815
                    // If the cart index item isn't the same item ID, don't remove it
1816
                    if ( !empty($cart_item['id']) && $cart_item['id'] != $item['id'] ) {
1817
                        continue;
1818
                    }
1819
                }
1820
            }
1821
1822
            $item_quantity = $this->items[ $key ]['quantity'];
1823
            if ( $item_quantity > $args['quantity'] ) {
1824
                $this->items[ $key ]['quantity'] -= $args['quantity'];
1825
                break;
1826
            } else {
1827
                unset( $this->items[ $key ] );
1828
                break;
1829
            }
1830
        }
1831
1832
        $found_cart_key = false;
1833
        if ( false === $args['cart_index'] ) {
1834
            foreach ( $this->cart_details as $cart_key => $item ) {
1835
                if ( $item_id != $item['id'] ) {
1836
                    continue;
1837
                }
1838
1839
                if ( false !== $args['item_price'] ) {
1840
                    if ( isset( $item['item_price'] ) && (float) $args['item_price'] != (float) $item['item_price'] ) {
1841
                        continue;
1842
                    }
1843
                }
1844
1845
                $found_cart_key = $cart_key;
1846
                break;
1847
            }
1848
        } else {
1849
            $cart_index = absint( $args['cart_index'] );
1850
1851
            if ( ! array_key_exists( $cart_index, $this->cart_details ) ) {
1852
                return false; // Invalid cart index passed.
1853
            }
1854
1855
            if ( (int) $this->cart_details[ $cart_index ]['id'] > 0 && (int) $this->cart_details[ $cart_index ]['id'] !== (int) $item_id ) {
1856
                return false; // We still need the proper Item ID to be sure.
1857
            }
1858
1859
            $found_cart_key = $cart_index;
1860
        }
1861
        
1862
        $cart_item  = $this->cart_details[$found_cart_key];
1863
        $quantity   = !empty( $cart_item['quantity'] ) ? $cart_item['quantity'] : 1;
1864
        
1865
        if ( count( $this->cart_details ) == 1 && ( $quantity - $args['quantity'] ) < 1 ) {
1866
            //return false; // Invoice must contain at least one item.
1867
        }
1868
        
1869
        $discounts  = $this->get_discounts();
0 ignored issues
show
Unused Code introduced by
The assignment to $discounts is dead and can be removed.
Loading history...
1870
        
1871
        if ( $quantity > $args['quantity'] ) {
1872
            $item_price         = $cart_item['item_price'];
1873
            $tax_rate           = !empty( $cart_item['vat_rate'] ) ? $cart_item['vat_rate'] : 0;
1874
            
1875
            $new_quantity       = max( $quantity - $args['quantity'], 1);
1876
            $subtotal           = $item_price * $new_quantity;
1877
            
1878
            $args['quantity']   = $new_quantity;
1879
            $discount           = !empty( $cart_item['discount'] ) ? $cart_item['discount'] : 0;
1880
            $tax                = $subtotal > 0 && $tax_rate > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1881
            
1882
            $discount_decrease  = (float)$cart_item['discount'] > 0 && $quantity > 0 ? wpinv_round_amount( ( (float)$cart_item['discount'] / $quantity ) ) : 0;
1883
            $discount_decrease  = $discount > 0 && $subtotal > 0 && (float)$cart_item['discount'] > $discount ? (float)$cart_item['discount'] - $discount : $discount_decrease; 
1884
            $tax_decrease       = (float)$cart_item['tax'] > 0 && $quantity > 0 ? wpinv_round_amount( ( (float)$cart_item['tax'] / $quantity ) ) : 0;
1885
            $tax_decrease       = $tax > 0 && $subtotal > 0 && (float)$cart_item['tax'] > $tax ? (float)$cart_item['tax'] - $tax : $tax_decrease;
1886
            
1887
            // The total increase equals the number removed * the item_price
1888
            $total_decrease     = wpinv_round_amount( $item_price );
1889
            
1890
            if ( wpinv_prices_include_tax() ) {
1891
                $subtotal -= wpinv_round_amount( $tax );
1892
            }
1893
1894
            $total              = $subtotal - $discount + $tax;
1895
1896
            // Do not allow totals to go negative
1897
            if( $total < 0 ) {
1898
                $total = 0;
1899
            }
1900
            
1901
            $cart_item['quantity']  = $new_quantity;
1902
            $cart_item['subtotal']  = $subtotal;
1903
            $cart_item['discount']  = $discount;
1904
            $cart_item['tax']       = $tax;
1905
            $cart_item['price']     = $total;
1906
            
1907
            $added_item             = $cart_item;
1908
            $added_item['id']       = $item_id;
1909
            $added_item['price']    = $total_decrease;
1910
            $added_item['quantity'] = $args['quantity'];
1911
            
1912
            $subtotal_decrease      = $total_decrease - $discount_decrease;
1913
            
1914
            $this->cart_details[$found_cart_key] = $cart_item;
1915
            
1916
            $remove_item = end( $this->cart_details );
1917
        } else {
1918
            $item_price     = $cart_item['item_price'];
1919
            $discount       = !empty( $cart_item['discount'] ) ? $cart_item['discount'] : 0;
1920
            $tax            = !empty( $cart_item['tax'] ) ? $cart_item['tax'] : 0;
1921
        
1922
            $subtotal_decrease  = ( $item_price * $quantity ) - $discount;
1923
            $tax_decrease       = $tax;
1924
1925
            unset( $this->cart_details[$found_cart_key] );
1926
            
1927
            $remove_item             = $args;
1928
            $remove_item['id']       = $item_id;
1929
            $remove_item['price']    = $subtotal_decrease;
1930
            $remove_item['quantity'] = $args['quantity'];
1931
        }
1932
        
1933
        $remove_item['action']      = 'remove';
1934
        $this->pending['items'][]   = $remove_item;
1935
               
1936
        $this->decrease_subtotal( $subtotal_decrease );
1937
        $this->decrease_tax( $tax_decrease );
1938
        
1939
        return true;
1940
    }
1941
    
1942
    public function update_items($temp = false) {
1943
        global $wpinv_euvat, $wpi_current_id, $wpi_item_id, $wpi_nosave;
1944
        
1945
        if ( !empty( $this->cart_details ) ) {
1946
            $wpi_nosave             = $temp;
1947
            $cart_subtotal          = 0;
1948
            $cart_discount          = 0;
1949
            $cart_tax               = 0;
1950
            $cart_details           = array();
1951
            
1952
            $_POST['wpinv_country'] = $this->country;
1953
            $_POST['wpinv_state']   = $this->state;
1954
            
1955
            foreach ( $this->cart_details as $key => $item ) {
1956
                $item_price = $item['item_price'];
1957
                $quantity   = wpinv_item_quantities_enabled() && $item['quantity'] > 0 ? absint( $item['quantity'] ) : 1;
1958
                $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...
1959
                $subtotal   = $item_price * $quantity;
1960
                
1961
                $wpi_current_id         = $this->ID;
1962
                $wpi_item_id            = $item['id'];
1963
                
1964
                $discount   = wpinv_get_cart_item_discount_amount( $item, $this->get_discounts() );
1965
                
1966
                $tax_rate   = wpinv_get_tax_rate( $this->country, $this->state, $wpi_item_id );
1967
                $tax_class  = $wpinv_euvat->get_item_class( $wpi_item_id );
1968
                $tax        = $item_price > 0 ? ( ( $subtotal - $discount ) * 0.01 * (float)$tax_rate ) : 0;
1969
1970
                if ( wpinv_prices_include_tax() ) {
1971
                    $subtotal -= wpinv_round_amount( $tax );
1972
                }
1973
1974
                $total      = $subtotal - $discount + $tax;
1975
1976
                // Do not allow totals to go negative
1977
                if( $total < 0 ) {
1978
                    $total = 0;
1979
                }
1980
1981
                $cart_details[] = array(
1982
                    'id'          => $item['id'],
1983
                    'name'        => $item['name'],
1984
                    'item_price'  => wpinv_round_amount( $item_price ),
1985
                    'custom_price'=> ( isset( $item['custom_price'] ) ? $item['custom_price'] : '' ),
1986
                    'quantity'    => $quantity,
1987
                    'discount'    => $discount,
1988
                    'subtotal'    => wpinv_round_amount( $subtotal ),
1989
                    'tax'         => wpinv_round_amount( $tax ),
1990
                    'price'       => wpinv_round_amount( $total ),
1991
                    'vat_rate'    => $tax_rate,
1992
                    'vat_class'   => $tax_class,
1993
                    'meta'        => isset($item['meta']) ? $item['meta'] : array(),
1994
                    'fees'        => isset($item['fees']) ? $item['fees'] : array(),
1995
                );
1996
                
1997
                $cart_subtotal  += (float)($subtotal - $discount); // TODO
1998
                $cart_discount  += (float)($discount);
1999
                $cart_tax       += (float)($tax);
2000
            }
2001
            if ( $cart_subtotal < 0 ) {
2002
                $cart_subtotal = 0;
2003
            }
2004
            if ( $cart_tax < 0 ) {
2005
                $cart_tax = 0;
2006
            }
2007
            $this->subtotal = wpinv_round_amount( $cart_subtotal );
2008
            $this->tax      = wpinv_round_amount( $cart_tax );
2009
            $this->discount = wpinv_round_amount( $cart_discount );
2010
            
2011
            $this->recalculate_total();
2012
            
2013
            $this->cart_details = $cart_details;
2014
        }
2015
2016
        return $this;
2017
    }
2018
    
2019
    public function recalculate_totals($temp = false) {        
2020
        $this->update_items($temp);
2021
        $this->save( true );
2022
        
2023
        return $this;
2024
    }
2025
    
2026
    public function needs_payment() {
2027
        $valid_invoice_statuses = apply_filters( 'wpinv_valid_invoice_statuses_for_payment', array( 'wpi-pending' ), $this );
2028
2029
        if ( $this->has_status( $valid_invoice_statuses ) && ( $this->get_total() > 0 || $this->is_free_trial() || $this->is_free() || $this->is_initial_free() ) ) {
2030
            $needs_payment = true;
2031
        } else {
2032
            $needs_payment = false;
2033
        }
2034
2035
        return apply_filters( 'wpinv_needs_payment', $needs_payment, $this, $valid_invoice_statuses );
2036
    }
2037
    
2038
    public function get_checkout_payment_url( $with_key = false, $secret = false ) {
2039
        $pay_url = wpinv_get_checkout_uri();
2040
2041
        if ( is_ssl() ) {
2042
            $pay_url = str_replace( 'http:', 'https:', $pay_url );
2043
        }
2044
        
2045
        $key = $this->get_key();
2046
2047
        if ( $with_key ) {
2048
            $pay_url = add_query_arg( 'invoice_key', $key, $pay_url );
2049
        } else {
2050
            $pay_url = add_query_arg( array( 'wpi_action' => 'pay_for_invoice', 'invoice_key' => $key ), $pay_url );
2051
        }
2052
        
2053
        if ( $secret ) {
2054
            $pay_url = add_query_arg( array( '_wpipay' => md5( $this->get_user_id() . '::' . $this->get_email() . '::' . $key ) ), $pay_url );
2055
        }
2056
2057
        return apply_filters( 'wpinv_get_checkout_payment_url', $pay_url, $this, $with_key, $secret );
2058
    }
2059
    
2060
    public function get_view_url( $with_key = false ) {
2061
        $invoice_url = get_permalink( $this->ID );
2062
2063
        if ( $with_key ) {
2064
            $invoice_url = add_query_arg( 'invoice_key', $this->get_key(), $invoice_url );
2065
        }
2066
2067
        return apply_filters( 'wpinv_get_view_url', $invoice_url, $this, $with_key );
2068
    }
2069
    
2070
    public function generate_key( $string = '' ) {
2071
        $auth_key  = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
2072
        return strtolower( md5( $string . date( 'Y-m-d H:i:s' ) . $auth_key . uniqid( 'wpinv', true ) ) );  // Unique key
2073
    }
2074
    
2075
    public function is_recurring() {
2076
        if ( empty( $this->cart_details ) ) {
2077
            return false;
2078
        }
2079
        
2080
        $has_subscription = false;
2081
        foreach( $this->cart_details as $cart_item ) {
2082
            if ( !empty( $cart_item['id'] ) && wpinv_is_recurring_item( $cart_item['id'] )  ) {
2083
                $has_subscription = true;
2084
                break;
2085
            }
2086
        }
2087
        
2088
        if ( count( $this->cart_details ) > 1 ) {
2089
            $has_subscription = false;
2090
        }
2091
2092
        return apply_filters( 'wpinv_invoice_has_recurring_item', $has_subscription, $this->cart_details );
2093
    }
2094
    
2095
    public function is_free_trial() {
2096
        $is_free_trial = false;
2097
        
2098
        if ( $this->is_parent() && $item = $this->get_recurring( true ) ) {
2099
            if ( !empty( $item ) && $item->has_free_trial() ) {
2100
                $is_free_trial = true;
2101
            }
2102
        }
2103
2104
        return apply_filters( 'wpinv_invoice_is_free_trial', $is_free_trial, $this->cart_details );
2105
    }
2106
    
2107
    public function is_initial_free() {
2108
        $is_initial_free = false;
2109
        
2110
        if ( ! ( (float)wpinv_round_amount( $this->get_total() ) > 0 ) && $this->is_parent() && $this->is_recurring() && ! $this->is_free_trial() && ! $this->is_free() ) {
2111
            $is_initial_free = true;
2112
        }
2113
2114
        return apply_filters( 'wpinv_invoice_is_initial_free', $is_initial_free, $this->cart_details );
2115
    }
2116
    
2117
    public function get_recurring( $object = false ) {
2118
        $item = NULL;
2119
        
2120
        if ( empty( $this->cart_details ) ) {
2121
            return $item;
2122
        }
2123
        
2124
        foreach( $this->cart_details as $cart_item ) {
2125
            if ( !empty( $cart_item['id'] ) && wpinv_is_recurring_item( $cart_item['id'] )  ) {
2126
                $item = $cart_item['id'];
2127
                break;
2128
            }
2129
        }
2130
        
2131
        if ( $object ) {
2132
            $item = $item ? new WPInv_Item( $item ) : NULL;
2133
            
2134
            apply_filters( 'wpinv_invoice_get_recurring_item', $item, $this );
2135
        }
2136
2137
        return apply_filters( 'wpinv_invoice_get_recurring_item_id', $item, $this );
2138
    }
2139
    
2140
    public function get_subscription_name() {
2141
        $item = $this->get_recurring( true );
2142
        
2143
        if ( empty( $item ) ) {
2144
            return NULL;
2145
        }
2146
        
2147
        if ( !($name = $item->get_name()) ) {
2148
            $name = $item->post_name;
2149
        }
2150
2151
        return apply_filters( 'wpinv_invoice_get_subscription_name', $name, $this );
2152
    }
2153
    
2154
    public function get_subscription_id() {
2155
        $subscription_id = $this->get_meta( '_wpinv_subscr_profile_id', true );
2156
        
2157
        if ( empty( $subscription_id ) && !empty( $this->parent_invoice ) ) {
2158
            $parent_invoice = wpinv_get_invoice( $this->parent_invoice );
2159
            
2160
            $subscription_id = $parent_invoice->get_meta( '_wpinv_subscr_profile_id', true );
2161
        }
2162
        
2163
        return $subscription_id;
2164
    }
2165
    
2166
    public function is_parent() {
2167
        $is_parent = empty( $this->parent_invoice ) ? true : false;
2168
2169
        return apply_filters( 'wpinv_invoice_is_parent', $is_parent, $this );
2170
    }
2171
    
2172
    public function is_renewal() {
2173
        $is_renewal = $this->parent_invoice && $this->parent_invoice != $this->ID ? true : false;
2174
2175
        return apply_filters( 'wpinv_invoice_is_renewal', $is_renewal, $this );
2176
    }
2177
    
2178
    public function get_parent_payment() {
2179
        $parent_payment = NULL;
2180
        
2181
        if ( $this->is_renewal() ) {
2182
            $parent_payment = wpinv_get_invoice( $this->parent_invoice );
2183
        }
2184
        
2185
        return $parent_payment;
2186
    }
2187
    
2188
    public function is_paid() {
2189
        $is_paid = $this->has_status( array( 'publish', 'wpi-processing', 'wpi-renewal' ) );
2190
2191
        return apply_filters( 'wpinv_invoice_is_paid', $is_paid, $this );
2192
    }
2193
2194
    /**
2195
     * Checks if this is a quote object.
2196
     * 
2197
     * @since 1.0.15
2198
     */
2199
    public function is_quote() {
2200
        return 'wpi_quote' === $this->post_type;
2201
    }
2202
    
2203
    public function is_refunded() {
2204
        $is_refunded = $this->has_status( array( 'wpi-refunded' ) );
2205
2206
        return apply_filters( 'wpinv_invoice_is_refunded', $is_refunded, $this );
2207
    }
2208
    
2209
    public function is_free() {
2210
        $is_free = false;
2211
        
2212
        if ( !( (float)wpinv_round_amount( $this->get_total() ) > 0 ) ) {
2213
            if ( $this->is_parent() && $this->is_recurring() ) {
2214
                $is_free = ( $this->has_non_recurring_discount() || (float)wpinv_round_amount( $this->get_recurring_details( 'total' ) ) ) > 0 ? false : true;
2215
            } else {
2216
                $is_free = true;
2217
            }
2218
        }
2219
        
2220
        return apply_filters( 'wpinv_invoice_is_free', $is_free, $this );
2221
    }
2222
    
2223
    public function has_vat() {
2224
        global $wpinv_euvat, $wpi_country;
2225
        
2226
        $requires_vat = false;
2227
        
2228
        if ( $this->country ) {
2229
            $wpi_country        = $this->country;
2230
            
2231
            $requires_vat       = $wpinv_euvat->requires_vat( $requires_vat, $this->get_user_id(), $wpinv_euvat->invoice_has_digital_rule( $this ) );
2232
        }
2233
        
2234
        return apply_filters( 'wpinv_invoice_has_vat', $requires_vat, $this );
2235
    }
2236
    
2237
    public function refresh_item_ids() {
2238
        $item_ids = array();
2239
        
2240
        if ( !empty( $this->cart_details ) ) {
2241
            foreach ( $this->cart_details as $key => $item ) {
2242
                if ( !empty( $item['id'] ) ) {
2243
                    $item_ids[] = $item['id'];
2244
                }
2245
            }
2246
        }
2247
        
2248
        $item_ids = !empty( $item_ids ) ? implode( ',', array_unique( $item_ids ) ) : '';
2249
        
2250
        update_post_meta( $this->ID, '_wpinv_item_ids', $item_ids );
2251
    }
2252
    
2253
    public function get_invoice_quote_type( $post_id ) {
2254
        if ( empty( $post_id ) ) {
2255
            return '';
2256
        }
2257
2258
        $type = get_post_type( $post_id );
2259
2260
        if ( 'wpi_invoice' === $type ) {
2261
            $post_type = __('Invoice', 'invoicing');
2262
        } else{
2263
            $post_type = __('Quote', 'invoicing');
2264
        }
2265
2266
        return apply_filters('get_invoice_type_label', $post_type, $post_id);
2267
    }
2268
}
2269