fix bug 16 and 17
Build & Push / Pipeline Tests (push) Successful in 1m53s
Test / Type Check (all packages) (push) Successful in 56s
Build & Push / Build & Push Docker Image (push) Failing after 4m53s
Test / API Unit Tests (push) Successful in 1m15s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 41s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 42s
Test / API Integration Tests (push) Successful in 1m6s

This commit is contained in:
root
2026-08-04 00:52:58 -04:00
parent 626da23c2e
commit 149806a773
72 changed files with 834 additions and 174 deletions
@@ -974,7 +974,7 @@ export default function ContractDetailPage() {
</div>
</div>
<div className="space-y-8 print:space-y-0">
<div className="contract-print-layout space-y-8 print:space-y-0">
<section className="mx-auto max-w-[1060px] rounded-lg border-2 border-neutral-950 bg-white p-4 text-[13.5px] leading-[1.25] text-neutral-950 shadow-2xl print:h-[calc(100vh-10mm)] print:max-w-none print:rounded-none print:border print:p-1.5 print:text-[8px] print:shadow-none print:break-after-page">
<header className="grid w-full grid-cols-[31fr_69fr] items-start gap-3 print:gap-1.5">
<div className="flex h-[86px] flex-col items-start justify-start px-2 text-left print:h-[52px] print:px-1">
@@ -54,7 +54,7 @@ interface Entitlement {
}
interface Entitlements {
plan: 'STARTER' | 'GROWTH' | 'PRO'
plan: 'STARTER' | 'GROWTH' | 'PRO' | 'ENTERPRISE'
subscriptionStatus: string
accessLevel: 'full' | 'limited' | 'read_only' | 'none'
currentPeriodEnd: string | null
@@ -7,7 +7,7 @@ import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
import { buildHomepageSignInPath } from '@/lib/dashboardPaths'
type Plan = 'STARTER' | 'GROWTH' | 'PRO'
type Plan = 'STARTER' | 'GROWTH' | 'PRO' | 'ENTERPRISE'
type BillingPeriod = 'MONTHLY' | 'ANNUAL'
interface Subscription {
@@ -65,7 +65,13 @@ const INVOICE_STATUS: Record<string, string> = {
REFUNDED: 'bg-slate-100 text-slate-600',
}
const PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO']
const PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO', 'ENTERPRISE']
const PLAN_LABELS: Record<Plan, string> = {
STARTER: 'Launch',
GROWTH: 'Growth',
PRO: 'Pro',
ENTERPRISE: 'Enterprise',
}
export default function SubscriptionPage() {
const router = useRouter()
const { language } = useDashboardI18n()
@@ -130,9 +136,10 @@ export default function SubscriptionPage() {
statusLabels: { TRIALING: 'Trialing', ACTIVE: 'Active', PAST_DUE: 'Past due', CANCELLED: 'Cancelled', CANCELED: 'Canceled', UNPAID: 'Unpaid', EXPIRED: 'Expired', SUSPENDED: 'Suspended' } as Record<string, string>,
invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record<string, string>,
planFeatures: {
STARTER: ['Up to 10 vehicles', '1 user seat', 'Basic analytics', 'Carplace listing'],
GROWTH: ['Up to 50 vehicles', '5 user seats', 'Full analytics', 'Priority listing', 'Custom branding'],
PRO: ['Unlimited vehicles', 'Unlimited seats', 'Advanced reports', 'API access', 'Dedicated support'],
STARTER: ['Up to 25 vehicles', '1 user seat', 'Basic analytics', 'Carplace listing'],
GROWTH: ['Up to 75 vehicles', '5 user seats', 'Full analytics', 'Priority listing', 'Custom branding'],
PRO: ['Up to 150 vehicles', 'Unlimited seats', 'Advanced reports', 'API access', 'Dedicated support'],
ENTERPRISE: ['150+ vehicles', 'Unlimited seats', 'Advanced reports', 'API access', 'Dedicated support'],
} as Record<Plan, string[]>,
},
fr: {
@@ -178,9 +185,10 @@ export default function SubscriptionPage() {
statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', CANCELED: 'Annulé', UNPAID: 'Impayé', EXPIRED: 'Expiré', SUSPENDED: 'Suspendu' } as Record<string, string>,
invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record<string, string>,
planFeatures: {
STARTER: ['Jusqu’à 10 véhicules', '1 utilisateur', 'Analyses de base', 'Présence sur Carplace'],
GROWTH: ['Jusqu’à 50 véhicules', '5 utilisateurs', 'Analyses complètes', 'Mise en avant prioritaire', 'Personnalisation'],
PRO: ['Véhicules illimités', 'Utilisateurs illimités', 'Rapports avancés', 'Accès API', 'Support dédié'],
STARTER: ['Jusqu’à 25 véhicules', '1 utilisateur', 'Analyses de base', 'Présence sur Carplace'],
GROWTH: ['Jusqu’à 75 véhicules', '5 utilisateurs', 'Analyses complètes', 'Mise en avant prioritaire', 'Personnalisation'],
PRO: ['Jusqu’à 150 véhicules', 'Utilisateurs illimités', 'Rapports avancés', 'Accès API', 'Support dédié'],
ENTERPRISE: ['150+ véhicules', 'Utilisateurs illimités', 'Rapports avancés', 'Accès API', 'Support dédié'],
} as Record<Plan, string[]>,
},
ar: {
@@ -226,9 +234,10 @@ export default function SubscriptionPage() {
statusLabels: { TRIALING: 'تجريبي', ACTIVE: 'نشط', PAST_DUE: 'متأخر', CANCELLED: 'ملغى', CANCELED: 'ملغى', UNPAID: 'غير مدفوع', EXPIRED: 'منتهي', SUSPENDED: 'معلّق' } as Record<string, string>,
invoiceStatusLabels: { PAID: 'مدفوع', PENDING: 'قيد الانتظار', FAILED: 'فشل', REFUNDED: 'مسترد' } as Record<string, string>,
planFeatures: {
STARTER: ['حتى 10 مركبات', 'مستخدم واحد', 'تحليلات أساسية', 'إدراج في السوق'],
GROWTH: ['حتى 50 مركبة', '5 مستخدمين', 'تحليلات كاملة', 'إدراج ذو أولوية', 'تخصيص العلامة'],
PRO: ['مركبات غير محدودة', 'مقاعد غير محدودة', 'تقارير متقدمة', 'وصول API', 'دعم مخصص'],
STARTER: ['حتى 25 مركبة', 'مستخدم واحد', 'تحليلات أساسية', 'إدراج في السوق'],
GROWTH: ['حتى 75 مركبة', '5 مستخدمين', 'تحليلات كاملة', 'إدراج ذو أولوية', 'تخصيص العلامة'],
PRO: ['حتى 150 مركبة', 'مقاعد غير محدودة', 'تقارير متقدمة', 'وصول API', 'دعم مخصص'],
ENTERPRISE: ['150+ مركبة', 'مقاعد غير محدودة', 'تقارير متقدمة', 'وصول API', 'دعم مخصص'],
} as Record<Plan, string[]>,
},
}[language]
@@ -430,7 +439,7 @@ export default function SubscriptionPage() {
<div>
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.currentPlan}</p>
<div className="mt-1 flex items-center gap-3">
<h3 className="text-2xl font-bold text-slate-900">{subscription.plan}</h3>
<h3 className="text-2xl font-bold text-slate-900">{PLAN_LABELS[subscription.plan]}</h3>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${STATUS_BADGE[subscription.status] ?? 'bg-slate-100 text-slate-600'}`}>
{copy.statusLabels[subscription.status] ?? subscription.status}
</span>
@@ -496,7 +505,7 @@ export default function SubscriptionPage() {
</div>
{/* Plan cards */}
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{PLANS.map((plan) => {
const price = planPrices[plan]?.[billingPeriod]?.[currency]
const isActive = subscription?.plan === plan && subscription?.status === 'ACTIVE'
@@ -512,7 +521,7 @@ export default function SubscriptionPage() {
} ${isActive ? 'ring-2 ring-green-200' : ''}`}
>
<div className="flex items-center justify-between">
<p className="font-semibold text-slate-900">{plan}</p>
<p className="font-semibold text-slate-900">{PLAN_LABELS[plan]}</p>
{isActive && <span className="badge-green">{copy.active}</span>}
</div>
<p className="mt-2 text-2xl font-black text-slate-900">
+7
View File
@@ -376,6 +376,13 @@ html.dark body {
background-color: rgb(23 37 84 / 0.9);
}
.dark .contract-print-layout,
.dark .contract-print-layout .bg-white {
background-color: #ffffff;
color: #0a0a0a;
color-scheme: light;
}
.dark .border-slate-200,
.dark .border-stone-200 {
border-color: rgb(30 64 175 / 0.50);
@@ -86,7 +86,7 @@ describe("dashboard public auth pages", () => {
const text = collectText(page).join(" ");
const signInLink = findElement(
page,
(element) => element.props.href === "/en/light/sign-in",
(element) => element.props.href === "/en/dark/sign-in",
);
expect(text).toContain("Invitation accepted");
@@ -10,6 +10,19 @@ import { buildHomepageSignInPath } from "@/lib/dashboardPaths";
import { websiteUrl } from "@/lib/urls";
const DASHBOARD_LOGO_SRC = "/dashboard/rentaldrivego.png";
const SUBSCRIPTION_PLANS = ["STARTER", "GROWTH", "PRO"] as const;
type SubscriptionPlan = (typeof SUBSCRIPTION_PLANS)[number];
const PLAN_LABELS: Record<SubscriptionPlan, string> = {
STARTER: "Launch",
GROWTH: "Growth",
PRO: "Pro",
};
function isSubscriptionPlan(value: string | null): value is SubscriptionPlan {
return SUBSCRIPTION_PLANS.some((plan) => plan === value);
}
function notifyParent(message: Record<string, unknown>) {
if (typeof window === "undefined" || window.parent === window) return;
@@ -41,6 +54,8 @@ export default function SignUpForm({
const searchParams = useSearchParams();
const requestedLanguage = searchParams.get("lang");
const requestedTheme = searchParams.get("theme");
const requestedPlan = searchParams.get("plan");
const selectedPlan = isSubscriptionPlan(requestedPlan) ? requestedPlan : null;
const initializedFromQuery = useRef(false);
const initialLanguage =
requestedLanguage === "en" ||
@@ -151,6 +166,7 @@ export default function SignUpForm({
passwordHint: "Minimum 8 characters",
languageLabel: "Preferred language",
create: "Create account",
subscribeToPlan: "Subscribe to {plan}",
creating: "Creating\u2026",
alreadyHave: "Already have an account?",
signIn: "Sign in",
@@ -179,6 +195,7 @@ export default function SignUpForm({
passwordHint: "Minimum 8 caract\u00e8res",
languageLabel: "Langue pr\u00e9f\u00e9r\u00e9e",
create: "Cr\u00e9er un compte",
subscribeToPlan: "Sabonner à {plan}",
creating: "Cr\u00e9ation\u2026",
alreadyHave: "Vous avez d\u00e9j\u00e0 un compte ?",
signIn: "Se connecter",
@@ -214,6 +231,8 @@ export default function SignUpForm({
"\u0627\u0644\u0644\u063a\u0629 \u0627\u0644\u0645\u0641\u0636\u0644\u0629",
create:
"\u0623\u0646\u0634\u0626 \u062d\u0633\u0627\u0628\u064b\u0627",
subscribeToPlan:
"\u0627\u0634\u062a\u0631\u0643 \u0641\u064a {plan}",
creating:
"\u062c\u0627\u0631\u064d \u0627\u0644\u0625\u0646\u0634\u0627\u0621\u2026",
alreadyHave:
@@ -240,6 +259,9 @@ export default function SignUpForm({
language: preferredLanguage,
theme,
});
const submitLabel = selectedPlan
? dict.subscribeToPlan.replace("{plan}", PLAN_LABELS[selectedPlan])
: dict.create;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -261,6 +283,7 @@ export default function SignUpForm({
email,
password,
preferredLanguage,
...(selectedPlan ? { subscriptionPlan: selectedPlan } : {}),
}),
});
@@ -498,7 +521,7 @@ export default function SignUpForm({
disabled={loading}
className="inline-flex w-full justify-center rounded-full bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-orange-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-orange-500 dark:text-white dark:hover:bg-orange-400"
>
{loading ? dict.creating : dict.create}
{loading ? dict.creating : submitLabel}
</button>
</form>
@@ -135,8 +135,8 @@ describe('DashboardAccessGuard route helpers', () => {
})
it('builds sign-in redirects with public dashboard return paths', () => {
expect(buildSignInRedirect('/reservations')).toBe('/en/light/sign-in?redirect=%2Fdashboard%2Freservations')
expect(buildSignInRedirect('/dashboard/fleet')).toBe('/en/light/sign-in?redirect=%2Fdashboard%2Ffleet')
expect(buildSignInRedirect('/reservations')).toBe('/en/dark/sign-in?redirect=%2Fdashboard%2Freservations')
expect(buildSignInRedirect('/dashboard/fleet')).toBe('/en/dark/sign-in?redirect=%2Fdashboard%2Ffleet')
})
it('treats subscription as an owner-only recovery route independent of menu registration', () => {
@@ -21,8 +21,8 @@ describe('dashboard path normalization', () => {
})
it('builds the canonical homepage sign-in URL with a dashboard return path', () => {
expect(buildHomepageSignInPath('/reservations')).toBe('/en/light/sign-in?redirect=%2Fdashboard%2Freservations')
expect(buildHomepageSignInPath('/reservations')).toBe('/en/dark/sign-in?redirect=%2Fdashboard%2Freservations')
expect(buildHomepageSignInPath('/dashboard/fleet', { locale: 'fr', theme: 'dark' })).toBe('/fr/dark/sign-in?redirect=%2Fdashboard%2Ffleet')
expect(buildHomepageSignInPath()).toBe('/en/light/sign-in')
expect(buildHomepageSignInPath()).toBe('/en/dark/sign-in')
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
const DASHBOARD_BASE_PATH = '/dashboard'
const DEFAULT_SIGN_IN_LOCALE = 'en'
const DEFAULT_SIGN_IN_THEME = 'light'
const DEFAULT_SIGN_IN_THEME = 'dark'
export function toDashboardAppPath(path?: string | null): string {
const value = (path ?? '').trim()
+4 -4
View File
@@ -77,7 +77,7 @@ describe('dashboard middleware', () => {
const response = middleware(request('http://dashboard:3001/dashboard/team') as never)
expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/en/light/sign-in?redirect=%2Fdashboard%2Fteam' })
expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/en/dark/sign-in?redirect=%2Fdashboard%2Fteam' })
})
it('redirects unprefixed internal app paths with a public dashboard return path', async () => {
@@ -85,7 +85,7 @@ describe('dashboard middleware', () => {
const response = middleware(request('http://dashboard:3001/reservations') as never)
expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/en/light/sign-in?redirect=%2Fdashboard%2Freservations' })
expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/en/dark/sign-in?redirect=%2Fdashboard%2Freservations' })
})
it('ignores spoofed forwarded host/proto when building the dashboard sign-in redirect', async () => {
@@ -98,7 +98,7 @@ describe('dashboard middleware', () => {
},
}) as never)
expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/en/light/sign-in?redirect=%2Fdashboard%2Fbilling' })
expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/en/dark/sign-in?redirect=%2Fdashboard%2Fbilling' })
})
it('ignores internal forwarded hosts when building the dashboard sign-in redirect', async () => {
@@ -111,7 +111,7 @@ describe('dashboard middleware', () => {
},
}) as never)
expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/en/light/sign-in?redirect=%2Fdashboard%2Ffleet' })
expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/en/dark/sign-in?redirect=%2Fdashboard%2Ffleet' })
})
it('redirects legacy dashboard sign-in requests to the localized homepage sign-in route', async () => {
+1 -1
View File
@@ -5,7 +5,7 @@ const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL ?? 'http://localhost:300
const DASHBOARD_PUBLIC_URL = process.env.NEXT_PUBLIC_DASHBOARD_URL ?? `${WEBSITE_URL.replace(/\/$/, '')}/dashboard`
const DASHBOARD_BASE_PATH = '/dashboard'
const DEFAULT_SIGN_IN_LOCALE = 'en'
const DEFAULT_SIGN_IN_THEME = 'light'
const DEFAULT_SIGN_IN_THEME = 'dark'
function toDashboardAppPath(pathname: string): string {
let normalized = pathname || '/'