trypost/app/Http/Controllers/App/OnboardingController.php

189 lines
5.7 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Http\Controllers\App;
use App\Actions\Billing\StartSubscriptionCheckout;
use App\Enums\Plan\Slug;
use App\Enums\SocialAccount\Platform as SocialPlatform;
use App\Enums\User\Goal;
use App\Enums\User\Persona;
use App\Http\Requests\App\Onboarding\StoreOnboardingGoalsRequest;
use App\Http\Requests\App\Onboarding\StoreOnboardingRequest;
use App\Http\Resources\App\SocialAccountResource;
use App\Models\Account;
use App\Models\Plan;
use App\Services\PostHogService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
class OnboardingController extends Controller
{
public function index(Request $request): Response|RedirectResponse
{
if (config('trypost.self_hosted')) {
return redirect()->route('app.calendar');
}
$user = $request->user();
if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) {
return redirect()->route('app.calendar');
}
return Inertia::render('onboarding/Index', [
'personas' => array_map(fn (Persona $persona): string => $persona->value, Persona::cases()),
'selected' => $user->persona?->value,
]);
}
public function store(StoreOnboardingRequest $request, PostHogService $postHog): RedirectResponse
{
if (config('trypost.self_hosted')) {
return redirect()->route('app.calendar');
}
$user = $request->user();
fix: merge-readiness — close two billing/network bugs, harden tests Bugs (both with regression tests): - OnboardingController::store now guards already-subscribed accounts (mirrors index), preventing a second Stripe Checkout / double subscription if a subscribed user re-POSTs /onboarding. - SocialAccountObserver: drop the `platform_user_id != …` clause from the creating-time one-per-network check. On create there is no "self" to exclude, so it only weakened the rule — the same account connected via two network variants (e.g. Instagram standalone + via Facebook, same id) could slip a second account into the network. Now any account in the network blocks. Robustness: - CreateWorkspace wraps create + member attach + switchWorkspace in a transaction (cache-forget / quantity-sync run after), so a partial failure can't leave an orphan workspace that inflates the Stripe seat count — covers both the signup and the add-workspace paths. Test honesty & coverage: - Scope the ten "connect multiple <platform> accounts" tests to self-hosted mode (config + name); they only passed because the test env defaults SELF_HOSTED=true, and in cloud the one-per-network rule blocks them. - network_taken popup now has controller-level tests on all six OAuth controllers (added Threads, YouTube, LinkedInPage, and a new InstagramFacebook test file; LinkedInPage/InstagramFacebook also exercise variant collapse). - Wiring tests that creating/deleting a workspace actually calls syncWorkspaceQuantity (guards per-seat billing against silent breakage). - Strengthen TrialLengthTest to assert the configured length reaches trial_ends_at; add a same-id network-variant block test.
2026-06-22 12:26:31 +00:00
if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) {
fix: merge-readiness — close two billing/network bugs, harden tests Bugs (both with regression tests): - OnboardingController::store now guards already-subscribed accounts (mirrors index), preventing a second Stripe Checkout / double subscription if a subscribed user re-POSTs /onboarding. - SocialAccountObserver: drop the `platform_user_id != …` clause from the creating-time one-per-network check. On create there is no "self" to exclude, so it only weakened the rule — the same account connected via two network variants (e.g. Instagram standalone + via Facebook, same id) could slip a second account into the network. Now any account in the network blocks. Robustness: - CreateWorkspace wraps create + member attach + switchWorkspace in a transaction (cache-forget / quantity-sync run after), so a partial failure can't leave an orphan workspace that inflates the Stripe seat count — covers both the signup and the add-workspace paths. Test honesty & coverage: - Scope the ten "connect multiple <platform> accounts" tests to self-hosted mode (config + name); they only passed because the test env defaults SELF_HOSTED=true, and in cloud the one-per-network rule blocks them. - network_taken popup now has controller-level tests on all six OAuth controllers (added Threads, YouTube, LinkedInPage, and a new InstagramFacebook test file; LinkedInPage/InstagramFacebook also exercise variant collapse). - Wiring tests that creating/deleting a workspace actually calls syncWorkspaceQuantity (guards per-seat billing against silent breakage). - Strengthen TrialLengthTest to assert the configured length reaches trial_ends_at; add a same-id network-variant block test.
2026-06-22 12:26:31 +00:00
return redirect()->route('app.calendar');
}
$persona = (string) $request->validated('persona');
$user->update(['persona' => $persona]);
$postHog->identify($user->id, [
'persona' => $persona,
]);
return redirect()->route('app.onboarding.goals');
}
public function goals(Request $request): Response|RedirectResponse
{
if (config('trypost.self_hosted')) {
return redirect()->route('app.calendar');
}
$user = $request->user();
if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) {
return redirect()->route('app.calendar');
}
if (! $user->persona) {
return redirect()->route('app.onboarding');
}
return Inertia::render('onboarding/Goals', [
'goals' => array_map(fn (Goal $goal): string => $goal->value, Goal::cases()),
'selected' => $user->goals ?? [],
]);
}
public function storeGoals(StoreOnboardingGoalsRequest $request, PostHogService $postHog): RedirectResponse
{
if (config('trypost.self_hosted')) {
return redirect()->route('app.calendar');
}
$user = $request->user();
if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) {
return redirect()->route('app.calendar');
}
if (! $user->persona) {
return redirect()->route('app.onboarding');
}
$goals = array_values($request->validated('goals'));
$user->update(['goals' => $goals]);
$postHog->identify($user->id, [
'goals' => $goals,
]);
return redirect()->route('app.onboarding.connect');
}
public function connect(Request $request): Response|RedirectResponse
{
if (config('trypost.self_hosted')) {
return redirect()->route('app.calendar');
}
$user = $request->user();
if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) {
return redirect()->route('app.calendar');
}
if (! $user->persona) {
return redirect()->route('app.onboarding');
}
if (! $user->goals) {
return redirect()->route('app.onboarding.goals');
}
$workspace = $user->currentWorkspace;
if (! $workspace) {
return redirect()->route('app.workspaces.create');
}
$accounts = $workspace->socialAccounts()->orderBy('id')->get();
$platforms = collect(SocialPlatform::cases())
->filter(fn (SocialPlatform $platform): bool => $platform->isConnectable())
->map(fn (SocialPlatform $platform): array => [
'value' => $platform->value,
'label' => $platform->label(),
'color' => $platform->color(),
'network' => $platform->network(),
])->values();
return Inertia::render('onboarding/Connect', [
'platforms' => $platforms,
'accounts' => SocialAccountResource::collection($accounts)->resolve(),
]);
}
public function checkout(Request $request, StartSubscriptionCheckout $checkout): SymfonyResponse|RedirectResponse
{
if (config('trypost.self_hosted')) {
return redirect()->route('app.calendar');
}
$user = $request->user();
$account = $user->account;
if ($account?->subscribed(Account::SUBSCRIPTION_NAME)) {
return redirect()->route('app.calendar');
}
$workspace = $user->currentWorkspace;
if (! $workspace || ! $workspace->socialAccounts()->exists()) {
return redirect()->route('app.onboarding.connect')
->with('flash.banner', __('onboarding.connect.must_connect'))
->with('flash.bannerStyle', 'danger');
}
$plan = Plan::where('slug', Slug::Workspace)->firstOrFail();
return $checkout->redirect(
$account,
(string) $plan->stripe_monthly_price_id,
route('app.onboarding.connect'),
);
}
}