Test Failed
Branch add-migration (b62a72)
by Rafael
07:00
created

src/Billable.php (2 issues)

1
<?php
2
3
namespace Phalcon\Cashier;
4
5
use Phalcon\Di\FactoryDefault;
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;
17
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
18
use Phalcon\Mvc\Model;
19
use Baka\Database\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) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
256
        }
257
    }
258
259
    /**
260
     * Find an invoice by ID.
261
     *
262
     * @param string $id
263
     */
264
    public function findInvoice($id)
265
    {
266
        try {
267
            $stripeInvoice = StripeInvoice::retrieve($id, $this->getStripeKey());
268
269
            $stripeInvoice->lines = StripeInvoice::retrieve($id, $this->getStripeKey())
270
                        ->lines
271
                        ->all(['limit' => 1000]);
272
            return new Invoice($this, $stripeInvoice);
273
        } catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
274
        }
275
    }
276
277
    /**
278
     * Find an invoice or throw a 404 error.
279
     *
280
     * @param string $id
281
     */
282
    public function findInvoiceOrFail($id)
283
    {
284
        $invoice = $this->findInvoice($id);
285
286
        if (is_null($invoice)) {
287
            throw new NotFoundHttpException;
288
        }
289
290
        if ($invoice->customer !== $this->stripe_id) {
291
            throw new AccessDeniedHttpException;
292
        }
293
294
        return $invoice;
295
    }
296
297
    /**
298
     * Create an invoice download Response.
299
     *
300
     * @param string $id
301
     * @param array  $data
302
     * @param string $storagePath
303
     * @todo
304
     */
305
    public function downloadInvoice($id, array $data, $storagePath = null)
306
    {
307
    }
308
309
    /**
310
     * Get a collection of the entity's invoices.
311
     *
312
     * @param bool  $includePending
313
     * @param array $parameters
314
     */
315
    public function invoices($includePending = false, $parameters = [])
316
    {
317
        $invoices = [];
318
        $parameters = array_merge(['limit' => 24], $parameters);
319
        $stripeInvoices = $this->asStripeCustomer()->invoices($parameters);
320
321
        // Here we will loop through the Stripe invoices and create our own custom Invoice
322
        // instances that have more helper methods and are generally more convenient to
323
        // work with than the plain Stripe objects are. Then, we'll return the array.
324
        if (!is_null($stripeInvoices)) {
325
            foreach ($stripeInvoices->data as $invoice) {
326
                if ($invoice->paid || $includePending) {
327
                    $invoices[] = new Invoice($this, $invoice);
328
                }
329
            }
330
        }
331
        return $invoices;
332
    }
333
334
    /**
335
     * Get an array of the entity's invoices.
336
     *
337
     * @param array $parameters
338
     */
339
    public function invoicesIncludingPending(array $parameters = [])
340
    {
341
        return $this->invoices(true, $parameters);
342
    }
343
344
    /**
345
    * Get a collection of the entity's cards.
346
    *
347
    * @param  array  $parameters
348
    * @return array
349
    */
350
    public function cards($parameters = [])
351
    {
352
        $cards = [];
353
        $parameters = array_merge(['limit' => 24], $parameters);
354
        $stripeCards = $this->asStripeCustomer()->sources->all(
355
            ['object' => 'card'] + $parameters
356
        );
357
358
        if (!is_null($stripeCards)) {
359
            foreach ($stripeCards->data as $card) {
360
                $cards[] = new Card($this, $card);
361
            }
362
        }
363
364
        return $cards;
365
    }
366
367
    /**
368
     * Get the default card for the entity.
369
     *
370
     * @return \Stripe\Card|null
371
     */
372
    public function defaultCard()
373
    {
374
        $customer = $this->asStripeCustomer();
375
        foreach ($customer->sources->data as $card) {
376
            if ($card->id === $customer->default_source) {
377
                return $card;
378
            }
379
        }
380
    }
381
382
    /**
383
     * Update customer's credit card.
384
     *
385
     * @param  string $token
386
     * @return void
387
     */
388
    public function updateCard($token)
