Drops the abort_if guard that blocked switching from a yearly billing cadence to monthly. The product decision was reversed — users should be free to move in either direction without going through support. Removes the corresponding 'swap blocks yearly to monthly downgrade' test.
174 lines
5.6 KiB
PHP
174 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\App;
|
|
|
|
use App\Models\Account;
|
|
use App\Models\Plan;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
|
|
|
class BillingController extends Controller
|
|
{
|
|
public function subscribe(Request $request): Response|RedirectResponse
|
|
{
|
|
if (config('trypost.self_hosted')) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
$account = $request->user()->account;
|
|
|
|
if ($account && $account->hasActiveSubscription()) {
|
|
return redirect()->route('app.billing.index');
|
|
}
|
|
|
|
return Inertia::render('billing/Subscribe', [
|
|
'plans' => Plan::active()->orderBy('sort')->get(),
|
|
'trialDays' => config('cashier.trial_days'),
|
|
]);
|
|
}
|
|
|
|
public function checkout(Request $request, Plan $plan): SymfonyResponse|RedirectResponse
|
|
{
|
|
if (config('trypost.self_hosted')) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
$user = $request->user();
|
|
$account = $user->account;
|
|
|
|
abort_unless($user->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN);
|
|
|
|
$request->validate([
|
|
'price_id' => ['required', 'string'],
|
|
]);
|
|
|
|
$priceId = $request->input('price_id');
|
|
|
|
abort_unless(
|
|
$priceId === $plan->stripe_monthly_price_id || $priceId === $plan->stripe_yearly_price_id,
|
|
422,
|
|
'Invalid price for this plan',
|
|
);
|
|
|
|
$account->createOrGetStripeCustomer([
|
|
'email' => $account->stripeEmail(),
|
|
'name' => $account->stripeName(),
|
|
]);
|
|
|
|
$subscription = $account->newSubscription(Account::SUBSCRIPTION_NAME, $priceId)
|
|
->allowPromotionCodes()
|
|
->trialDays(config('cashier.trial_days'));
|
|
|
|
$checkoutSession = $subscription->checkout([
|
|
'success_url' => route('app.billing.processing'),
|
|
'cancel_url' => route('app.subscribe'),
|
|
]);
|
|
|
|
return Inertia::location($checkoutSession->url);
|
|
}
|
|
|
|
public function processing(Request $request): Response|RedirectResponse
|
|
{
|
|
if (config('trypost.self_hosted')) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
$account = $request->user()->account;
|
|
|
|
return Inertia::render('billing/Processing', [
|
|
'subscriptionActive' => $account && $account->subscribed(Account::SUBSCRIPTION_NAME),
|
|
]);
|
|
}
|
|
|
|
public function index(Request $request): Response|RedirectResponse
|
|
{
|
|
if (config('trypost.self_hosted')) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
$account = $request->user()->account;
|
|
|
|
abort_unless($request->user()->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN);
|
|
|
|
$subscription = $account->subscription(Account::SUBSCRIPTION_NAME);
|
|
|
|
return Inertia::render('settings/account/Billing', [
|
|
'hasSubscription' => $account->subscribed(Account::SUBSCRIPTION_NAME),
|
|
'onTrial' => $subscription?->onTrial() ?? false,
|
|
'trialEndsAt' => $subscription?->trial_ends_at,
|
|
'subscription' => $subscription?->only([
|
|
'stripe_status',
|
|
'ends_at',
|
|
]),
|
|
'plan' => $account->plan,
|
|
'plans' => Plan::active()->orderBy('sort')->get(),
|
|
'invoices' => $account->invoices()->map(fn ($invoice) => [
|
|
'id' => $invoice->id,
|
|
'date' => $invoice->date(),
|
|
'total' => $invoice->total(),
|
|
'status' => $invoice->status,
|
|
'invoice_pdf' => $invoice->invoice_pdf,
|
|
]),
|
|
'defaultPaymentMethod' => $account->displayablePaymentMethod(),
|
|
]);
|
|
}
|
|
|
|
public function swap(Request $request, Plan $plan): RedirectResponse
|
|
{
|
|
if (config('trypost.self_hosted')) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
$account = $request->user()->account;
|
|
|
|
abort_unless($request->user()->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN);
|
|
abort_unless($account->subscribed(Account::SUBSCRIPTION_NAME), 422, 'No active subscription');
|
|
|
|
$request->validate([
|
|
'price_id' => ['required', 'string'],
|
|
]);
|
|
|
|
$priceId = $request->input('price_id');
|
|
$subscription = $account->subscription(Account::SUBSCRIPTION_NAME);
|
|
|
|
abort_unless(
|
|
$priceId === $plan->stripe_monthly_price_id || $priceId === $plan->stripe_yearly_price_id,
|
|
422,
|
|
'Invalid price for this plan',
|
|
);
|
|
|
|
$authorization = Gate::inspect('swapPlan', [$account, $plan]);
|
|
|
|
if ($authorization->denied()) {
|
|
return back()->with('flash.error', $authorization->message());
|
|
}
|
|
|
|
$subscription->swap($priceId);
|
|
$account->update(['plan_id' => $plan->id]);
|
|
$account->forgetPlanFeatureCache();
|
|
|
|
return redirect()->route('app.billing.index')
|
|
->with('flash.success', __('billing.flash.plan_changed', ['plan' => $plan->name]));
|
|
}
|
|
|
|
public function portal(Request $request): RedirectResponse
|
|
{
|
|
if (config('trypost.self_hosted')) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
$account = $request->user()->account;
|
|
|
|
abort_unless($request->user()->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN);
|
|
|
|
return $account->redirectToBillingPortal(
|
|
route('app.billing.index')
|
|
);
|
|
}
|
|
}
|