Test Failed
Push — master ( 2ef8f2...f7ad86 )
by Maximo
04:50 queued 01:35
created

Billable::subscribedToPlan()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 15
c 0
b 0
f 0
rs 9.6111
cc 5
nc 4
nop 2
1
<?php
2
3
namespace Phalcon\Cashier;
4
5
use Phalcon\Di\FactoryDefault;
0 ignored issues
show
Bug introduced by
The type Phalcon\Di\FactoryDefault 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...
6
use Exception;
7
use Carbon\Carbon;
8
use InvalidArgumentException;
9
use Stripe\Token as StripeToken;
10
use Stripe\Charge as StripeCharge;
11
use Stripe\Refund as StripeRefund;
12
use Stripe\Invoice as StripeInvoice;
13
use Stripe\Customer as StripeCustomer;
14
use Stripe\InvoiceItem as StripeInvoiceItem;
15
use Stripe\Error\InvalidRequest as StripeErrorInvalidRequest;
16
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\HttpKe...n\NotFoundHttpException 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...
17
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\HttpKe...cessDeniedHttpException 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...
18
use Phalcon\Mvc\Model;
19
use Baka\Auth\Models\Apps;
20
21
trait Billable
22
{
23
    /**
24
     * The Stripe API key.
25
     *
26
     * @var string
27
     */
28
    protected static $stripeKey;
29
30
    /**
31
     * Make a "one off" charge on the customer for the given amount.
32
     *
33
     * @param  int   $amount
34
     * @param  array $options
35
     * @return \Stripe\Charge
36
     *
37
     * @throws \Stripe\Error\Card
38
     */
39
    public function charge($amount, array $options = [])
40
    {
41
        $options = array_merge([
42
            'currency' => $this->preferredCurrency(),
43
        ], $options);
44
45
        $options['amount'] = $amount;
46
        if (!array_key_exists('source', $options) && $this->stripe_id) {
47
            $options['customer'] = $this->stripe_id;
48
        }
49
        if (!array_key_exists('source', $options) && !array_key_exists('customer', $options)) {
50
            throw new InvalidArgumentException('No payment source provided.');
51
        }
52
        return StripeCharge::create($options, ['api_key' => $this->getStripeKey()]);
53
    }
54
55
    /**
56
     * Refund a customer for a charge.
57
     *
58
     * @param  string $charge
59
     * @param  array  $options
60
     * @return \Stripe\Charge
61
     *
62
     * @throws \Stripe\Error\Refund
63
     */
64
    public function refund($charge, array $options = [])
65
    {
66
        $options['charge'] = $charge;
67
68
        return StripeRefund::create($options, ['api_key' => $this->getStripeKey()]);
69
    }
70
71
    /**
72
     * Determines if the customer currently has a card on file.
73
     *
74
     * @return bool
75
     */
76
    public function hasCardOnFile()
77
    {
78
        return (bool)$this->card_brand;
79
    }
80
81
    /**
82
     * Add an invoice item to the customer's upcoming invoice.
83
     *
84
     * @param  string  $description
85
     * @param  int  $amount
86
     * @param  array  $options
87
     * @return \Stripe\InvoiceItem
88
     *
89
     * @throws \InvalidArgumentException
90
     */
91
    public function tab($description, $amount, array $options = [])
92
    {
93
        if (!$this->stripe_id) {
94
            throw new InvalidArgumentException(class_basename($this) . ' is not a Stripe customer. See the createAsStripeCustomer method.');
95
        }
96
        $options = array_merge([
97
            'customer' => $this->stripe_id,
98
            'amount' => $amount,
99
            'currency' => $this->preferredCurrency(),
100
            'description' => $description,
101
        ], $options);
102
103
        return StripeInvoiceItem::create(
104
            $options,
105
            ['api_key' => $this->getStripeKey()]
106
        );
107
    }
108
109
    /**
110
     * Invoice the customer for the given amount and generate an invoice immediately.
111
     *
112
     * @param  string  $description
113
     * @param  int  $amount
114
     * @param  array  $options
115
     * @return \Laravel\Cashier\Invoice|bool
116
     */
117
    public function invoiceFor($description, $amount, array $options = [])
118
    {
119
        $this->tab($description, $amount, $options);
120
        return $this->invoice();
121
    }
122
123
    /**
124
     * Begin creating a new subscription.
125
     *
126
     * @param string $subscription
127
     * @param string $plan
128
     */
129
    public function newSubscription($subscription, $plan, Model $company, Apps $apps)
130
    {
131
        return new SubscriptionBuilder($this, $subscription, $plan, $company, $apps);
132
    }
133
134
    /**
135
     * Determine if the user is on trial.
136
     *
137
     * @param  string      $subscription
138
     * @param  string|null $plan
139
     * @return bool
140
     */
141
    public function onTrial($subscription = 'default', $plan = null)
142
    {
143
        if (func_num_args() === 0 && $this->onGenericTrial()) {
144
            return true;
145
        }
146
147
        $subscription = $this->subscription($subscription);
148
149
        if (is_null($plan)) {
150
            return $subscription && $subscription->onTrial();
151
        }
152
153
        return $subscription && $subscription->onTrial() &&
154
        $subscription->stripe_plan === $plan;
155
    }
156
157
    /**
158
     * Determine if the user is on a "generic" trial at the user level.
159
     *
160
     * @return bool
161
     */
162
    public function onGenericTrial()
163
    {
164
        $trialEndsAt = new \DateTime($this->trial_ends_at);
165
166
        return $this->trial_ends_at && Carbon::now()->lt(Carbon::instance($trialEndsAt));
167
    }
168
169
    /**
170
     * Determine if the user has a given subscription.
171
     *
172
     * @param  string      $subscription
173
     * @param  string|null $plan
174
     * @return bool
175
     */
176
    public function subscribed($subscription = 'default', $plan = null)
177
    {
178
        $subscription = $this->subscription($subscription);
179
180
        if (is_null($subscription)) {
181
            return false;
182
        }
183
184
        if (is_null($plan)) {
185
            return $subscription->valid();
186
        }
187
188
        return $subscription->valid() && $subscription->stripe_plan === $plan;
189
    }
190
191
    /**
192
     * Get a subscription instance by name.
193
     *
194
     * @param string $subscription
195
     */
196
    public function subscription($subscription = 'default')
197
    {
198
        $subscriptions = $this->subscriptions();
199
200
        foreach ($subscriptions as $object) {
201
            if ($object->name === $subscription) {
202
                return $object;
203
            }
204
        }
205
        return null;
206
    }
207
208
    /**
209
     * Get all of the subscriptions for the user.
210
     */
211
    public function subscriptions()
212
    {
213
        $this->hasMany(
214
            'id',
215
            Subscription::class,
216
            'user_id',
217
            [
218
                'alias' => 'subscriptions',
219
                'params' => ['order' => 'id DESC']
220
            ]
221
        );
222
        return $this->getRelated('subscriptions');
223
    }
224
225
    /**
226
     * Invoice the billable entity outside of regular billing cycle.
227
     *
228
     * @return StripeInvoice|bool
229
     */
230
    public function invoice()
231
    {
232
        if ($this->stripe_id) {
233
            try {
234
                return StripeInvoice::create(['customer' => $this->stripe_id], $this->getStripeKey())->pay();
235
            } catch (StripeErrorInvalidRequest $e) {
236
                return false;
237
            }
238
        }
239
240
        return true;
241
    }
242
243
    /**
244
     * Get the entity's upcoming invoice.
245
     */
246
    public function upcomingInvoice()
247
    {
248
        try {
249
            $stripeInvoice = StripeInvoice::upcoming(
250
                ['customer' => $this->stripe_id],
251
                ['api_key' => $this->getStripeKey()]
252
            );
253
254
            return new Invoice($this, $stripeInvoice);
255
        } catch (StripeErrorInvalidRequest $e) {
256
            //
257
        }
258
    }
259
260
    /**
261
     * Find an invoice by ID.
262
     *
263
     * @param string $id
264
     */
265
    public function findInvoice($id)
266
    {
267
        try {
268
            $stripeInvoice = StripeInvoice::retrieve($id, $this->getStripeKey());
269
270
            $stripeInvoice->lines = StripeInvoice::retrieve($id, $this->getStripeKey())
271
                        ->lines
272
                        ->all(['limit' => 1000]);
273
            return new Invoice($this, $stripeInvoice);
274
        } catch (Exception $e) {
275
            //
276
        }
277
    }
278
279
    /**
280
     * Find an invoice or throw a 404 error.
281
     *
282
     * @param string $id
283
     */
284
    public function findInvoiceOrFail($id)
285
    {
286
        $invoice = $this->findInvoice($id);
287
288
        if (is_null($invoice)) {
289
            throw new NotFoundHttpException;
290
        }
291
292
        if ($invoice->customer !== $this->stripe_id) {
293
            throw new AccessDeniedHttpException;
294
        }
295
296
        return $invoice;
297
    }
298
299
    /**
300
     * Create an invoice download Response.
301
     *
302
     * @param string $id
303
     * @param array  $data
304
     * @param string $storagePath
305
     * @todo
306
     */
307
    public function downloadInvoice($id, array $data, $storagePath = null)
308
    {
309
    }
310
311
    /**
312
     * Get a collection of the entity's invoices.
313
     *
314
     * @param bool  $includePending
315
     * @param array $parameters
316
     */
317
    public function invoices($includePending = false, $parameters = [])
318
    {
319
        $invoices = [];
320
        $parameters = array_merge(['limit' => 24], $parameters);
321
        $stripeInvoices = $this->asStripeCustomer()->invoices($parameters);
322
323
        // Here we will loop through the Stripe invoices and create our own custom Invoice
324
        // instances that have more helper methods and are generally more convenient to
325
        // work with than the plain Stripe objects are. Then, we'll return the array.
326
        if (!is_null($stripeInvoices)) {
327
            foreach ($stripeInvoices->data as $invoice) {
328
                if ($invoice->paid || $includePending) {
329
                    $invoices[] = new Invoice($this, $invoice);
330
                }
331
            }
332
        }
333
        return $invoices;
334
    }
335
336
    /**
337
     * Get an array of the entity's invoices.
338
     *
339
     * @param array $parameters
340
     */
341
    public function invoicesIncludingPending(array $parameters = [])
342
    {
343
        return $this->invoices(true, $parameters);
344
    }
345
346
    /**
347
    * Get a collection of the entity's cards.
348
    *
349
    * @param  array  $parameters
350
    * @return array
351
    */
352
    public function cards($parameters = [])
353
    {
354
        $cards = [];
355
        $parameters = array_merge(['limit' => 24], $parameters);
356
        $stripeCards = $this->asStripeCustomer()->sources->all(
357
            ['object' => 'card'] + $parameters
358
        );
359
360
        if (!is_null($stripeCards)) {
361
            foreach ($stripeCards->data as $card) {
362
                $cards[] = new Card($this, $card);
363
            }
364
        }
365
366
        return $cards;
367
    }
368
369
    /**
370
     * Get the default card for the entity.
371
     *
372
     * @return \Stripe\Card|null
373
     */
374
    public function defaultCard()
375
    {
376
        $customer = $this->asStripeCustomer();
377
        foreach ($customer->sources->data as $card) {
378
            if ($card->id === $customer->default_source) {
379
                return $card;
380
            }
381
        }
382
    }
383
384
    /**
385
     * Update customer's credit card.
386
     *
387
     * @param  string $token
388
     * @return void
389
     */
390
    public function updateCard($token)
391
    {
392
        $customer = $this->asStripeCustomer();
393
394
        $token = StripeToken::retrieve($token, ['api_key' => $this->getStripeKey()]);
395
396
        // If the given token already has the card as their default source, we can just
397
        // bail out of the method now. We don't need to keep adding the same card to
398
        // the user's account each time we go through this particular method call.
399
        if ($token->card->id === $customer->default_source) {
400
            return;
401
        }
402
403
        $card = $customer->sources->create(['source' => $token]);
404
405
        $customer->default_source = $card->id;
406
407
        $customer->save();
408
409
        // Next, we will get the default source for this user so we can update the last
410
        // four digits and the card brand on this user record in the database, which
411
        // is convenient when displaying on the front-end when updating the cards.
412
        $source = $customer->default_source
413
            ? $customer->sources->retrieve($customer->default_source)
414
            : null;
415
416
        $this->fillCardDetails($source);
417
418
        $this->save();
419
    }
420
421
    /**
422
     * Synchronises the customer's card from Stripe back into the database.
423
     *
424
     * @return $this
425
     */
426
    public function updateCardFromStripe()
427
    {
428
        $defaultCard = $this->defaultCard();
429
        if ($defaultCard) {
430
            $this->fillCardDetails($defaultCard)->save();
431
        } else {
432
            $this->card_brand = null;
433
            $this->card_last_four = null;
434
            $this->update();
435
        }
436
        return $this;
437
    }
438
439
    /**
440
     * Fills the model's properties with the source from Stripe.
441
     *
442
     * @param  \Stripe\Card|\Stripe\BankAccount|null  $card
443
     * @return $this
444
     */
445
    protected function fillCardDetails($card)
446
    {
447
        if ($card instanceof StripeCard) {
448
            $this->card_brand = $card->brand;
449
            $this->card_last_four = $card->last4;
450
        } elseif ($card instanceof StripeBankAccount) {
451
            $this->card_brand = 'Bank Account';
452
            $this->card_last_four = $card->last4;
453
        }
454
        return $this;
455
    }
456
457
    /**
458
    * Deletes the entity's cards.
459
    *
460
    * @return void
461
    */
462
    public function deleteCards()
463
    {
464
        foreach ($this->cards() as $card) {
465
            $card->delete();
466
        }
467
468
        $this->updateCardFromStripe();
469
    }
470
471
    /**
472
     * Apply a coupon to the billable entity.
473
     *
474
     * @param  string $coupon
475
     * @return void
476
     */
477
    public function applyCoupon($coupon)
478
    {
479
        $customer = $this->asStripeCustomer();
480
481
        $customer->coupon = $coupon;
482
483
        $customer->save();
484
    }
485
486
    /**
487
     * Determine if the user is actively subscribed to one of the given plans.
488
     *
489
     * @param  array|string $plans
490
     * @param  string       $subscription
491
     * @return bool
492
     */
493
    public function subscribedToPlan($plans, $subscription = 'default')
494
    {
495
        $subscription = $this->subscription($subscription);
496
497
        if (!$subscription || !$subscription->valid()) {
498
            return false;
499
        }
500
501
        foreach ((array) $plans as $plan) {
502
            if ($subscription->stripe_plan === $plan) {
503
                return true;
504
            }
505
        }
506
507
        return false;
508
    }
509
510
    /**
511
     * Determine if the entity is on the given plan.
512
     *
513
     * @param  string $plan
514
     * @return bool
515
     */
516
    public function onPlan($plan)
517
    {
518
        return !is_null(
519
            $this->subscriptions->first(
520
                function ($key, $value) use ($plan) {
521
                    return $value->stripe_plan === $plan && $value->valid();
522
                }
523
            )
524
        );
525
    }
526
527
    /**
528
     * Determine if the entity has a Stripe customer ID.
529
     *
530
     * @return bool
531
     */
532
    public function hasStripeId()
533
    {
534
        return !is_null($this->stripe_id);
535
    }
536
537
    /**
538
     * Create a Stripe customer for the given user.
539
     *
540
     * @param  string $token
541
     * @param  array  $options
542
     * @return StripeCustomer
543
     */
544
    public function createAsStripeCustomer($token, array $options = [])
545
    {
546
        $options = array_key_exists('email', $options)
547
            ? $options : array_merge($options, ['email' => $this->email]);
548
549
        // Here we will create the customer instance on Stripe and store the ID of the
550
        // user from Stripe. This ID will correspond with the Stripe user instances
551
        // and allow us to retrieve users from Stripe later when we need to work.
552
        $customer = StripeCustomer::create(
553
            $options,
554
            $this->getStripeKey()
555
        );
556
557
        $this->stripe_id = $customer->id;
558
559
        $this->save();
560
561
        // Next we will add the credit card to the user's account on Stripe using this
562
        // token that was provided to this method. This will allow us to bill users
563
        // when they subscribe to plans or we need to do one-off charges on them.
564
        if (!is_null($token)) {
565
            $this->updateCard($token);
566
        }
567
568
        return $customer;
569
    }
570
571
    /**
572
     * Get the Stripe customer for the user.
573
     *
574
     * @return \Stripe\Customer
575
     */
576
    public function asStripeCustomer()
577
    {
578
        return StripeCustomer::retrieve($this->stripe_id, $this->getStripeKey());
579
    }
580
581
    /**
582
     * Get the Stripe supported currency used by the entity.
583
     *
584
     * @return string
585
     */
586
    public function preferredCurrency()
587
    {
588
        return Cashier::usesCurrency();
589
    }
590
591
    /**
592
     * Get the tax percentage to apply to the subscription.
593
     *
594
     * @return int
595
     */
596
    public function taxPercentage()
597
    {
598
        return 0;
599
    }
600
601
    /**
602
     * Get the Stripe API key.
603
     *
604
     * @return string
605
     */
606
    public static function getStripeKey()
607
    {
608
        if (static::$stripeKey) {
609
            return static::$stripeKey;
610
        }
611
        $di = FactoryDefault::getDefault();
612
        $stripe = $di->getConfig()->stripe;
613
614
        return $stripe->secretKey ?: getenv('STRIPE_SECRET');
615
    }
616
617
    /**
618
     * Set the Stripe API key.
619
     *
620
     * @param  string $key
621
     * @return void
622
     */
623
    public static function setStripeKey($key)
624
    {
625
        static::$stripeKey = $key;
626
    }
627
628
    /**
629
     * @link https://stripe.com/docs/api/php#create_card_token
630
     * @param $option
631
     * @return bool
632
     */
633
    public function createCardToken($option)
634
    {
635
        $object = StripToken::create($option, ['api_key' => $this->getStripeKey()]);
636
        if (is_object($object)) {
637
            $token = $object->__toArray(true);
638
            return $token['id'] ?: false;
639
        }
640
        return false;
641
    }
642
643
    /**
644
     * Update default payment method with new card
645
     * @param string $customerId
646
     * @param string $token
647
     * @return StripeCustomer
648
     */
649
    public function updatePaymentMethod(string $customerId, string $token)
650
    {
651
        $customer = StripeCustomer::update($customerId, ['source' => $token], $this->getStripeKey());
652
653
        if (is_object($customer)) {
654
            return $customer;
655
        }
656
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type Stripe\Customer.
Loading history...
657
    }
658
659
    /**
660
     * Create a new Invoice Item
661
     * @param array $data Stripe Invoice Item data
662
     */
663
    public function createInvoiceItem(array $data)
664
    {
665
        $invoiceItem = StripeInvoiceItem::create($data, $this->getStripeKey());
666
667
        if (is_object($invoiceItem)) {
668
            return $invoiceItem;
669
        }
670
671
        return false;
672
    }
673
674
    /**
675
     * Create and send new Invoice to a customer
676
     * @param string $customerId Stripe customer id
677
     * @param array $options
678
     */
679
    public function sendNewInvoice(string $customerId, array $options)
680
    {
681
        $invoice = StripeInvoice::create([
682
            'customer' => $customerId,
683
            'billing' => isset($options['billing']) ? $options['billing'] : 'send_invoice',
684
            'days_until_due' => isset($options['days_until_due']) ? $options['days_until_due'] : 30,
685
        ], $this->getStripeKey());
686
687
        if (is_object($invoice)) {
688
            //Send invoice email to user
689
            if ($invoice->sendInvoice()) {
690
                return $invoice;
691
            }
692
        }
693
694
        return false;
695
    }
696
}
697