Passed
Pull Request — master (#375)
by Brian
101:34
created

process_item_discount()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 6
c 1
b 0
f 1
dl 0
loc 16
rs 10
cc 3
nc 3
nop 1
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Payment form submission class
8
 *
9
 */
10
class GetPaid_Payment_Form_Submission {
11
12
    /**
13
	 * Submission ID
14
	 *
15
	 * @var string
16
	 */
17
	public $id = null;
18
19
	/**
20
	 * Sets the associated payment form.
21
	 *
22
	 * @var GetPaid_Payment_Form
23
	 */
24
    protected $payment_form = null;
25
26
    /**
27
	 * The country for the submission.
28
	 *
29
	 * @var string
30
	 */
31
	public $country = null;
32
33
    /**
34
	 * The state for the submission.
35
	 *
36
	 * @since 1.0.19
37
	 * @var string
38
	 */
39
	public $state = null;
40
41
	/**
42
	 * The invoice associated with the submission.
43
	 *
44
	 * @var WPInv_Invoice
45
	 */
46
	protected $invoice = null;
47
48
	/**
49
	 * The discount associated with the submission.
50
	 *
51
	 * @var WPInv_Discount
52
	 */
53
	protected $discount = null;
54
55
	/**
56
	 * The raw submission data.
57
	 *
58
	 * @var array
59
	 */
60
	protected $data = null;
61
62
	/**
63
	 * Whether this submission contains a recurring item.
64
	 *
65
	 * @var bool
66
	 */
67
	public $has_recurring = false;
68
69
	/**
70
	 * The sub total amount for the submission.
71
	 *
72
	 * @var float
73
	 */
74
	public $subtotal_amount = 0;
75
76
	/**
77
	 * The total discount amount for the submission.
78
	 *
79
	 * @var float
80
	 */
81
	protected $total_discount_amount = 0;
82
83
	/**
84
	 * The total tax amount for the submission.
85
	 *
86
	 * @var float
87
	 */
88
	protected $total_tax_amount = 0;
89
90
	/**
91
	 * The total fees amount for the submission.
92
	 *
93
	 * @var float
94
	 */
95
	protected $total_fees_amount = 0;
96
97
	/**
98
	 * An array of fees for the submission.
99
	 *
100
	 * @var array
101
	 */
102
	protected $fees = array();
103
104
	/**
105
	 * An array of discounts for the submission.
106
	 *
107
	 * @var array
108
	 */
109
	protected $discounts = array();
110
111
	/**
112
	 * An array of taxes for the submission.
113
	 *
114
	 * @var array
115
	 */
116
	protected $taxes = array();
117
118
	/**
119
	 * An array of items for the submission.
120
	 *
121
	 * @var GetPaid_Form_Item[]
122
	 */
123
	protected $items = array();
124
125
	/**
126
	 * The last error.
127
	 *
128
	 * @var string
129
	 */
130
	public $last_error = null;
131
132
	/**
133
	 * Is the discount valid?
134
	 *
135
	 * @var string
136
	 */
137
    public $is_discount_valid = true;
138
139
    /**
140
	 * Class constructor.
141
	 *
142
	 */
143
	public function __construct() {
144
145
		// Set the state and country to the default state and country.
146
		$this->country = wpinv_default_billing_country();
147
		$this->state = wpinv_get_default_state();
0 ignored issues
show
Documentation Bug introduced by
It seems like wpinv_get_default_state() can also be of type false. However, the property $state is declared as type string. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
148
149
		// Do we have an actual submission?
150
		if ( isset( $_POST['getpaid_payment_form_submission'] ) ) {
151
			$this->load_data( $_POST );
152
		}
153
	}
154
155
	/**
156
	 * Loads submission data.
157
	 *
158
	 * @param array $data
159
	 */
160
	public function load_data( $data ) {
161
162
		// Prepare submitted data...
163
		$data = wp_unslash( $data );
164
165
		// Fitter the data.
166
		$data = apply_filters( 'getpaid_submission_data', $data, $this );
167
168
		$this->data = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data can also be of type string. However, the property $data is declared as type array. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
169
170
		$this->id = md5( wp_json_encode( $data ) );
0 ignored issues
show
Bug introduced by
It seems like wp_json_encode($data) can also be of type false; however, parameter $str of md5() 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

170
		$this->id = md5( /** @scrutinizer ignore-type */ wp_json_encode( $data ) );
Loading history...
171
172
		// Every submission needs an active payment form.
173
		if ( empty( $data['form_id'] ) ) {
174
			$this->last_error = __( 'Missing payment form', 'invoicing' );
175
            return;
176
		}
177
178
		// Fetch the payment form.
179
		$form = new GetPaid_Payment_Form( $data['form_id'] );
180
181
		if ( ! $form->is_active() ) {
182
			$this->last_error = __( 'Payment form not active', 'invoicing' );
183
			return;
184
		}
185
186
		// Fetch the payment form.
187
		$this->payment_form = $form;
188
189
		// For existing invoices, make sure that it is valid.
190
        if ( ! empty( $data['invoice_id'] ) ) {
191
            $invoice = wpinv_get_invoice( $data['invoice_id'] );
192
193
            if ( empty( $invoice ) ) {
194
				$this->last_error = __( 'Invalid invoice', 'invoicing' );
195
                return;
196
            }
197
198
			$this->payment_form->set_items( $invoice->cart_details );
199
200
			$this->country = $invoice->country;
201
			$this->state = $invoice->state;
202
203
		// Default forms do not have items.
204
        } else if ( $form->is_default() && isset( $data['form_items'] ) ) {
205
			$this->payment_form->set_items( $data['form_items'] );
206
		}
207
208
		// User's country.
209
		if ( ! empty( $data['wpinv_country'] ) ) {
210
			$this->country = $data['wpinv_country'];
211
		}
212
213
		// User's state.
214
		if ( ! empty( $data['wpinv_state'] ) ) {
215
			$this->country = $data['wpinv_state'];
216
		}
217
218
		// Handle discounts.
219
		$this->maybe_prepare_discount();
220
221
		// Handle items.
222
		$selected_items = array();
223
		if ( ! empty( $data['getpaid-items'] ) ) {
224
			$selected_items = wpinv_clean( $data['getpaid-items'] );
225
		}
226
227
		foreach ( $this->payment_form->get_items() as $item ) {
228
229
			// Continue if this is an optional item and it has not been selected.
230
			if ( ! $item->is_required() && ! isset( $selected_items[ $item->get_id() ] ) ) {
231
				continue;
232
			}
233
234
			// (maybe) let customers change the quantities and prices.
235
			if ( isset( $selected_items[ $item->get_id() ] ) ) {
236
237
				// Maybe change the quantities.
238
				if ( $item->allows_quantities() && is_numeric( $selected_items[ $item->get_id() ]['quantity'] ) ) {
239
					$item->set_quantity( (int) $selected_items[ $item->get_id() ]['quantity'] );
240
				}
241
242
				// Maybe change the price.
243
				if ( $item->user_can_set_their_price() ) {
244
					$price = (float) wpinv_sanitize_amount( $selected_items[ $item->get_id() ]['price'] );
245
246
					// But don't get lower than the minimum price.
247
					if ( $price < $item->get_minimum_price() ) {
248
						$price = $item->get_minimum_price();
249
					}
250
251
					$item->set_price( $price );
252
253
				}
254
255
			}
256
257
			// Add the item to the form.
258
			$this->add_item( $item );
259
260
		}
261
262
		// Fired when we are done processing a submission.
263
		do_action( 'getpaid_process_submission', $this );
264
265
		// Remove invalid discount.
266
		$this->maybe_remove_discount();
267
268
	}
269
270
    /**
271
	 * Returns the payment form.
272
	 *
273
	 * @since 1.0.19
274
	 * @return GetPaid_Payment_Form
275
	 */
276
	public function get_payment_form() {
277
		return $this->payment_form;
278
	}
279
280
	/**
281
	 * Returns the associated invoice.
282
	 *
283
	 * @since 1.0.19
284
	 * @return WPInv_Invoice
285
	 */
286
	public function get_invoice() {
287
		return $this->invoice;
288
	}
289
290
	/**
291
	 * Checks whether there is an invoice associated with this submission.
292
	 *
293
	 * @since 1.0.19
294
	 * @return bool
295
	 */
296
	public function has_invoice() {
297
		return ! empty( $this->invoice );
298
	}
299
	
300
	/**
301
	 * Returns the appropriate currency for the submission.
302
	 *
303
	 * @since 1.0.19
304
	 * @return string
305
	 */
306
	public function get_currency() {
307
		if ( $this->has_invoice() ) {
308
			return $this->invoice->get_currency();
309
		}
310
		return wpinv_get_currency();
311
    }
312
313
    /**
314
	 * Returns the raw submission data.
315
	 *
316
	 * @since 1.0.19
317
	 * @return array
318
	 */
319
	public function get_data() {
320
		return $this->data;
321
	}
322
323
	///////// Items //////////////
324
325
	/**
326
	 * Adds an item to the submission.
327
	 *
328
	 * @since 1.0.19
329
	 * @param GetPaid_Form_Item $item
330
	 */
331
	public function add_item( $item ) {
332
333
		// Make sure that it is available for purchase.
334
		if ( ! $item->can_purchase() ) {
335
			return;
336
		}
337
338
		// Do we have a recurring item?
339
		if ( $item->is_recurring() ) {
340
			$this->has_recurring = true;
341
		}
342
343
		$this->items[ $item->get_id() ] = $item;
344
345
		$this->subtotal_amount += $item->get_price() * $item->get_qantity();
346
347
		$this->process_item_discount( $item );
348
349
		$this->process_item_tax( $item );
350
	}
351
352
	/**
353
	 * Retrieves a specific item.
354
	 *
355
	 * @since 1.0.19
356
	 */
357
	public function get_item( $item_id ) {
358
		return isset( $this->items[ $item_id ] ) ? $this->items[ $item_id ] : null;
359
	}
360
361
	/**
362
	 * Returns all items.
363
	 *
364
	 * @since 1.0.19
365
	 * @return GetPaid_Form_Item[]
366
	 */
367
	public function get_items() {
368
		return $this->items;
369
	}
370
371
	///////// TAXES //////////////
372
373
	/**
374
	 * Adds a tax to the submission.
375
	 *
376
	 * @since 1.0.19
377
	 */
378
	public function add_tax( $name, $amount ) {
379
		$amount = (float) wpinv_sanitize_amount( $amount );
380
381
		$this->total_tax_amount += $amount;
382
383
		if ( isset( $this->taxes[ $name ] ) ) {
384
			$this->taxes[ $name ] += $amount;
385
		} else {
386
			$this->taxes[ $name ] = $amount;
387
		}
388
389
	}
390
391
	/**
392
	 * Whether or not we'll use taxes for the submission.
393
	 *
394
	 * @since 1.0.19
395
	 */
396
	public function use_taxes() {
397
398
		$use_taxes = wpinv_use_taxes();
399
400
		if ( $this->has_invoice() && $this->invoice->disable_taxes ) {
401
			$use_taxes = false;
402
		}
403
404
		return apply_filters( 'getpaid_submission_use_taxes', $use_taxes, $this );
405
406
	}
407
408
	/**
409
	 * Maybe process tax.
410
	 *
411
	 * @since 1.0.19 
412
	 * @param GetPaid_Form_Item $item
413
	 */
414
	public function process_item_tax( $item ) {
415
416
		// Abort early if we're not using taxes.
417
		if ( ! $this->use_taxes() ) {
418
			return;
419
		}
420
421
		$rate  = wpinv_get_tax_rate( $this->country, $this->state, $item->get_id() );
422
		$price = $item->get_sub_total();
423
424
		if ( wpinv_prices_include_tax() ) {
425
			$item_tax = $price - ( $price - $price * $rate * 0.01 );
426
		} else {
427
			$item_tax = $price * $rate * 0.01;
428
		}
429
430
		$this->add_tax( 'Tax', $item_tax );
431
432
	}
433
434
	/**
435
	 * Returns the total tax amount.
436
	 *
437
	 * @since 1.0.19
438
	 */
439
	public function get_total_tax() {
440
		return $this->total_tax_amount;
441
	}
442
443
	/**
444
	 * Retrieves a specific tax.
445
	 *
446
	 * @since 1.0.19
447
	 */
448
	public function get_tax( $name ) {
449
		return isset( $this->taxes[ $name ] ) ? $this->taxes[ $name ] : 0;
450
	}
451
452
	/**
453
	 * Returns all taxes.
454
	 *
455
	 * @since 1.0.19
456
	 */
457
	public function get_taxes() {
458
		return $this->taxes;
459
	}
460
461
	///////// DISCOUNTS //////////////
462
463
	/**
464
	 * Adds a discount to the submission.
465
	 *
466
	 * @since 1.0.19
467
	 */
468
	public function add_discount( $name, $amount ) {
469
		$amount = wpinv_sanitize_amount( $amount );
470
471
		$this->total_discount_amount += $amount;
472
473
		if ( isset( $this->discounts[ $name ] ) ) {
474
			$this->discounts[ $name ] += $amount;
475
		} else {
476
			$this->discounts[ $name ] = $amount;
477
		}
478
479
	}
480
481
	/**
482
	 * Removes a discount from the submission.
483
	 *
484
	 * @since 1.0.19
485
	 */
486
	public function remove_discount( $name ) {
487
488
		if ( isset( $this->discounts[ $name ] ) ) {
489
			$this->total_discount_amount -= $this->discounts[ $name ];
490
			unset( $this->discounts[ $name ] );
491
		}
492
493
	}
494
495
	/**
496
	 * Checks whether there is a discount code associated with this submission.
497
	 *
498
	 * @since 1.0.19
499
	 * @return bool
500
	 */
501
	public function has_discount_code() {
502
		return ! empty( $this->discount );
503
	}
504
505
	/**
506
	 * Returns the discount code.
507
	 *
508
	 * @since 1.0.19
509
	 * @return bool
510
	 */
511
	public function get_discount_code() {
512
		return $this->has_discount_code() ? $this->discount->code : '';
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->has_discou...is->discount->code : '' returns the type string which is incompatible with the documented return type boolean.
Loading history...
513
	}
514
515
	/**
516
	 * Prepares an item discount.
517
	 *
518
	 * @since 1.0.19
519
	 */
520
	public function maybe_prepare_discount() {
521
522
		// Do we have a discount?
523
		if ( empty( $this->data['discount'] ) ) {
524
			return;
525
		}
526
527
		// Fetch the discount.
528
		$discount = wpinv_get_discount_obj( $this->data['discount'] );
529
530
		// Ensure it is active.
531
        if ( ! $discount->exists() || ! $discount->is_active() || ! $discount->has_started() || $discount->is_expired() ) {
532
			$this->is_discount_valid = false;
0 ignored issues
show
Documentation Bug introduced by
The property $is_discount_valid was declared of type string, but false is of type false. Maybe add a type cast?

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

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

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
533
			$this->last_error = __( 'Invalid or expired discount code', 'invoicing' );
534
			return;
535
		}
536
537
		// For single use discounts...
538
		if ( $discount->is_single_use ) {
539
540
			if ( ! $this->has_billing_email() ) {
541
				$this->is_discount_valid = false;
542
				$this->last_error = __( 'You need to enter your billing email before applying this discount', 'invoicing' );
543
				return;
544
			}
545
546
			if ( ! $discount->is_valid_for_user( $this->get_billing_email() ) ) {
547
				$this->is_discount_valid = false;
548
				$this->last_error = __( 'You have already used this discount', 'invoicing' );
549
				return;
550
			}
551
		}
552
553
		// Set the discount.
554
		$this->discount = $discount;
555
556
	}
557
558
	/**
559
	 * Removes an invalid discount code.
560
	 *
561
	 * @since 1.0.19
562
	 */
563
	public function maybe_remove_discount() {
564
565
		// Do we have a discount?
566
		if ( empty( $this->has_discount_code() ) ) {
567
			return;
568
		}
569
570
		// Fetch the discount amount.
571
		$amount = $this->get_discount( 'Discount' );
572
573
		// Abort early if this is a "zero" discount.
574
		if ( empty( $amount ) ) {
575
			return;
576
		}
577
578
		$total = $this->subtotal_amount + $this->get_total_fees() + $this->get_total_tax();
579
580
		if ( ! $this->discount->is_minimum_amount_met( $total ) ) {
581
			$this->is_discount_valid = false;
0 ignored issues
show
Documentation Bug introduced by
The property $is_discount_valid was declared of type string, but false is of type false. Maybe add a type cast?

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

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

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
582
            $min = wpinv_price( wpinv_format_amount( $$this->discount->min_total ) );
583
			$this->last_error = sprintf( __( 'The minimum total for using this discount is %s', 'invoicing' ), $min );
584
        }
585
586
        if ( ! $$this->discount->is_maximum_amount_met( $total ) ) {
587
			$this->is_discount_valid = false;
588
            $max = wpinv_price( wpinv_format_amount( $$this->discount->max_total ) );
589
			$this->last_error = sprintf( __( 'The maximum total for using this discount is %s', 'invoicing' ), $max );
590
		}
591
592
		if ( ! $this->is_discount_valid ) {
593
			$this->discount = null;
594
			$this->remove_discount( 'Discount' );
595
		}
596
597
    }
598
599
	/**
600
	 * Maybe process discount.
601
	 *
602
	 * @since 1.0.19
603
	 * @param GetPaid_Form_Item $item
604
	 */
605
	public function process_item_discount( $item ) {
606
607
		// Abort early if there is no discount.
608
		if ( ! $this->has_discount_code() ) {
609
			return;
610
		}
611
612
		// Ensure that it is valid for this item.
613
		if ( ! $this->discount->is_valid_for_items( array( $item->get_id() ) ) ) {
614
			return;
615
		}
616
617
		// Fetch the discounted amount.
618
		$discount = $this->discount->get_discounted_amount( $item->get_price() * $item->get_qantity() );
619
620
		$this->add_discount( 'Discount', $discount );
621
622
	}
623
624
	/**
625
	 * Returns the total discount amount.
626
	 *
627
	 * @since 1.0.19
628
	 */
629
	public function get_total_discount() {
630
		return $this->total_discount_amount;
631
	}
632
633
	/**
634
	 * Gets a specific discount.
635
	 *
636
	 * @since 1.0.19
637
	 */
638
	public function get_discount( $name ) {
639
		return isset( $this->discounts[ $name ] ) ? $this->discounts[ $name ] : 0;
640
	}
641
642
	/**
643
	 * Returns all discounts.
644
	 *
645
	 * @since 1.0.19
646
	 */
647
	public function get_discounts() {
648
		return $this->discounts;
649
	}
650
651
	///////// FEES //////////////
652
653
	/**
654
	 * Adds a fee to the submission.
655
	 *
656
	 * @since 1.0.19
657
	 */
658
	public function add_fee( $name, $amount ) {
659
		$amount = wpinv_sanitize_amount( $amount );
660
661
		$this->total_fees_amount += $amount;
662
663
		if ( isset( $this->fees[ $name ] ) ) {
664
			$this->fees[ $name ] += $amount;
665
		} else {
666
			$this->fees[ $name ] = $amount;
667
		}
668
669
	}
670
671
	/**
672
	 * Returns the total fees amount.
673
	 *
674
	 * @since 1.0.19
675
	 */
676
	public function get_total_fees() {
677
		return $this->total_fees_amount;
678
	}
679
680
	/**
681
	 * Retrieves a specific fee.
682
	 *
683
	 * @since 1.0.19
684
	 */
685
	public function get_fee( $name ) {
686
		return isset( $this->fees[ $name ] ) ? $this->fees[ $name ] : 0;
687
	}
688
689
	/**
690
	 * Returns all fees.
691
	 *
692
	 * @since 1.0.19
693
	 */
694
	public function get_fees() {
695
		return $this->fees;
696
	}
697
698
	// MISC //
699
700
	/**
701
	 * Returns the total amount to collect for this submission.
702
	 *
703
	 * @since 1.0.19
704
	 */
705
	public function get_total() {
706
		$total = $this->subtotal_amount + $this->get_total_fees() - $this->get_total_discount() + $this->get_total_tax();
707
		$total = apply_filters( 'getpaid_get_submission_total_amount', $total, $this  );
708
		return wpinv_sanitize_amount( $total );
709
	}
710
711
	/**
712
	 * Whether payment details should be collected for this submission.
713
	 *
714
	 * @since 1.0.19
715
	 */
716
	public function get_payment_details() {
717
		$collect = $this->subtotal_amount + $this->get_total_fees() - $this->get_total_discount() + $this->get_total_tax();
718
719
		if ( $this->has_recurring ) {
720
			$collect = true;
721
		}
722
723
		$collect = apply_filters( 'getpaid_submission_collect_payment_details', $collect, $this  );
724
		return $collect;
725
	}
726
727
	/**
728
	 * Returns the billing email of the user.
729
	 *
730
	 * @since 1.0.19
731
	 */
732
	public function get_billing_email() {
733
		$billing_email = empty( $this->data['billing_email'] ) ? '' : $this->data['billing_email'];
734
		return apply_filters( 'getpaid_get_submission_billing_email', $billing_email, $this  );
735
	}
736
737
	/**
738
	 * Checks if the submitter has a billing email.
739
	 *
740
	 * @since 1.0.19
741
	 */
742
	public function has_billing_email() {
743
		$billing_email = $this->get_billing_email();
744
		return ! empty( $billing_email );
745
	}
746
747
}
748