389
    {
390
        $customer = $this->asStripeCustomer();
391
392
        $token = StripeToken::retrieve($token, ['api_key' => $this->getStripeKey()]);
393
394
        // If the given token already has the card as their default source, we can just
395
        // bail out of the method now. We don't need to keep adding the same card to
396
        // the user's account each time we go through this particular method call.
397
        if ($token->card->id === $customer->default_source) {
398
            return;
399
        }
400
401
        $card = $customer->sources->create(['source' => $token]);
402
403
        $customer->default_source = $card->id;
404
405
        $customer->save();
406
407
        // Next, we will get the default source for this user so we can update the last
408
        // four digits and the card brand on this user record in the database, which
409
        // is convenient when displaying on the front-end when updating the cards.
410
        $source = $customer->default_source
411
            ? $customer->sources->retrieve($customer->default_source)
412
            : null;
413
414
        $this->fillCardDetails($source);
415
416
        $this->save();
417
    }
418
419
    /**
420
     * Synchronises the customer's card from Stripe back into the database.
421
     *
422
     * @return $this
423
     */
424
    public function updateCardFromStripe()
425
    {
426
        $defaultCard = $this->defaultCard();
427
        if ($defaultCard) {
428
            $this->fillCardDetails($defaultCard)->save();
429
        } else {
430
            $this->card_brand = null;
431
            $this->card_last_four = null;
432
            $this->update();
433
        }
434
        return $this;
435
    }
436
437
    /**
438
     * Fills the model's properties with the source from Stripe.
439
     *
440
     * @param  \Stripe\Card|\Stripe\BankAccount|null  $card
441
     * @return $this
442
     */
443
    protected function fillCardDetails($card)
444
    {
445
        if ($card instanceof StripeCard) {
446
            $this->card_brand = $card->brand;
447
            $this->card_last_four = $card->last4;
448
        } elseif ($card instanceof StripeBankAccount) {
449
            $this->card_brand = 'Bank Account';
450
            $this->card_last_four = $card->last4;
451
        }
452
        return $this;
453
    }
454
455
    /**
456
    * Deletes the entity's cards.
457
    *
458
    * @return void
459
    */
460
    public function deleteCards()
461
    {
462
        foreach ($this->cards() as $card) {
463
            $card->delete();
464
        }
465
466
        $this->updateCardFromStripe();
467
    }
468
469
    /**
470
     * Apply a coupon to the billable entity.
471
     *
472
     * @param  string $coupon
473
     * @return void
474
     */
475
    public function applyCoupon($coupon)
476
    {
477
        $customer = $this->asStripeCustomer();
478
479
        $customer->coupon = $coupon;
480
481
        $customer->save();
482
    }
483
484
    /**
485
     * Determine if the user is actively subscribed to one of the given plans.
486
     *
487
     * @param  array|string $plans
488
     * @param  string       $subscription
489
     * @return bool
490
     */
491
    public function subscribedToPlan($plans, $subscription = 'default')
492
    {
493
        $subscription = $this->subscription($subscription);
494
495
        if (!$subscription || !$subscription->valid()) {
496
            return false;
497
        }
498
499
        foreach ((array) $plans as $plan) {
500
            if ($subscription->stripe_plan === $plan) {
501
                return true;
502
            }
503
        }
504
505
        return false;
506
    }
507
508
    /**
509
     * Determine if the entity is on the given plan.
510
     *
511
     * @param  string $plan
512
     * @return bool
513
     */
514
    public function onPlan($plan)
515
    {
516
        return !is_null(
517
            $this->subscriptions->first(
518
                function ($key, $value) use ($plan) {
519
                    return $value->stripe_plan === $plan && $value->valid();
520
                }
521
            )
522
        );
523
    }
524
525
    /**
526
     * Determine if the entity has a Stripe customer ID.
527
     *
528
     * @return bool
529
     */
530
    public function hasStripeId()
531
    {
532
        return !is_null($this->stripe_id);
533
    }
534
535
    /**
536
     * Create a Stripe customer for the given user.
537
     *
538
     * @param  string $token
539
     * @param  array  $options
540
     * @return StripeCustomer
541
     */
542
    public function createAsStripeCustomer($token, array $options = [])
