feat: translate billing page and fix flash messages
- Move billing routes from /billing to /settings/billing
- Add billing translations (en/pt-br)
- Update billing/Index.vue to use $t() for all strings
- Fix all controllers using route('dashboard') to use route('calendar') or route('accounts')
- Replace ->with('error', ...) pattern with session()->flash('flash.banner', ...)
- Add self-hosted mode support for hasActiveSubscription() and workspace methods
- Add flash translations for account connection errors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1d056f426f
commit
2239d2480c
21 changed files with 291 additions and 69 deletions
|
|
@ -45,7 +45,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
session([
|
||||
|
|
@ -174,15 +177,19 @@ public function selectPage(Request $request)
|
|||
$workspaceId = session('social_connect_workspace');
|
||||
|
||||
if (! $oauthData || ! $workspaceId) {
|
||||
return redirect()->route('dashboard')
|
||||
->with('error', 'Session expired. Please try again.');
|
||||
session()->flash('flash.banner', __('accounts.flash.session_expired'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route('accounts');
|
||||
}
|
||||
|
||||
$workspace = Workspace::find($workspaceId);
|
||||
|
||||
if (! $workspace) {
|
||||
return redirect()->route('dashboard')
|
||||
->with('error', 'Workspace not found.');
|
||||
session()->flash('flash.banner', __('accounts.flash.workspace_not_found'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route('accounts');
|
||||
}
|
||||
|
||||
return Inertia::render('accounts/FacebookPageSelect', [
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
session([
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
return $this->redirectToProvider($request, $this->driver, $this->scopes);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,10 @@ public function connect(Request $request): SymfonyResponse|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
session([
|
||||
|
|
@ -128,15 +131,19 @@ public function selectPage(Request $request): Response|RedirectResponse
|
|||
$pendingData = session('linkedin_page_pending');
|
||||
|
||||
if (! $pendingData) {
|
||||
return redirect()->route('dashboard')
|
||||
->with('error', 'Session expired. Please try again.');
|
||||
session()->flash('flash.banner', __('accounts.flash.session_expired'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route('accounts');
|
||||
}
|
||||
|
||||
$workspace = Workspace::find($pendingData['workspace_id']);
|
||||
|
||||
if (! $workspace || ! $request->user()->can('manageAccounts', $workspace)) {
|
||||
return redirect()->route('dashboard')
|
||||
->with('error', 'Workspace not found.');
|
||||
session()->flash('flash.banner', __('accounts.flash.workspace_not_found'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route('accounts');
|
||||
}
|
||||
|
||||
return Inertia::render('accounts/LinkedInPageSelect', [
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
return $this->redirectToProvider($request, $this->driver, $this->scopes);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
session([
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
session(['social_reconnect_id' => $existingAccount?->id]);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
return $this->redirectToProvider($request, $this->driver, $this->scopes);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ public function connect(Request $request): Response|RedirectResponse
|
|||
->first();
|
||||
|
||||
if ($existingAccount && ! $existingAccount->isDisconnected()) {
|
||||
return back()->with('error', 'This platform is already connected.');
|
||||
session()->flash('flash.banner', __('accounts.flash.already_connected'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
session([
|
||||
|
|
@ -164,15 +167,19 @@ public function selectChannel(Request $request)
|
|||
$workspaceId = session('social_connect_workspace');
|
||||
|
||||
if (! $oauthData || ! $workspaceId) {
|
||||
return redirect()->route('dashboard')
|
||||
->with('error', 'Session expired. Please try again.');
|
||||
session()->flash('flash.banner', __('accounts.flash.session_expired'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route('accounts');
|
||||
}
|
||||
|
||||
$workspace = Workspace::find($workspaceId);
|
||||
|
||||
if (! $workspace) {
|
||||
return redirect()->route('dashboard')
|
||||
->with('error', 'Workspace not found.');
|
||||
session()->flash('flash.banner', __('accounts.flash.workspace_not_found'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route('accounts');
|
||||
}
|
||||
|
||||
// Fetch YouTube channels
|
||||
|
|
@ -183,8 +190,10 @@ public function selectChannel(Request $request)
|
|||
$this->forgetSocialConnectSession();
|
||||
session()->forget('youtube_oauth');
|
||||
|
||||
return redirect()->route($redirectRoute)
|
||||
->with('error', 'No YouTube channels found. Please create a channel first.');
|
||||
session()->flash('flash.banner', __('accounts.flash.no_youtube_channels'));
|
||||
session()->flash('flash.bannerStyle', 'danger');
|
||||
|
||||
return redirect()->route($redirectRoute);
|
||||
}
|
||||
|
||||
return inertia('accounts/YouTubeChannelSelect', [
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ public function processing(Request $request): Response|RedirectResponse
|
|||
$user = $request->user();
|
||||
$status = $request->query('status', 'processing');
|
||||
|
||||
// If already subscribed, redirect to dashboard
|
||||
// If already subscribed, redirect to calendar
|
||||
if ($user->subscribed('default')) {
|
||||
return redirect()->route('dashboard');
|
||||
return redirect()->route('calendar');
|
||||
}
|
||||
|
||||
// Validate status
|
||||
|
|
|
|||
|
|
@ -52,9 +52,14 @@ public function ownedWorkspacesCount(): int
|
|||
|
||||
/**
|
||||
* Check if user can create more workspaces based on subscription.
|
||||
* In self-hosted mode, users can create unlimited workspaces.
|
||||
*/
|
||||
public function canCreateWorkspace(): bool
|
||||
{
|
||||
if (config('trypost.self_hosted')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->hasActiveSubscription()) {
|
||||
return $this->ownedWorkspacesCount() === 0;
|
||||
}
|
||||
|
|
@ -66,9 +71,14 @@ public function canCreateWorkspace(): bool
|
|||
|
||||
/**
|
||||
* Increment workspace quantity on subscription.
|
||||
* Skipped in self-hosted mode (no Stripe subscription).
|
||||
*/
|
||||
public function incrementWorkspaceQuantity(): void
|
||||
{
|
||||
if (config('trypost.self_hosted')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hasActiveSubscription()) {
|
||||
$this->subscription('default')->incrementQuantity();
|
||||
}
|
||||
|
|
@ -76,9 +86,14 @@ public function incrementWorkspaceQuantity(): void
|
|||
|
||||
/**
|
||||
* Decrement workspace quantity on subscription.
|
||||
* Skipped in self-hosted mode (no Stripe subscription).
|
||||
*/
|
||||
public function decrementWorkspaceQuantity(): void
|
||||
{
|
||||
if (config('trypost.self_hosted')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hasActiveSubscription()) {
|
||||
$subscription = $this->subscription('default');
|
||||
|
||||
|
|
@ -90,9 +105,14 @@ public function decrementWorkspaceQuantity(): void
|
|||
|
||||
/**
|
||||
* Sync subscription quantity with actual workspace count.
|
||||
* Skipped in self-hosted mode (no Stripe subscription).
|
||||
*/
|
||||
public function syncWorkspaceQuantity(): void
|
||||
{
|
||||
if (config('trypost.self_hosted')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hasActiveSubscription()) {
|
||||
$count = $this->ownedWorkspacesCount();
|
||||
|
||||
|
|
|
|||
|
|
@ -88,9 +88,14 @@ public function language(): BelongsTo
|
|||
|
||||
/**
|
||||
* Check if user has an active subscription.
|
||||
* In self-hosted mode, always returns true (no subscription required).
|
||||
*/
|
||||
public function hasActiveSubscription(): bool
|
||||
{
|
||||
if (config('trypost.self_hosted')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->subscribed('default');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,5 +59,10 @@
|
|||
|
||||
'flash' => [
|
||||
'disconnected' => 'Account disconnected successfully!',
|
||||
'connected' => 'Account connected successfully!',
|
||||
'session_expired' => 'Session expired. Please try again.',
|
||||
'workspace_not_found' => 'Workspace not found.',
|
||||
'already_connected' => 'This platform is already connected.',
|
||||
'no_youtube_channels' => 'No YouTube channels found. Please create a channel first.',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
37
lang/en/billing.php
Normal file
37
lang/en/billing.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Subscription',
|
||||
'description' => 'Manage your subscription and payment method',
|
||||
|
||||
'trial' => [
|
||||
'title' => 'Trial period active',
|
||||
'description' => 'Your trial ends on :date. After that, your subscription will be charged automatically.',
|
||||
],
|
||||
|
||||
'subscription' => [
|
||||
'title' => 'Your Subscription',
|
||||
'workspaces' => 'Workspaces',
|
||||
'quantity' => 'Subscription quantity',
|
||||
'expires' => 'Expires :date',
|
||||
'canceled_on' => 'Your subscription will be canceled on :date',
|
||||
'manage' => 'Manage on Stripe',
|
||||
],
|
||||
|
||||
'invoices' => [
|
||||
'title' => 'Invoices',
|
||||
'description' => 'Payment history',
|
||||
'empty' => 'No invoices found',
|
||||
'paid' => 'Paid',
|
||||
],
|
||||
|
||||
'status' => [
|
||||
'active' => 'Active',
|
||||
'canceled' => 'Canceled',
|
||||
'incomplete' => 'Incomplete',
|
||||
'incomplete_expired' => 'Expired',
|
||||
'past_due' => 'Past due',
|
||||
'trialing' => 'Trial',
|
||||
'unpaid' => 'Unpaid',
|
||||
],
|
||||
];
|
||||
|
|
@ -59,5 +59,10 @@
|
|||
|
||||
'flash' => [
|
||||
'disconnected' => 'Conta desconectada com sucesso!',
|
||||
'connected' => 'Conta conectada com sucesso!',
|
||||
'session_expired' => 'Sessão expirada. Por favor, tente novamente.',
|
||||
'workspace_not_found' => 'Workspace não encontrado.',
|
||||
'already_connected' => 'Esta plataforma já está conectada.',
|
||||
'no_youtube_channels' => 'Nenhum canal do YouTube encontrado. Por favor, crie um canal primeiro.',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
37
lang/pt-br/billing.php
Normal file
37
lang/pt-br/billing.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Assinatura',
|
||||
'description' => 'Gerencie sua assinatura e método de pagamento',
|
||||
|
||||
'trial' => [
|
||||
'title' => 'Período de teste ativo',
|
||||
'description' => 'Seu período de teste termina em :date. Após isso, sua assinatura será cobrada automaticamente.',
|
||||
],
|
||||
|
||||
'subscription' => [
|
||||
'title' => 'Sua Assinatura',
|
||||
'workspaces' => 'Workspaces',
|
||||
'quantity' => 'Quantidade da assinatura',
|
||||
'expires' => 'Expira em :date',
|
||||
'canceled_on' => 'Sua assinatura será cancelada em :date',
|
||||
'manage' => 'Gerenciar no Stripe',
|
||||
],
|
||||
|
||||
'invoices' => [
|
||||
'title' => 'Faturas',
|
||||
'description' => 'Histórico de pagamentos',
|
||||
'empty' => 'Nenhuma fatura encontrada',
|
||||
'paid' => 'Pago',
|
||||
],
|
||||
|
||||
'status' => [
|
||||
'active' => 'Ativo',
|
||||
'canceled' => 'Cancelado',
|
||||
'incomplete' => 'Incompleto',
|
||||
'incomplete_expired' => 'Expirado',
|
||||
'past_due' => 'Vencido',
|
||||
'trialing' => 'Teste',
|
||||
'unpaid' => 'Não pago',
|
||||
],
|
||||
];
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { IconCreditCard, IconFileText, IconBuilding, IconExternalLink, IconSparkles } from '@tabler/icons-vue';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
|
|
@ -7,6 +8,7 @@ import { Badge } from '@/components/ui/badge';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { portal } from '@/routes/billing';
|
||||
import { type BreadcrumbItemType } from '@/types';
|
||||
|
||||
interface Subscription {
|
||||
|
|
@ -44,26 +46,17 @@ defineProps<Props>();
|
|||
|
||||
const breadcrumbs: BreadcrumbItemType[] = [
|
||||
{
|
||||
title: 'Subscription',
|
||||
href: '/billing',
|
||||
title: trans('billing.title'),
|
||||
href: '/settings/billing',
|
||||
},
|
||||
];
|
||||
|
||||
function openPortal() {
|
||||
window.location.href = '/billing/portal';
|
||||
window.location.href = portal.url();
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
active: 'Active',
|
||||
canceled: 'Canceled',
|
||||
incomplete: 'Incomplete',
|
||||
incomplete_expired: 'Expired',
|
||||
past_due: 'Past due',
|
||||
trialing: 'Trial',
|
||||
unpaid: 'Unpaid',
|
||||
};
|
||||
return labels[status] || status;
|
||||
return trans(`billing.status.${status}`) || status;
|
||||
}
|
||||
|
||||
function getStatusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
|
|
@ -74,23 +67,22 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Subscription" />
|
||||
<Head :title="$t('billing.title')" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="flex flex-col gap-6 p-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Subscription</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{{ $t('billing.title') }}</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Manage your subscription and payment method
|
||||
{{ $t('billing.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert v-if="onTrial" class="border-primary/50 bg-primary/5">
|
||||
<IconSparkles class="h-4 w-4 text-primary" />
|
||||
<AlertTitle>Trial period active</AlertTitle>
|
||||
<AlertTitle>{{ $t('billing.trial.title') }}</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your trial ends on <strong>{{ trialEndsAt }}</strong>.
|
||||
After that, your subscription will be charged automatically.
|
||||
{{ $t('billing.trial.description', { date: trialEndsAt }) }}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
|
|
@ -100,7 +92,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
|
|||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<IconBuilding class="h-5 w-5" />
|
||||
Your Subscription
|
||||
{{ $t('billing.subscription.title') }}
|
||||
</CardTitle>
|
||||
<Badge :variant="getStatusVariant(subscription?.stripe_status || '')">
|
||||
{{ getStatusLabel(subscription?.stripe_status || '') }}
|
||||
|
|
@ -110,11 +102,11 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
|
|||
<CardContent class="space-y-4">
|
||||
<div class="flex justify-between items-center p-4 bg-muted rounded-lg">
|
||||
<div>
|
||||
<p class="text-sm text-muted-foreground">Workspaces</p>
|
||||
<p class="text-sm text-muted-foreground">{{ $t('billing.subscription.workspaces') }}</p>
|
||||
<p class="text-2xl font-bold">{{ workspacesCount }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-muted-foreground">Subscription quantity</p>
|
||||
<p class="text-sm text-muted-foreground">{{ $t('billing.subscription.quantity') }}</p>
|
||||
<p class="text-2xl font-bold">{{ subscription?.quantity || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,21 +116,21 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
|
|||
<div>
|
||||
<p class="font-medium capitalize">{{ defaultPaymentMethod.brand }} **** {{ defaultPaymentMethod.last4 }}</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Expires {{ defaultPaymentMethod.exp_month }}/{{ defaultPaymentMethod.exp_year }}
|
||||
{{ $t('billing.subscription.expires', { date: `${defaultPaymentMethod.exp_month}/${defaultPaymentMethod.exp_year}` }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="subscription?.ends_at" class="p-3 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-950 dark:border-yellow-800">
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
Your subscription will be canceled on {{ subscription.ends_at }}
|
||||
{{ $t('billing.subscription.canceled_on', { date: subscription.ends_at }) }}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button @click="openPortal" variant="outline" class="w-full">
|
||||
<IconExternalLink class="mr-2 h-4 w-4" />
|
||||
Manage on Stripe
|
||||
{{ $t('billing.subscription.manage') }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
|
@ -147,15 +139,15 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
|
|||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<IconFileText class="h-5 w-5" />
|
||||
Invoices
|
||||
{{ $t('billing.invoices.title') }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Payment history
|
||||
{{ $t('billing.invoices.description') }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="invoices.length === 0" class="text-center py-6 text-muted-foreground">
|
||||
No invoices found
|
||||
{{ $t('billing.invoices.empty') }}
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<a
|
||||
|
|
@ -170,7 +162,7 @@ function getStatusVariant(status: string): 'default' | 'secondary' | 'destructiv
|
|||
<p class="text-sm text-muted-foreground">{{ invoice.total }}</p>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{{ invoice.status === 'paid' ? 'Paid' : invoice.status }}
|
||||
{{ invoice.status === 'paid' ? $t('billing.invoices.paid') : invoice.status }}
|
||||
</Badge>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -140,9 +140,9 @@
|
|||
Route::put('labels/{label}', [WorkspaceLabelController::class, 'update'])->name('labels.update');
|
||||
Route::delete('labels/{label}', [WorkspaceLabelController::class, 'destroy'])->name('labels.destroy');
|
||||
|
||||
// Billing
|
||||
Route::get('billing', [BillingController::class, 'index'])->name('billing.index');
|
||||
Route::get('billing/portal', [BillingController::class, 'portal'])->name('billing.portal');
|
||||
// Billing (under settings prefix)
|
||||
Route::get('settings/billing', [BillingController::class, 'index'])->name('billing.index');
|
||||
Route::get('settings/billing/portal', [BillingController::class, 'portal'])->name('billing.portal');
|
||||
});
|
||||
|
||||
require __DIR__.'/settings.php';
|
||||
|
|
|
|||
|
|
@ -186,7 +186,8 @@
|
|||
$response = $this->actingAs($this->user)->get(route('social.linkedin-page.connect'));
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('error', 'This platform is already connected.');
|
||||
$response->assertSessionHas('flash.banner', __('accounts.flash.already_connected'));
|
||||
$response->assertSessionHas('flash.bannerStyle', 'danger');
|
||||
});
|
||||
|
||||
test('user can reconnect disconnected linkedin page account', function () {
|
||||
|
|
|
|||
|
|
@ -54,10 +54,15 @@
|
|||
);
|
||||
});
|
||||
|
||||
test('create workspace redirects to billing if user already has workspace and no subscription', function () {
|
||||
test('create workspace shows form when user already has workspace in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('workspaces.create'));
|
||||
|
||||
$response->assertRedirect(route('billing.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('workspaces/Create', false)
|
||||
);
|
||||
});
|
||||
|
||||
// Store tests
|
||||
|
|
@ -84,12 +89,19 @@
|
|||
]);
|
||||
});
|
||||
|
||||
test('store workspace redirects to billing for second workspace without subscription', function () {
|
||||
test('store workspace creates second workspace in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('workspaces.store'), [
|
||||
'name' => 'Second Workspace',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('billing.index'));
|
||||
$response->assertRedirect(route('calendar'));
|
||||
|
||||
$this->assertDatabaseHas('workspaces', [
|
||||
'name' => 'Second Workspace',
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('store workspace validates name is required', function () {
|
||||
|
|
|
|||
|
|
@ -81,13 +81,17 @@
|
|||
expect($user->ownedWorkspacesCount())->toBe(3);
|
||||
});
|
||||
|
||||
test('user without subscription can create first workspace', function () {
|
||||
test('user without subscription can create first workspace in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect($user->canCreateWorkspace())->toBeTrue();
|
||||
});
|
||||
|
||||
test('user without subscription cannot create second workspace', function () {
|
||||
test('user without subscription cannot create second workspace in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
|
||||
$workspace->members()->attach($user->id, ['role' => 'owner']);
|
||||
|
|
@ -95,7 +99,22 @@
|
|||
expect($user->canCreateWorkspace())->toBeFalse();
|
||||
});
|
||||
|
||||
test('user with subscription can create workspaces up to quantity', function () {
|
||||
test('user can create unlimited workspaces in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$workspaces = Workspace::factory()->count(5)->create(['user_id' => $user->id]);
|
||||
|
||||
foreach ($workspaces as $workspace) {
|
||||
$workspace->members()->attach($user->id, ['role' => 'owner']);
|
||||
}
|
||||
|
||||
expect($user->canCreateWorkspace())->toBeTrue();
|
||||
});
|
||||
|
||||
test('user with subscription can create workspaces up to quantity in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->subscriptions()->create([
|
||||
'type' => 'default',
|
||||
|
|
@ -111,7 +130,9 @@
|
|||
expect($user->canCreateWorkspace())->toBeTrue();
|
||||
});
|
||||
|
||||
test('user with subscription cannot exceed workspace quantity', function () {
|
||||
test('user with subscription cannot exceed workspace quantity in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->subscriptions()->create([
|
||||
'type' => 'default',
|
||||
|
|
@ -130,7 +151,9 @@
|
|||
expect($user->canCreateWorkspace())->toBeFalse();
|
||||
});
|
||||
|
||||
test('increment workspace quantity does nothing without subscription', function () {
|
||||
test('increment workspace quantity does nothing without subscription in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->incrementWorkspaceQuantity();
|
||||
|
|
@ -138,7 +161,20 @@
|
|||
expect($user->subscription('default'))->toBeNull();
|
||||
});
|
||||
|
||||
test('decrement workspace quantity does nothing without subscription', function () {
|
||||
test('increment workspace quantity is skipped in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Should not throw any errors even without subscription
|
||||
$user->incrementWorkspaceQuantity();
|
||||
|
||||
expect($user->subscription('default'))->toBeNull();
|
||||
});
|
||||
|
||||
test('decrement workspace quantity does nothing without subscription in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->decrementWorkspaceQuantity();
|
||||
|
|
@ -146,7 +182,20 @@
|
|||
expect($user->subscription('default'))->toBeNull();
|
||||
});
|
||||
|
||||
test('sync workspace quantity does nothing without subscription', function () {
|
||||
test('decrement workspace quantity is skipped in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Should not throw any errors even without subscription
|
||||
$user->decrementWorkspaceQuantity();
|
||||
|
||||
expect($user->subscription('default'))->toBeNull();
|
||||
});
|
||||
|
||||
test('sync workspace quantity does nothing without subscription in SaaS mode', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$workspaces = Workspace::factory()->count(2)->create(['user_id' => $user->id]);
|
||||
|
||||
|
|
@ -159,7 +208,25 @@
|
|||
expect($user->subscription('default'))->toBeNull();
|
||||
});
|
||||
|
||||
test('sync workspace quantity is skipped in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$workspaces = Workspace::factory()->count(2)->create(['user_id' => $user->id]);
|
||||
|
||||
foreach ($workspaces as $workspace) {
|
||||
$workspace->members()->attach($user->id, ['role' => 'owner']);
|
||||
}
|
||||
|
||||
// Should not throw any errors even without subscription
|
||||
$user->syncWorkspaceQuantity();
|
||||
|
||||
expect($user->subscription('default'))->toBeNull();
|
||||
});
|
||||
|
||||
test('sync workspace quantity does nothing with zero workspaces', function () {
|
||||
config(['trypost.self_hosted' => false]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->subscriptions()->create([
|
||||
'type' => 'default',
|
||||
|
|
|
|||
Loading…
Reference in a new issue