Skip to content

Commit

Permalink
[12.x] Implement new proration and pending updates (#949)
Browse files Browse the repository at this point in the history
Implement new proration and pending updates
  • Loading branch information
driesvints authored May 29, 2020
1 parent d7c2345 commit 7018689
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 43 deletions.
59 changes: 59 additions & 0 deletions src/Concerns/InteractsWithPaymentBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Laravel\Cashier\Concerns;

trait InteractsWithPaymentBehavior
{
/**
* Set the payment behavior for any subscription updates.
*
* @var string
*/
protected $paymentBehavior = 'allow_incomplete';

/**
* Allow subscription changes even if payment fails.
*
* @return $this
*/
public function allowPaymentFailures()
{
$this->paymentBehavior = 'allow_incomplete';

return $this;
}

/**
* Set any subscription change as pending until payment is successful.
*
* @return $this
*/
public function pendingIfPaymentFails()
{
$this->paymentBehavior = 'pending_if_incomplete';

return $this;
}

/**
* Prevent any subscription change if payment is unsuccessful.
*
* @return $this
*/
public function errorIfPaymentFails()
{
$this->paymentBehavior = 'error_if_incomplete';

return $this;
}

/**
* Determine the payment behavior when updating the subscription.
*
* @return string
*/
public function paymentBehavior()
{
return $this->paymentBehavior;
}
}
28 changes: 20 additions & 8 deletions src/Concerns/Prorates.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ trait Prorates
/**
* Indicates if the plan change should be prorated.
*
* @var bool
* @var string
*/
protected $prorate = true;
protected $prorationBehavior = 'create_prorations';

/**
* Indicate that the plan change should not be prorated.
Expand All @@ -18,7 +18,7 @@ trait Prorates
*/
public function noProrate()
{
$this->prorate = false;
$this->prorationBehavior = 'none';

return $this;
}
Expand All @@ -30,20 +30,32 @@ public function noProrate()
*/
public function prorate()
{
$this->prorate = true;
$this->prorationBehavior = 'create_prorations';

return $this;
}

/**
* Indicate that the plan change should always be invoiced.
*
* @return $this
*/
public function alwaysInvoice()
{
$this->prorationBehavior = 'always_invoice';

return $this;
}

/**
* Set the prorating behavior.
*
* @param bool $prorate
* @param string $prorationBehavior
* @return $this
*/
public function setProrate($prorate)
public function setProrationBehavior($prorationBehavior)
{
$this->prorate = $prorate;
$this->prorationBehavior = $prorationBehavior;

return $this;
}
Expand All @@ -55,6 +67,6 @@ public function setProrate($prorate)
*/
public function prorateBehavior()
{
return $this->prorate ? 'create_prorations' : 'none';
return $this->prorationBehavior;
}
}
13 changes: 13 additions & 0 deletions src/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,19 @@ public function downloadAs($filename, array $data)
]);
}

/**
* Void the Stripe invoice.
*
* @param array $options
* @return $this
*/
public function void(array $options = [])
{
$this->invoice = $this->invoice->voidInvoice($options, $this->owner->stripeOptions());

return $this;
}

/**
* Get the Stripe model instance.
*
Expand Down
77 changes: 56 additions & 21 deletions src/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Laravel\Cashier\Concerns\InteractsWithPaymentBehavior;
use Laravel\Cashier\Concerns\Prorates;
use Laravel\Cashier\Exceptions\IncompletePayment;
use Laravel\Cashier\Exceptions\SubscriptionUpdateFailure;
Expand All @@ -16,6 +17,7 @@

class Subscription extends Model
{
use InteractsWithPaymentBehavior;
use Prorates;

/**
Expand Down Expand Up @@ -394,7 +396,7 @@ public function incrementQuantity($count = 1, $plan = null)
$this->guardAgainstIncomplete();

if ($plan) {
$this->findItemOrFail($plan)->setProrate($this->prorate)->incrementQuantity($count);
$this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->incrementQuantity($count);

return $this->refresh();
}
Expand All @@ -420,8 +422,10 @@ public function incrementAndInvoice($count = 1, $plan = null)
{
$this->guardAgainstIncomplete();

$this->alwaysInvoice();

if ($plan) {
$this->findItemOrFail($plan)->setProrate($this->prorate)->incrementQuantity($count);
$this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->incrementQuantity($count);

return $this->refresh();
}
Expand All @@ -430,8 +434,6 @@ public function incrementAndInvoice($count = 1, $plan = null)

$this->incrementQuantity($count, $plan);

$this->invoice();

return $this;
}

Expand All @@ -449,7 +451,7 @@ public function decrementQuantity($count = 1, $plan = null)
$this->guardAgainstIncomplete();

if ($plan) {
$this->findItemOrFail($plan)->setProrate($this->prorate)->decrementQuantity($count);
$this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->decrementQuantity($count);

return $this->refresh();
}
Expand All @@ -473,7 +475,7 @@ public function updateQuantity($quantity, $plan = null)
$this->guardAgainstIncomplete();

if ($plan) {
$this->findItemOrFail($plan)->setProrate($this->prorate)->updateQuantity($quantity);
$this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->updateQuantity($quantity);

return $this->refresh();
}
Expand All @@ -483,7 +485,7 @@ public function updateQuantity($quantity, $plan = null)
$stripeSubscription = $this->asStripeSubscription();

$stripeSubscription->quantity = $quantity;

$stripeSubscription->payment_behavior = $this->paymentBehavior();
$stripeSubscription->proration_behavior = $this->prorateBehavior();

$stripeSubscription->save();
Expand Down Expand Up @@ -577,6 +579,7 @@ public function swap($plans, $options = [])
);

$this->fill([
'stripe_status' => $stripeSubscription->status,
'stripe_plan' => $stripeSubscription->plan ? $stripeSubscription->plan->id : null,
'quantity' => $stripeSubscription->quantity,
'ends_at' => null,
Expand All @@ -596,6 +599,12 @@ public function swap($plans, $options = [])

$this->unsetRelation('items');

if ($stripeSubscription->latest_invoice->payment_intent) {
(new Payment(
$stripeSubscription->latest_invoice->payment_intent
))->validate();
}

return $this;
}

Expand All @@ -611,11 +620,9 @@ public function swap($plans, $options = [])
*/
public function swapAndInvoice($plans, $options = [])
{
$subscription = $this->swap($plans, $options);

$this->invoice();
$this->alwaysInvoice();

return $subscription;
return $this->swap($plans, $options);
}

