Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.0] Payment Methods #696

Merged
merged 1 commit into from
Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/views/emails/confirm_payment.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

{{ __('Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.') }}

@component('mail::button', ['url' => route('cashier.payment', ['id' => $payment->id()])])
@component('mail::button', ['url' => route('cashier.payment', ['id' => $payment->id])])
{{ __('Confirm Payment') }}
@endcomponent

Expand Down
161 changes: 91 additions & 70 deletions src/Billable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

use Exception;
use Stripe\Card as StripeCard;
use Stripe\Token as StripeToken;
use Illuminate\Support\Collection;
use Stripe\Invoice as StripeInvoice;
use Stripe\Customer as StripeCustomer;
use Stripe\BankAccount as StripeBankAccount;
use Stripe\InvoiceItem as StripeInvoiceItem;
use Stripe\Error\Card as StripeCardException;
use Stripe\PaymentIntent as StripePaymentIntent;
use Stripe\PaymentMethod as StripePaymentMethod;
use Laravel\Cashier\Exceptions\InvalidStripeCustomer;
use Stripe\Error\InvalidRequest as StripeErrorInvalidRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Expand Down Expand Up @@ -66,16 +66,6 @@ public function refund($paymentIntent, array $options = [])
return $intent->charges->data[0]->refund($options);
}

/**
* Determines if the customer currently has a card on file.
*
* @return bool
*/
public function hasCardOnFile()
{
return (bool) $this->card_brand;
}

/**
* Add an invoice item to the customer's upcoming invoice.
*
Expand Down Expand Up @@ -353,102 +343,116 @@ public function invoicesIncludingPending(array $parameters = [])
}

/**
* Get a collection of the entity's cards.
* Determines if the customer currently has a payment method.
*
* @return bool
*/
public function hasPaymentMethod()
{
return (bool) $this->card_brand;
}

