2023-08-17 13:19:37 +00:00
< ? php
2023-12-07 18:06:32 +00:00
namespace App\Livewire\Subscription ;
2023-08-17 13:19:37 +00:00
2026-02-26 17:22:03 +00:00
use App\Actions\Stripe\CancelSubscriptionAtPeriodEnd ;
use App\Actions\Stripe\RefundSubscription ;
use App\Actions\Stripe\ResumeSubscription ;
2026-03-03 11:28:16 +00:00
use App\Actions\Stripe\UpdateSubscriptionQuantity ;
2024-02-23 14:45:53 +00:00
use App\Models\Team ;
2026-02-26 17:22:03 +00:00
use Illuminate\Support\Facades\Hash ;
2023-08-17 13:19:37 +00:00
use Livewire\Component ;
2026-02-26 17:22:03 +00:00
use Stripe\StripeClient ;
2023-08-17 13:19:37 +00:00
class Actions extends Component
{
2024-02-23 10:21:14 +00:00
public $server_limits = 0 ;
2024-06-10 20:43:34 +00:00
2026-03-03 11:28:16 +00:00
public int $quantity = UpdateSubscriptionQuantity :: MIN_SERVER_LIMIT ;
public int $minServerLimit = UpdateSubscriptionQuantity :: MIN_SERVER_LIMIT ;
public int $maxServerLimit = UpdateSubscriptionQuantity :: MAX_SERVER_LIMIT ;
public ? array $pricePreview = null ;
2026-02-26 17:22:03 +00:00
public bool $isRefundEligible = false ;
public int $refundDaysRemaining = 0 ;
public bool $refundCheckLoading = true ;
public bool $refundAlreadyUsed = false ;
public function mount () : void
2024-02-23 10:21:14 +00:00
{
2024-02-23 14:45:53 +00:00
$this -> server_limits = Team :: serverLimit ();
2026-03-03 11:28:16 +00:00
$this -> quantity = ( int ) $this -> server_limits ;
}
public function loadPricePreview ( int $quantity ) : void
{
$this -> quantity = $quantity ;
$result = ( new UpdateSubscriptionQuantity ) -> fetchPricePreview ( currentTeam (), $quantity );
$this -> pricePreview = $result [ 'success' ] ? $result [ 'preview' ] : null ;
}
// Password validation is intentionally skipped for quantity updates.
// Unlike refunds/cancellations, changing the server limit is a
// non-destructive, reversible billing adjustment (prorated by Stripe).
public function updateQuantity ( string $password = '' ) : bool
{
if ( $this -> quantity < UpdateSubscriptionQuantity :: MIN_SERVER_LIMIT ) {
$this -> dispatch ( 'error' , 'Minimum server limit is ' . UpdateSubscriptionQuantity :: MIN_SERVER_LIMIT . '.' );
$this -> quantity = UpdateSubscriptionQuantity :: MIN_SERVER_LIMIT ;
return true ;
}
if ( $this -> quantity === ( int ) $this -> server_limits ) {
return true ;
}
$result = ( new UpdateSubscriptionQuantity ) -> execute ( currentTeam (), $this -> quantity );
if ( $result [ 'success' ]) {
$this -> server_limits = $this -> quantity ;
$this -> pricePreview = null ;
$this -> dispatch ( 'success' , 'Server limit updated to ' . $this -> quantity . '.' );
return true ;
}
$this -> dispatch ( 'error' , $result [ 'error' ] ? ? 'Failed to update server limit.' );
$this -> quantity = ( int ) $this -> server_limits ;
return true ;
2024-02-23 10:21:14 +00:00
}
2024-06-10 20:43:34 +00:00
2026-02-26 17:22:03 +00:00
public function loadRefundEligibility () : void
{
$this -> checkRefundEligibility ();
$this -> refundCheckLoading = false ;
}
public function stripeCustomerPortal () : void
2024-02-23 10:21:14 +00:00
{
2023-08-24 14:14:09 +00:00
$session = getStripeCustomerPortalSession ( currentTeam ());
redirect ( $session -> url );
}
2026-02-26 17:22:03 +00:00
public function refundSubscription ( string $password ) : bool | string
{
if ( ! shouldSkipPasswordConfirmation () && ! Hash :: check ( $password , auth () -> user () -> password )) {
return 'Invalid password.' ;
}
$result = ( new RefundSubscription ) -> execute ( currentTeam ());
if ( $result [ 'success' ]) {
$this -> dispatch ( 'success' , 'Subscription refunded successfully.' );
$this -> redirect ( route ( 'subscription.index' ), navigate : true );
return true ;
}
$this -> dispatch ( 'error' , 'Something went wrong with the refund. Please <a href="' . config ( 'constants.urls.contact' ) . '" target="_blank" class="underline">contact us</a>.' );
return true ;
}
public function cancelImmediately ( string $password ) : bool | string
{
if ( ! shouldSkipPasswordConfirmation () && ! Hash :: check ( $password , auth () -> user () -> password )) {
return 'Invalid password.' ;
}
$team = currentTeam ();
$subscription = $team -> subscription ;
if ( ! $subscription ? -> stripe_subscription_id ) {
$this -> dispatch ( 'error' , 'Something went wrong with the cancellation. Please <a href="' . config ( 'constants.urls.contact' ) . '" target="_blank" class="underline">contact us</a>.' );
return true ;
}
try {
$stripe = new StripeClient ( config ( 'subscription.stripe_api_key' ));
$stripe -> subscriptions -> cancel ( $subscription -> stripe_subscription_id );
$subscription -> update ([
'stripe_cancel_at_period_end' => false ,
'stripe_invoice_paid' => false ,
'stripe_trial_already_ended' => false ,
'stripe_past_due' => false ,
'stripe_feedback' => 'Cancelled immediately by user' ,
'stripe_comment' => 'Subscription cancelled immediately by user at ' . now () -> toDateTimeString (),
]);
$team -> subscriptionEnded ();
\Log :: info ( " Subscription { $subscription -> stripe_subscription_id } cancelled immediately for team { $team -> name } " );
$this -> dispatch ( 'success' , 'Subscription cancelled successfully.' );
$this -> redirect ( route ( 'subscription.index' ), navigate : true );
return true ;
} catch ( \Exception $e ) {
\Log :: error ( " Immediate cancellation error for team { $team -> id } : " . $e -> getMessage ());
$this -> dispatch ( 'error' , 'Something went wrong with the cancellation. Please <a href="' . config ( 'constants.urls.contact' ) . '" target="_blank" class="underline">contact us</a>.' );
return true ;
}
}
public function cancelAtPeriodEnd ( string $password ) : bool | string
{
if ( ! shouldSkipPasswordConfirmation () && ! Hash :: check ( $password , auth () -> user () -> password )) {
return 'Invalid password.' ;
}
$result = ( new CancelSubscriptionAtPeriodEnd ) -> execute ( currentTeam ());
if ( $result [ 'success' ]) {
$this -> dispatch ( 'success' , 'Subscription will be cancelled at the end of the billing period.' );
return true ;
}
$this -> dispatch ( 'error' , 'Something went wrong with the cancellation. Please <a href="' . config ( 'constants.urls.contact' ) . '" target="_blank" class="underline">contact us</a>.' );
return true ;
}
public function resumeSubscription () : bool
{
$result = ( new ResumeSubscription ) -> execute ( currentTeam ());
if ( $result [ 'success' ]) {
$this -> dispatch ( 'success' , 'Subscription resumed successfully.' );
return true ;
}
$this -> dispatch ( 'error' , 'Something went wrong resuming the subscription. Please <a href="' . config ( 'constants.urls.contact' ) . '" target="_blank" class="underline">contact us</a>.' );
return true ;
}
private function checkRefundEligibility () : void
{
if ( ! isCloud () || ! currentTeam () -> subscription ? -> stripe_subscription_id ) {
return ;
}
try {
$this -> refundAlreadyUsed = currentTeam () -> subscription ? -> stripe_refunded_at !== null ;
$result = ( new RefundSubscription ) -> checkEligibility ( currentTeam ());
$this -> isRefundEligible = $result [ 'eligible' ];
$this -> refundDaysRemaining = $result [ 'days_remaining' ];
} catch ( \Exception $e ) {
\Log :: warning ( 'Refund eligibility check failed: ' . $e -> getMessage ());
}
}
2023-08-17 13:19:37 +00:00
}