543
    {
544
        $options = array_key_exists('email', $options)
545
            ? $options : array_merge($options, ['email' => $this->email]);
546
547
        // Here we will create the customer instance on Stripe and store the ID of the
548
        // user from Stripe. This ID will correspond with the Stripe user instances
549
        // and allow us to retrieve users from Stripe later when we need to work.
550
        $customer = StripeCustomer::create(
551
            $options,
552
            $this->getStripeKey()
553
        );
554
555
        $this->stripe_id = $customer->id;
556
557
        $this->save();
558
559
        // Next we will add the credit card to the user's account on Stripe using this
560
        // token that was provided to this method. This will allow us to bill users
561
        // when they subscribe to plans or we need to do one-off charges on them.
562
        if (!is_null($token)) {
563
            $this->updateCard($token);
564
        }
565
566
        return $customer;
567
    }
568
569
    /**
570
     * Get the Stripe customer for the user.
571
     *
572
     * @return \Stripe\Customer
573
     */
574
    public function asStripeCustomer()
575
    {
576
        return StripeCustomer::retrieve($this->stripe_id, $this->getStripeKey());
577
    }
578
579
    /**
580
     * Get the Stripe supported currency used by the entity.
581
     *
582
     * @return string
583
     */
584
    public function preferredCurrency()
585
    {
586
        return Cashier::usesCurrency();
587
    }
588
589
    /**
590
     * Get the tax percentage to apply to the subscription.
591
     *
592
     * @return int
593
     */
594
    public function taxPercentage()
595
    {
596
        return 0;
597
    }
598
599
    /**
600
     * Get the Stripe API key.
601
     *
602
     * @return string
603
     */
604
    public static function getStripeKey()
605
    {
606
        if (static::$stripeKey) {
607
            return static::$stripeKey;
608
        }
609
        $di = FactoryDefault::getDefault();
610
        $stripe = $di->getConfig()->stripe;
611
612
        return $stripe->secretKey ?: getenv('STRIPE_SECRET');
613
    }
614
615
    /**
616
     * Set the Stripe API key.
617
     *
618
     * @param  string $key
619
     * @return void
620
     */
621
    public static function setStripeKey($key)
622
    {
623
        static::$stripeKey = $key;
624
    }
625
626
    /**
627
     * @link https://stripe.com/docs/api/php#create_card_token
628
     * @param $option
629
     * @return bool
630
     */
631
    public function createCardToken($option)
632
    {
633
        $object = StripToken::create($option, ['api_key' => $this->getStripeKey()]);
634
        if (is_object($object)) {
635
            $token = $object->__toArray(true);
636
            return $token['id'] ?: false;
637
        }
638
        return false;
639
    }
640
641
    /**
642
     * Update default payment method with new card.
643
     * @param string $customerId
644
     * @param string $token
645
     * @return StripeCustomer
646
     */
647
    public function updatePaymentMethod(string $customerId, string $token)
648
    {
649
        $customer = StripeCustomer::update($customerId, ['source' => $token], $this->getStripeKey());
650
651
        if (is_object($customer)) {
652
            return $customer;
653
        }
654
        return false;
655
    }
656
657
    /**
658
     * Create a new Invoice Item.
659
     * @param array $data Stripe Invoice Item data
660
     */
661
    public function createInvoiceItem(array $data)
662
    {
663
        $invoiceItem = StripeInvoiceItem::create($data, $this->getStripeKey());
664
665
        if (is_object($invoiceItem)) {
666
            return $invoiceItem;
667
        }
668
669
        return false;
670
    }
671
672
    /**
673
     * Create and send new Invoice to a customer.
674
     * @param string $customerId Stripe customer id
675
     * @param array $options
676
     */
677
    public function sendNewInvoice(string $customerId, array $options)
678
    {
679
        $invoice = StripeInvoice::create([
680
            'customer' => $customerId,
681
            'billing' => isset($options['billing']) ? $options['billing'] : 'send_invoice',
682
            'days_until_due' => isset($options['days_until_due']) ? $options['days_until_due'] : 30,
683
        ], $this->getStripeKey());
684
685
        if (is_object($invoice)) {
686
            //Send invoice email to user
687
            if ($invoice->sendInvoice()) {
688
                return $invoice;
689
            }
690
        }
691
692
        return false;
693
    }
694
}
695