Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions app/[locale]/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { unstable_noStore as noStore } from 'next/cache';
import { headers } from 'next/headers';
import { notFound } from 'next/navigation';
import type { ReactNode } from 'react';
import { ArrowLeft } from 'lucide-react';

import { Link } from '@/i18n/navigation';
import type { AppLocale } from '@/i18n/routing';
Expand Down Expand Up @@ -43,8 +44,9 @@ export default async function AdminPolicyPage({
<div className="min-w-0">
<Link
href="/dashboard"
className="text-sm font-semibold text-[#9fb8b2] transition hover:text-white"
className="inline-flex h-10 items-center gap-2 rounded-lg border border-[#234238] bg-[#0f2d26] px-3 text-sm font-bold text-[#b9d1cb] transition hover:border-[#27e0b4] hover:text-[#27e0b4]"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
{copy.back}
</Link>
<h1 className="mt-3 text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
Expand Down Expand Up @@ -80,7 +82,7 @@ export default async function AdminPolicyPage({
</div>
<button
type="submit"
className="inline-flex h-11 items-center justify-center rounded-[10px] bg-[#20D9A3] px-5 text-sm font-extrabold text-[#062b22] transition hover:bg-[#2fe9b1]"
className="inline-flex h-11 items-center justify-center rounded-[10px] bg-[#20D9A3] px-5 text-sm font-extrabold text-[#062b22] shadow-[0_14px_32px_rgba(32,217,163,0.18)] transition hover:bg-[#2fe9b1]"
>
{copy.save}
</button>
Expand Down Expand Up @@ -346,38 +348,38 @@ function TextField({
function getCopy(locale: AppLocale) {
if (locale === 'fr') {
return {
back: 'Retour au dashboard',
title: 'Console administration',
back: 'Retour au tableau de bord',
title: "Console d'administration",
description:
'Modifie les regles produit qui pilotent les quotas, les limites de session et les conditions d acces.',
secured: 'Acces admin',
saved: 'Parametres enregistres.',
"Modifie les règles produit qui pilotent les quotas, les limites de session et les conditions d'accès.",
secured: 'Accès administrateur',
saved: 'Paramètres enregistrés.',
save: 'Enregistrer',
policyMatrix: 'Matrice produit',
accessRules: 'Regles d acces',
accessRules: "Règles d'accès",
userStatus: 'Statut utilisateur',
sessionLimit: 'Limite session',
unlockCondition: 'Condition de deblocage',
newTrialUser: 'Nouvel utilisateur trial',
consistentTrialUser: 'Utilisateur trial regulier',
sessionLimit: 'Limite de session',
unlockCondition: 'Condition de déblocage',
newTrialUser: "Nouvel utilisateur d'essai",
consistentTrialUser: "Utilisateur d'essai régulier",
paidUser: 'Utilisateur payant',
highRiskUser: 'Utilisateur a risque',
sessionsToComplete: 'Sessions a completer',
questionLimit: 'Limite questions',
fullAccess: 'Acces complet',
highRiskUser: 'Utilisateur à risque',
sessionsToComplete: 'Sessions à compléter',
questionLimit: 'Limite de questions',
fullAccess: 'Accès complet',
freeTrial: 'Essai gratuit',
freeQuestionLimit: 'Questions gratuites',
warningThreshold: 'Seuil avertissement',
sessionDefaults: 'Creation de session',
defaultQuestions: 'Questions par defaut',
warningThreshold: "Seuil d'avertissement",
sessionDefaults: 'Création de session',
defaultQuestions: 'Questions par défaut',
maxQuestions: 'Questions max',
minimumMembers: 'Membres minimum',
timersAndCompletion: 'Timers et completion',
timersAndCompletion: 'Timers et complétion',
perQuestionTimer: 'Timer par question',
globalTimer: 'Timer global',
maxTimer: 'Timer max',
completionMin: 'Min completion',
completionMax: 'Max completion',
completionMin: 'Complétion min',
completionMax: 'Complétion max',
};
}

Expand Down
26 changes: 18 additions & 8 deletions app/api/sessions/[sessionId]/start/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { NextResponse } from 'next/server';
import { getTranslations } from 'next-intl/server';

import type { AppLocale } from '@/i18n/routing';
import { getUserTierCapabilities } from '@/lib/billing/user-tier';
import { deriveUserTier, getUserTierCapabilities } from '@/lib/billing/user-tier';
import { createGroupNotifications } from '@/lib/notifications/in-app';
import { createPerfTracker } from '@/lib/observability/perf';
import { getAppPolicySettings } from '@/lib/policy/app-policy';
import { createInitialQuestionFast } from '@/lib/session/flow';
import { createSupabaseAdminClient } from '@/lib/supabase/admin';
import { createSupabaseServerClient } from '@/lib/supabase/server';
Expand Down Expand Up @@ -69,6 +70,7 @@ export async function POST(request: Request, { params }: RouteContext) {

perf.setContext({ locale });
const supabase = createSupabaseServerClient();
const policy = await getAppPolicySettings();
const { data: fastRows, error: fastError } = await (
supabase.schema('public') as unknown as {
rpc: (
Expand Down Expand Up @@ -157,12 +159,12 @@ export async function POST(request: Request, { params }: RouteContext) {
)
.eq('id', sessionId)
.maybeSingle(),
admin
.schema('public')
.from('users')
.select('user_tier')
.eq('id', user.id)
.maybeSingle(),
admin
.schema('public')
.from('users')
.select('questions_answered, has_valid_payment_method, subscription_status')
.eq('id', user.id)
.maybeSingle(),
]);
perf.step('session_and_tier_loaded');

Expand Down Expand Up @@ -197,7 +199,15 @@ export async function POST(request: Request, { params }: RouteContext) {
);
}

const userTier = userTierResult.data?.user_tier ?? 'locked';
const userTier = userTierResult.data
? deriveUserTier({
questionsAnswered: userTierResult.data.questions_answered ?? 0,
hasValidPaymentMethod:
userTierResult.data.has_valid_payment_method ?? false,
subscriptionStatus: userTierResult.data.subscription_status ?? 'none',
policy,
})
: 'locked';
if (!getUserTierCapabilities(userTier).canJoinSessions) {
return NextResponse.json(
{ ok: false, message: await getFeedback('upgradeRequiredToJoinSession') },
Expand Down
14 changes: 11 additions & 3 deletions app/api/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
import { getTranslations } from 'next-intl/server';

import type { AppLocale } from '@/i18n/routing';
import { getUserTierCapabilities } from '@/lib/billing/user-tier';
import { deriveUserTier, getUserTierCapabilities } from '@/lib/billing/user-tier';
import { hasEmailEnv } from '@/lib/env';
import { APP_EVENTS } from '@/lib/logging/events';
import { logAppEvent } from '@/lib/logging/logger';
Expand Down Expand Up @@ -221,7 +221,7 @@ export async function POST(request: Request) {
admin
.schema('public')
.from('users')
.select('user_tier')
.select('questions_answered, has_valid_payment_method, subscription_status')
.eq('id', user.id)
.maybeSingle(),
admin
Expand Down Expand Up @@ -253,7 +253,15 @@ export async function POST(request: Request) {
);
}

const userTier = userTierResult.data?.user_tier ?? 'locked';
const userTier = userTierResult.data
? deriveUserTier({
questionsAnswered: userTierResult.data.questions_answered ?? 0,
hasValidPaymentMethod:
userTierResult.data.has_valid_payment_method ?? false,
subscriptionStatus: userTierResult.data.subscription_status ?? 'none',
policy,
})
: 'locked';
if (!getUserTierCapabilities(userTier).canCreateSession) {
return NextResponse.json(
{
Expand Down
8 changes: 4 additions & 4 deletions lib/policy/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export const DEFAULT_APP_POLICY_SETTINGS: AppPolicySettings = {
completionMinMembers: 2,
completionMaxMembers: 5,
consistentTrialUnlockConditionEn: 'Maintain review completion',
consistentTrialUnlockConditionFr: 'Maintenir la revision',
consistentTrialUnlockConditionFr: 'Maintenir la révision',
paidUnlockConditionEn: 'Immediate access',
paidUnlockConditionFr: 'Acces immediat',
paidUnlockConditionFr: 'Accès immédiat',
highRiskSessionLimitEn: 'Suggested smaller sessions',
highRiskSessionLimitFr: 'Sessions plus courtes suggerees',
highRiskSessionLimitFr: 'Sessions plus courtes suggérées',
highRiskConditionEn: 'Low completion or poor consistency',
highRiskConditionFr: 'Faible completion ou faible regularite',
highRiskConditionFr: 'Faible complétion ou faible régularité',
};

export type SessionCreationPolicy = Pick<
Expand Down
8 changes: 4 additions & 4 deletions supabase/migrations/20260615120000_admin_policy_settings.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ create table if not exists public.app_policy_settings (
completion_min_members integer not null default 2 check (completion_min_members between 1 and 100),
completion_max_members integer not null default 5 check (completion_max_members between 1 and 100),
consistent_trial_unlock_condition_en text not null default 'Maintain review completion',
consistent_trial_unlock_condition_fr text not null default 'Maintenir la revision',
consistent_trial_unlock_condition_fr text not null default 'Maintenir la révision',
paid_unlock_condition_en text not null default 'Immediate access',
paid_unlock_condition_fr text not null default 'Acces immediat',
paid_unlock_condition_fr text not null default 'Accès immédiat',
high_risk_session_limit_en text not null default 'Suggested smaller sessions',
high_risk_session_limit_fr text not null default 'Sessions plus courtes suggerees',
high_risk_session_limit_fr text not null default 'Sessions plus courtes suggérées',
high_risk_condition_en text not null default 'Low completion or poor consistency',
high_risk_condition_fr text not null default 'Faible completion ou faible regularite',
high_risk_condition_fr text not null default 'Faible complétion ou faible régularité',
updated_by uuid references public.users(id) on delete set null,
updated_at timestamptz not null default timezone('utc', now()),
constraint app_policy_settings_trial_warning_lte_limit
Expand Down
30 changes: 30 additions & 0 deletions supabase/migrations/20260615143000_admin_policy_french_copy.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
alter table public.app_policy_settings
alter column consistent_trial_unlock_condition_fr set default 'Maintenir la révision',
alter column paid_unlock_condition_fr set default 'Accès immédiat',
alter column high_risk_session_limit_fr set default 'Sessions plus courtes suggérées',
alter column high_risk_condition_fr set default 'Faible complétion ou faible régularité';

update public.app_policy_settings
set
consistent_trial_unlock_condition_fr = case
when consistent_trial_unlock_condition_fr = 'Maintenir la revision'
then 'Maintenir la révision'
else consistent_trial_unlock_condition_fr
end,
paid_unlock_condition_fr = case
when paid_unlock_condition_fr = 'Acces immediat'
then 'Accès immédiat'
else paid_unlock_condition_fr
end,
high_risk_session_limit_fr = case
when high_risk_session_limit_fr = 'Sessions plus courtes suggerees'
then 'Sessions plus courtes suggérées'
else high_risk_session_limit_fr
end,
high_risk_condition_fr = case
when high_risk_condition_fr = 'Faible completion ou faible regularite'
then 'Faible complétion ou faible régularité'
else high_risk_condition_fr
end,
updated_at = timezone('utc', now())
where id = 'default';
Loading