/**
Expand Down Expand Up @@ -668,21 +675,28 @@ protected function mergeItemsThatShouldBeDeletedDuringSwap(Collection $items)
*/
protected function getSwapOptions(Collection $items, $options)
{
$options = array_merge([
$payload = [
'items' => $items->values()->all(),
'payment_behavior' => $this->paymentBehavior(),
'proration_behavior' => $this->prorateBehavior(),
'cancel_at_period_end' => false,
], $options);
'expand' => ['latest_invoice.payment_intent'],
];

if ($payload['payment_behavior'] !== 'pending_if_incomplete') {
$payload['cancel_at_period_end'] = false;
}

$payload = array_merge($payload, $options);

if (! is_null($this->billingCycleAnchor)) {
$options['billing_cycle_anchor'] = $this->billingCycleAnchor;
$payload['billing_cycle_anchor'] = $this->billingCycleAnchor;
}

$options['trial_end'] = $this->onTrial()
$payload['trial_end'] = $this->onTrial()
? $this->trial_ends_at->getTimestamp()
: 'now';

return $options;
return $payload;
}

/**
Expand All @@ -709,6 +723,7 @@ public function addPlan($plan, $quantity = 1, $options = [])
'plan' => $plan,
'quantity' => $quantity,
'tax_rates' => $this->getPlanTaxRatesForPayload($plan),
'payment_behavior' => $this->paymentBehavior(),
'proration_behavior' => $this->prorateBehavior(),
], $options));

Expand Down Expand Up @@ -743,11 +758,9 @@ public function addPlan($plan, $quantity = 1, $options = [])
*/
public function addPlanAndInvoice($plan, $quantity = 1, $options = [])
{
$subscription = $this->addPlan($plan, $quantity, $options);
$this->alwaysInvoice();

$this->invoice();

return $subscription;
return $this->addPlan($plan, $quantity, $options);
}

/**
Expand Down Expand Up @@ -883,6 +896,16 @@ public function resume()
return $this;
}

/**
* Determine if the subscription has pending updates.
*
* @return bool
*/
public function pending()
{
return $this->asStripeSubscription()->pending_update !== null;
}

/**
* Invoice the subscription outside of the regular billing cycle.
*
Expand All @@ -905,6 +928,18 @@ public function invoice(array $options = [])
}
}

/**
* Get the latest invoice for the subscription.
*
* @return \Laravel\Cashier\Invoice
*/
public function latestInvoice()
{
$stripeSubscription = $this->asStripeSubscription(['latest_invoice']);

return new Invoice($this->owner, $stripeSubscription->latest_invoice);
}

/**
* Sync the tax percentage of the user to the subscription.
*
Expand Down
7 changes: 7 additions & 0 deletions src/SubscriptionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
use DateTimeInterface;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Laravel\Cashier\Concerns\InteractsWithPaymentBehavior;
use Laravel\Cashier\Concerns\Prorates;
use Stripe\Subscription as StripeSubscription;

class SubscriptionBuilder
{
use InteractsWithPaymentBehavior;
use Prorates;

/**
* The model that is subscribing.
*
Expand Down Expand Up @@ -315,6 +320,8 @@ protected function buildPayload()
'expand' => ['latest_invoice.payment_intent'],
'metadata' => $this->metadata,
'items' => collect($this->items)->values()->all(),
'payment_behavior' => $this->paymentBehavior(),
'proration_behavior' => $this->prorateBehavior(),
'trial_end' => $this->getTrialEndForPayload(),
'off_session' => true,
]);
Expand Down
Loading

0 comments on commit 7018689

Please sign in to comment.