/**
* Get a collection of the entity's payment methods.
*
* @param array $parameters
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection|\Laravel\Cashier\PaymentMethod[]
*/
public function cards($parameters = [])
public function paymentMethods($parameters = [])
{
$this->assertCustomerExists();

$cards = [];

$parameters = array_merge(['limit' => 24], $parameters);

$stripeCards = $this->asStripeCustomer()->sources->all(
['object' => 'card'] + $parameters
// Note that "type" is temporarily required here by Stripe until
// they've rolled out support for bank account.
$paymentMethods = StripePaymentMethod::all(
['customer' => $this->stripe_id, 'type' => 'card'] + $parameters,
Cashier::stripeOptions()
);

if (! is_null($stripeCards)) {
foreach ($stripeCards->data as $card) {
$cards[] = new Card($this, $card);
}
}

return new Collection($cards);
return collect($paymentMethods->data)->map(function ($paymentMethod) {
return new PaymentMethod($this, $paymentMethod);
});
}

/**
* Get the default card for the entity.
* Get the default payment method for the entity.
*
* @return \Stripe\Card|null
* @return \Laravel\Cashier\PaymentMethod|\Stripe\Card|\Stripe\BankAccount|null
*/
public function defaultCard()
public function defaultPaymentMethod()
{
if (! $this->hasStripeId()) {
return;
}

$customer = $this->asStripeCustomer();
$customer = StripeCustomer::retrieve(
['id' => $this->stripe_id, 'expand' => ['invoice_settings.default_payment_method', 'default_source']],
Cashier::stripeOptions()
);

foreach ($customer->sources->data as $card) {
if ($card->id === $customer->default_source) {
return $card;
}
if ($customer->invoice_settings->default_payment_method) {
return new PaymentMethod($this, $customer->invoice_settings->default_payment_method);
}

// If we can't find a default payment method, we'll try to see if a Card or
// BankAccount source was still set from the legacy Sources implementation.
return $customer->default_source;
}

/**
* Update customer's credit card.
* Update customer's default payment method.
*
* @param string $token
* @param string $paymentMethod
* @return void
*/
public function updateCard($token)
public function updatePaymentMethod($paymentMethod)
{
$this->assertCustomerExists();

$customer = $this->asStripeCustomer();

$token = StripeToken::retrieve($token, Cashier::stripeOptions());
$paymentMethod = StripePaymentMethod::retrieve($paymentMethod, Cashier::stripeOptions());

// If the given token already has the card as their default source, we can just
// bail out of the method now. We don't need to keep adding the same card to
// a model's account every time we go through this particular method call.
if ($token[$token->type]->id === $customer->default_source) {
// If the customer already has the payment method as their default, we can bail out
// of the call now. We don't need to keep adding the same payment method to this
// model's account every single time we go through this specific process call.
if ($paymentMethod->id === $customer->invoice_settings->default_payment_method) {
return;
}

$card = $customer->sources->create(['source' => $token]);
$paymentMethod = $paymentMethod->attach(['customer' => $customer->id], Cashier::stripeOptions());

$customer->default_source = $card->id;
$customer->invoice_settings = ['default_payment_method' => $paymentMethod->id];

$customer->save();
$customer->save(Cashier::stripeOptions());

// Next we will get the default source for this model so we can update the last
// four digits and the card brand on the record in the database. This allows
// us to display the information on the front-end when updating the cards.
$source = $customer->default_source
? $customer->sources->retrieve($customer->default_source)
: null;

$this->fillCardDetails($source);
// Next we'll get the default payment method for this user so we can update
// the payment method details on the record in the database. This allows
// us to show that on the front-end when updating the payment methods.
$this->fillPaymentMethodDetails($paymentMethod);

$this->save();
}

/**
* Synchronises the customer's card from Stripe back into the database.
* Synchronises the customer's payment method from Stripe back into the database.
*
* @return $this
*/
public function updateCardFromStripe()
public function updatePaymentMethodFromStripe()
{
$defaultCard = $this->defaultCard();
$defaultPaymentMethod = $this->defaultPaymentMethod();

if ($defaultCard) {
$this->fillCardDetails($defaultCard)->save();
if ($defaultPaymentMethod) {
if ($defaultPaymentMethod instanceof PaymentMethod) {
$this->fillPaymentMethodDetails(
$defaultPaymentMethod->asStripePaymentMethod()
)->save();
} else {
$this->fillSourceDetails($defaultPaymentMethod)->save();
}
} else {
$this->forceFill([
'card_brand' => null,
Expand All @@ -459,37 +463,54 @@ public function updateCardFromStripe()
return $this;
}

/**
* Fills the model's properties with the payment method from Stripe.
*
* @param \Stripe\PaymentMethod|null $paymentMethod
* @return $this
*/
protected function fillPaymentMethodDetails($paymentMethod)
{
if ($paymentMethod->type === 'card') {
$this->card_brand = $paymentMethod->card->brand;
$this->card_last_four = $paymentMethod->card->last4;
}

return $this;
}

/**
* Fills the model's properties with the source from Stripe.
*
* @param \Stripe\Card|\Stripe\BankAccount|null $card
* @param \Stripe\Card|\Stripe\BankAccount|null $source
* @return $this
* @deprecated Will be removed in a future Cashier update. You should use the new payment methods api instead.
*/
protected function fillCardDetails($card)
protected function fillSourceDetails($source)
{
if ($card instanceof StripeCard) {
$this->card_brand = $card->brand;
$this->card_last_four = $card->last4;
} elseif ($card instanceof StripeBankAccount) {
if ($source instanceof StripeCard) {
$this->card_brand = $source->brand;
$this->card_last_four = $source->last4;
} elseif ($source instanceof StripeBankAccount) {
$this->card_brand = 'Bank Account';
$this->card_last_four = $card->last4;
$this->card_last_four = $source->last4;
}

return $this;
}

/**
* Deletes the entity's cards.
* Deletes the entity's payment methods.
*
* @return void
*/
public function deleteCards()
public function deletePaymentMethods()
{
$this->cards()->each(function ($card) {
$card->delete();
$this->paymentMethods()->each(function (PaymentMethod $paymentMethod) {
$paymentMethod->delete();
});

$this->updateCardFromStripe();
$this->updatePaymentMethodFromStripe();
}

/**
Expand Down Expand Up @@ -610,7 +631,7 @@ public function updateStripeCustomer(array $options = [])
}

/**
* Get the Stripe customer instance for the current user and token.
* Get the Stripe customer instance for the current user or create one.
*
* @param array $options
* @return \Stripe\Customer
Expand Down
66 changes: 0 additions & 66 deletions src/Card.php

This file was deleted.

4 changes: 2 additions & 2 deletions src/Exceptions/PaymentFailure.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ class PaymentFailure extends IncompletePayment
* @param \Laravel\Cashier\Payment $payment
* @return self
*/
public static function cardError(Payment $payment)
public static function invalidPaymentMethod(Payment $payment)
{
return new self(
$payment,
'The payment attempt failed because of a card error.'
'The payment attempt failed because of an invalid payment method.'
);
}
}
17 changes: 1 addition & 16 deletions src/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,22 +135,7 @@ protected function handleCustomerSubscriptionDeleted(array $payload)
protected function handleCustomerUpdated(array $payload)
{
if ($user = $this->getUserByStripeId($payload['data']['object']['id'])) {
$user->updateCardFromStripe();
}

return new Response('Webhook Handled', 200);
}

/**
* Handle customer source deleted.
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function handleCustomerSourceDeleted(array $payload)
{
if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
$user->updateCardFromStripe();
$user->updatePaymentMethodFromStripe();
}

return new Response('Webhook Handled', 200);
Expand Down
Loading