diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 9a404eaa..d1b0255c 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -1,7 +1,6 @@ import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' import DashboardContent from '@/components/dashboard/DashboardContent' -import { LEGACY_GENERAL_EXTENSIONS } from '@/lib/extensions/toggle-check' import type { Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types' export const dynamic = 'force-dynamic' @@ -51,7 +50,6 @@ export default async function DashboardPage() { { count: postedEntriesCount }, { data: entriesWithDocs }, { data: recentReceiptActivity }, - { data: enabledToggles }, { count: sieImportCount }, { count: staleUncategorizedCount }, ] = await Promise.all([ @@ -76,7 +74,6 @@ export default async function DashboardPage() { supabase.from('journal_entries').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'posted').in('source_type', needsDocSourceTypes), supabase.from('document_attachments').select('journal_entry_id').eq('user_id', user.id).eq('is_current_version', true).not('journal_entry_id', 'is', null), supabase.from('receipts').select('created_at').eq('user_id', user.id).eq('status', 'confirmed').order('created_at', { ascending: false }).limit(30), - supabase.from('extension_toggles').select('sector_slug, extension_slug').eq('user_id', user.id).eq('enabled', true), supabase.from('sie_imports').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'completed'), supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('user_id', user.id).is('journal_entry_id', null).not('is_business', 'eq', false).lt('date', new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]), ]) @@ -224,12 +221,6 @@ export default async function DashboardPage() { staleUncategorizedCount: staleUncategorizedCount || 0, }} onboardingProgress={onboardingProgress} - enabledExtensions={[ - ...(enabledToggles || []), - ...LEGACY_GENERAL_EXTENSIONS - .filter(slug => !(enabledToggles || []).some(t => t.sector_slug === 'general' && t.extension_slug === slug)) - .map(slug => ({ sector_slug: 'general', extension_slug: slug })), - ]} /> ) } diff --git a/app/(dashboard)/receipts/page.tsx b/app/(dashboard)/receipts/page.tsx index 8a8400e8..6e4502d2 100644 --- a/app/(dashboard)/receipts/page.tsx +++ b/app/(dashboard)/receipts/page.tsx @@ -1,7 +1,7 @@ 'use client' import dynamic from 'next/dynamic' -import { useExtensionToggle } from '@/lib/extensions/hooks' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Receipt, Loader2 } from 'lucide-react' @@ -12,18 +12,10 @@ const ReceiptsPageOCR = dynamic( { loading: () =>
} ) +const ocrEnabled = ENABLED_EXTENSION_IDS.has('receipt-ocr') + export default function ReceiptsPage() { - const { enabled, isLoading } = useExtensionToggle('general', 'receipt-ocr') - - if (isLoading) { - return ( -
- -
- ) - } - - if (enabled) { + if (ocrEnabled) { return } diff --git a/app/(dashboard)/receipts/scan/page.tsx b/app/(dashboard)/receipts/scan/page.tsx index dc226b74..77147b62 100644 --- a/app/(dashboard)/receipts/scan/page.tsx +++ b/app/(dashboard)/receipts/scan/page.tsx @@ -1,9 +1,9 @@ 'use client' import dynamic from 'next/dynamic' -import { useExtensionToggle } from '@/lib/extensions/hooks' -import { useRouter } from 'next/navigation' import { useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { Loader2 } from 'lucide-react' const ScanReceiptPageOCR = dynamic( @@ -11,22 +11,19 @@ const ScanReceiptPageOCR = dynamic( { loading: () =>
} ) +const ocrEnabled = ENABLED_EXTENSION_IDS.has('receipt-ocr') + export default function ScanReceiptPage() { - const { enabled, isLoading } = useExtensionToggle('general', 'receipt-ocr') const router = useRouter() useEffect(() => { - if (!isLoading && !enabled) { + if (!ocrEnabled) { router.replace('/receipts') } - }, [isLoading, enabled, router]) + }, [router]) - if (isLoading || !enabled) { - return ( -
- -
- ) + if (!ocrEnabled) { + return null } return diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index d51e56c9..53779af7 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -39,7 +39,6 @@ import { SecuritySettings } from '@/components/settings/SecuritySettings' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' const BankingPanel = getSettingsPanel('enable-banking') -const bankingCompiledIn = ENABLED_EXTENSION_IDS.has('enable-banking') export default function SettingsPage() { const router = useRouter() @@ -50,10 +49,8 @@ export default function SettingsPage() { const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) const [settings, setSettings] = useState(null) - const [hasBankingExtension, setHasBankingExtension] = useState( - bankingCompiledIn ? null : false - ) - const [hasCalendarExtension, setHasCalendarExtension] = useState(false) + const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking') + const hasCalendarExtension = ENABLED_EXTENSION_IDS.has('calendar') const [bankConnectionError, setBankConnectionError] = useState(null) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [deleteConfirmText, setDeleteConfirmText] = useState('') @@ -86,34 +83,10 @@ export default function SettingsPage() { return } - // Fetch settings and extension toggles in parallel - const [settingsRes, bankingToggleRes, calendarToggleRes] = await Promise.all([ - supabase.from('company_settings').select('*').eq('user_id', user.id).single(), - fetch('/api/extensions/toggles/general/enable-banking').catch(() => null), - fetch('/api/extensions/toggles/general/calendar').catch(() => null), - ]) + const settingsRes = await supabase.from('company_settings').select('*').eq('user_id', user.id).single() setSettings(settingsRes.data) - if (bankingToggleRes?.ok) { - const { data } = await bankingToggleRes.json() - setHasBankingExtension(data?.enabled || false) - } else { - // Fallback: check for existing bank connections - const { data: connections } = await supabase - .from('bank_connections') - .select('id') - .eq('user_id', user.id) - .eq('status', 'active') - .limit(1) - setHasBankingExtension((connections && connections.length > 0) || false) - } - - if (calendarToggleRes?.ok) { - const { data } = await calendarToggleRes.json() - setHasCalendarExtension(data?.enabled || false) - } - setIsLoading(false) } @@ -570,11 +543,7 @@ export default function SettingsPage() {
)} - {hasBankingExtension === null ? ( -
- -
- ) : hasBankingExtension && BankingPanel ? ( + {hasBankingExtension && BankingPanel ? ( ) : ( diff --git a/app/(onboarding)/onboarding/page.tsx b/app/(onboarding)/onboarding/page.tsx index 59e4ddae..9d869ec8 100644 --- a/app/(onboarding)/onboarding/page.tsx +++ b/app/(onboarding)/onboarding/page.tsx @@ -9,7 +9,7 @@ import { useToast } from '@/components/ui/use-toast' import { Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration' -import { useExtensionToggle } from '@/lib/extensions/hooks' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import type { CompanyLookupResult } from '@/lib/company-lookup/types' import type { CompanySettings, EntityType, MomsPeriod } from '@/types' @@ -76,7 +76,7 @@ function OnboardingPageContent() { const [isSaving, setIsSaving] = useState(false) const [currentStep, setCurrentStep] = useState(1) const [settings, setSettings] = useState>({}) - const { enabled: ticEnabled } = useExtensionToggle('general', 'tic') + const ticEnabled = ENABLED_EXTENSION_IDS.has('tic') const [ticLookup, setTicLookup] = useState(null) const totalSteps = 5 diff --git a/app/api/extensions/ext/[...path]/__tests__/route.test.ts b/app/api/extensions/ext/[...path]/__tests__/route.test.ts index 8e3f7236..33c475d8 100644 --- a/app/api/extensions/ext/[...path]/__tests__/route.test.ts +++ b/app/api/extensions/ext/[...path]/__tests__/route.test.ts @@ -15,10 +15,6 @@ vi.mock('@/lib/init', () => ({ ensureInitialized: vi.fn(), })) -vi.mock('@/lib/extensions/toggle-check', () => ({ - isExtensionEnabled: vi.fn(), -})) - vi.mock('@/lib/extensions/context-factory', () => ({ createExtensionContext: vi.fn().mockReturnValue({ userId: 'user-1', @@ -28,11 +24,9 @@ vi.mock('@/lib/extensions/context-factory', () => ({ import { createClient } from '@/lib/supabase/server' import { extensionRegistry } from '@/lib/extensions/registry' -import { isExtensionEnabled } from '@/lib/extensions/toggle-check' import { GET, POST } from '../route' const mockCreateClient = vi.mocked(createClient) -const mockIsEnabled = vi.mocked(isExtensionEnabled) function createPathParams(path: string[]) { return { params: Promise.resolve({ path }) } @@ -89,29 +83,6 @@ describe('Extension Catch-All Route', () => { expect(status).toBe(401) }) - it('returns 403 when extension is disabled', async () => { - extensionRegistry.register({ - id: 'test-ext', - name: 'Test', - version: '1.0.0', - apiRoutes: [{ method: 'GET', path: '/data', handler: vi.fn() }], - }) - - const { supabase } = createQueuedMockSupabase() - supabase.auth.getUser.mockResolvedValue({ - data: { user: { id: 'user-1' } }, - error: null, - }) - mockCreateClient.mockResolvedValue(supabase as never) - mockIsEnabled.mockResolvedValue(false) - - const request = createMockRequest('/api/extensions/ext/test-ext/data') - const response = await GET(request, createPathParams(['test-ext', 'data'])) - const { status } = await parseJsonResponse(response) - - expect(status).toBe(403) - }) - it('returns 404 for unmatched method/path', async () => { extensionRegistry.register({ id: 'test-ext', @@ -126,7 +97,6 @@ describe('Extension Catch-All Route', () => { error: null, }) mockCreateClient.mockResolvedValue(supabase as never) - mockIsEnabled.mockResolvedValue(true) // GET doesn't match POST /data const request = createMockRequest('/api/extensions/ext/test-ext/data') @@ -154,7 +124,6 @@ describe('Extension Catch-All Route', () => { error: null, }) mockCreateClient.mockResolvedValue(supabase as never) - mockIsEnabled.mockResolvedValue(true) const request = createMockRequest('/api/extensions/ext/enable-banking/banks') const response = await GET(request, createPathParams(['enable-banking', 'banks'])) @@ -185,7 +154,6 @@ describe('Extension Catch-All Route', () => { error: null, }) mockCreateClient.mockResolvedValue(supabase as never) - mockIsEnabled.mockResolvedValue(true) const request = createMockRequest('/api/extensions/ext/test-ext/connect', { method: 'POST', diff --git a/app/api/extensions/ext/[...path]/route.ts b/app/api/extensions/ext/[...path]/route.ts index 0ddc3f78..d7affc1a 100644 --- a/app/api/extensions/ext/[...path]/route.ts +++ b/app/api/extensions/ext/[...path]/route.ts @@ -3,7 +3,6 @@ import { NextResponse } from 'next/server' import { ensureInitialized } from '@/lib/init' import { extensionRegistry } from '@/lib/extensions/registry' import { createExtensionContext } from '@/lib/extensions/context-factory' -import { isExtensionEnabled } from '@/lib/extensions/toggle-check' import { hasAiConsent, isAiExtension } from '@/lib/extensions/ai-consent' import type { ApiRouteDefinition } from '@/lib/extensions/types' @@ -118,13 +117,6 @@ async function handleRequest( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // Toggle check — use extension's declared sector, fallback to 'general' - const sector = extension.sector || 'general' - const enabled = await isExtensionEnabled(user.id, sector, extensionId) - if (!enabled) { - return NextResponse.json({ error: 'Extension is disabled' }, { status: 403 }) - } - // AI consent check if (isAiExtension(extensionId)) { const consented = await hasAiConsent(supabase, user.id, extensionId) diff --git a/app/api/extensions/toggles/[sector]/[slug]/route.ts b/app/api/extensions/toggles/[sector]/[slug]/route.ts deleted file mode 100644 index f7f27175..00000000 --- a/app/api/extensions/toggles/[sector]/[slug]/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createClient } from '@/lib/supabase/server' -import { NextResponse } from 'next/server' -import { LEGACY_GENERAL_EXTENSIONS } from '@/lib/extensions/toggle-check' - -export async function GET( - _request: Request, - { params }: { params: Promise<{ sector: string; slug: string }> } -) { - const { sector, slug } = await params - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { data } = await supabase - .from('extension_toggles') - .select('*') - .eq('user_id', user.id) - .eq('sector_slug', sector) - .eq('extension_slug', slug) - .single() - - if (data) { - return NextResponse.json({ data }) - } - - // No toggle row: legacy general extensions default to enabled - const defaultEnabled = - sector === 'general' && LEGACY_GENERAL_EXTENSIONS.includes(slug) - return NextResponse.json({ data: { enabled: defaultEnabled } }) -} - -export async function DELETE( - _request: Request, - { params }: { params: Promise<{ sector: string; slug: string }> } -) { - const { sector, slug } = await params - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { error } = await supabase - .from('extension_toggles') - .delete() - .eq('user_id', user.id) - .eq('sector_slug', sector) - .eq('extension_slug', slug) - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }) - } - - return NextResponse.json({ success: true }) -} diff --git a/app/api/extensions/toggles/__tests__/route.test.ts b/app/api/extensions/toggles/__tests__/route.test.ts deleted file mode 100644 index d49a6f95..00000000 --- a/app/api/extensions/toggles/__tests__/route.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { - createMockRequest, - parseJsonResponse, - createQueuedMockSupabase, - makeExtensionToggle, -} from '@/tests/helpers' - -const { supabase: mockSupabase, enqueue, reset } = createQueuedMockSupabase() -vi.mock('@/lib/supabase/server', () => ({ - createClient: () => Promise.resolve(mockSupabase), -})) - -import { GET, POST } from '../route' - -describe('GET /api/extensions/toggles', () => { - const mockUser = { id: 'user-1', email: 'test@test.se' } - - beforeEach(() => { - vi.clearAllMocks() - reset() - mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } }) - }) - - it('returns 401 when not authenticated', async () => { - mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } }) - - const response = await GET() - const { status, body } = await parseJsonResponse(response) - - expect(status).toBe(401) - expect(body).toEqual({ error: 'Unauthorized' }) - }) - - it('returns enabled toggles for user', async () => { - const toggles = [ - makeExtensionToggle({ extension_slug: 'receipt-ocr' }), - makeExtensionToggle({ extension_slug: 'ai-chat' }), - ] - enqueue({ data: toggles, error: null }) - - const response = await GET() - const { status, body } = await parseJsonResponse<{ data: unknown[] }>(response) - - expect(status).toBe(200) - expect(body.data).toEqual(toggles) - expect(mockSupabase.from).toHaveBeenCalledWith('extension_toggles') - }) -}) - -describe('POST /api/extensions/toggles', () => { - const mockUser = { id: 'user-1', email: 'test@test.se' } - - beforeEach(() => { - vi.clearAllMocks() - reset() - mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } }) - }) - - it('returns 401 when not authenticated', async () => { - mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } }) - - const request = createMockRequest('/api/extensions/toggles', { - method: 'POST', - body: { sector_slug: 'general', extension_slug: 'receipt-ocr', enabled: true }, - }) - const response = await POST(request) - const { status, body } = await parseJsonResponse(response) - - expect(status).toBe(401) - expect(body).toEqual({ error: 'Unauthorized' }) - }) - - it('returns 400 when missing fields', async () => { - const request = createMockRequest('/api/extensions/toggles', { - method: 'POST', - body: { sector_slug: 'general' }, - }) - const response = await POST(request) - const { status, body } = await parseJsonResponse<{ error: string }>(response) - - expect(status).toBe(400) - expect(body.error).toBe('sector_slug, extension_slug, and enabled are required') - }) - - it('upserts toggle and returns data', async () => { - const toggle = makeExtensionToggle({ - sector_slug: 'general', - extension_slug: 'receipt-ocr', - enabled: true, - }) - enqueue({ data: toggle, error: null }) - - const request = createMockRequest('/api/extensions/toggles', { - method: 'POST', - body: { sector_slug: 'general', extension_slug: 'receipt-ocr', enabled: true }, - }) - const response = await POST(request) - const { status, body } = await parseJsonResponse<{ data: unknown }>(response) - - expect(status).toBe(200) - expect(body.data).toEqual(toggle) - expect(mockSupabase.from).toHaveBeenCalledWith('extension_toggles') - }) -}) diff --git a/app/api/extensions/toggles/route.ts b/app/api/extensions/toggles/route.ts deleted file mode 100644 index 62e4a6a8..00000000 --- a/app/api/extensions/toggles/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { createClient } from '@/lib/supabase/server' -import { NextResponse } from 'next/server' - -export async function GET() { - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { data, error } = await supabase - .from('extension_toggles') - .select('*') - .eq('user_id', user.id) - .eq('enabled', true) - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }) - } - - return NextResponse.json({ data }) -} - -export async function POST(request: Request) { - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - - if (!user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const body = await request.json() - const { sector_slug, extension_slug, enabled } = body - - if (!sector_slug || !extension_slug || typeof enabled !== 'boolean') { - return NextResponse.json( - { error: 'sector_slug, extension_slug, and enabled are required' }, - { status: 400 } - ) - } - - const { data, error } = await supabase - .from('extension_toggles') - .upsert( - { - user_id: user.id, - sector_slug, - extension_slug, - enabled, - }, - { onConflict: 'user_id,sector_slug,extension_slug' } - ) - .select() - .single() - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }) - } - - return NextResponse.json({ data }) -} diff --git a/components/chat/ChatWidget.tsx b/components/chat/ChatWidget.tsx index c74ea0c0..c38f8361 100644 --- a/components/chat/ChatWidget.tsx +++ b/components/chat/ChatWidget.tsx @@ -5,41 +5,12 @@ import { Button } from '@/components/ui/button' import { ChatPanel } from './ChatPanel' import { MessageCircle, X } from 'lucide-react' import { cn } from '@/lib/utils' +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' + +const chatEnabled = ENABLED_EXTENSION_IDS.has('ai-chat') export function ChatWidget() { const [isOpen, setIsOpen] = useState(false) - // Default to true for legacy compatibility (ai-chat defaults to enabled) - const [enabled, setEnabled] = useState(true) - - // Fetch initial toggle state - useEffect(() => { - const check = async () => { - try { - const res = await fetch('/api/extensions/toggles/general/ai-chat') - if (res.ok) { - const { data } = await res.json() - // Legacy: enabled by default when no toggle row exists - setEnabled(data?.enabled ?? true) - } - } catch { - // Keep default (enabled) on fetch failure - } - } - check() - }, []) - - // Listen for real-time toggle changes - useEffect(() => { - const handler = (e: Event) => { - const { sectorSlug, extensionSlug, enabled: newValue } = (e as CustomEvent).detail - if (sectorSlug === 'general' && extensionSlug === 'ai-chat') { - setEnabled(newValue) - if (!newValue) setIsOpen(false) - } - } - window.addEventListener('extension-toggle-changed', handler) - return () => window.removeEventListener('extension-toggle-changed', handler) - }, []) // Allow other components to open the chat via custom event useEffect(() => { @@ -48,7 +19,7 @@ export function ChatWidget() { return () => window.removeEventListener('open-ai-chat', handler) }, []) - if (!enabled) return null + if (!chatEnabled) return null return ( <> diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx index ea4e23fc..24d4267c 100644 --- a/components/dashboard/DashboardContent.tsx +++ b/components/dashboard/DashboardContent.tsx @@ -19,7 +19,7 @@ import { FileWarning, Clock, } from 'lucide-react' -import { getExtensionDefinition } from '@/lib/extensions/sectors' +import { getAllExtensions } from '@/lib/extensions/sectors' import { resolveIcon } from '@/lib/extensions/icon-resolver' import type { QuickActionDefinition } from '@/lib/extensions/types' import type { CompanySettings, Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types' @@ -47,31 +47,11 @@ interface DashboardContentProps { staleUncategorizedCount: number } onboardingProgress?: OnboardingProgress - enabledExtensions?: { sector_slug: string; extension_slug: string }[] } -export default function DashboardContent({ firstName, settings, summary, onboardingProgress, enabledExtensions }: DashboardContentProps) { +export default function DashboardContent({ firstName, settings, summary, onboardingProgress }: DashboardContentProps) { const [showAllAlerts, setShowAllAlerts] = useState(false) const [showMore, setShowMore] = useState(false) - const [liveExtensions, setLiveExtensions] = useState(enabledExtensions ?? []) - - useEffect(() => { - setLiveExtensions(enabledExtensions ?? []) - }, [enabledExtensions]) - - useEffect(() => { - const handler = ((e: CustomEvent<{ sector_slug: string; extension_slug: string; enabled: boolean }>) => { - setLiveExtensions(prev => { - if (e.detail.enabled) { - if (prev.some(x => x.sector_slug === e.detail.sector_slug && x.extension_slug === e.detail.extension_slug)) return prev - return [...prev, { sector_slug: e.detail.sector_slug, extension_slug: e.detail.extension_slug }] - } - return prev.filter(x => !(x.sector_slug === e.detail.sector_slug && x.extension_slug === e.detail.extension_slug)) - }) - }) as EventListener - window.addEventListener('extension-toggle-changed', handler) - return () => window.removeEventListener('extension-toggle-changed', handler) - }, []) // Setup gate — blocks dashboard until user imports data or chooses fresh start const needsSetup = onboardingProgress && !onboardingProgress.hasBankConnected && !onboardingProgress.hasSIEImport @@ -256,14 +236,10 @@ export default function DashboardContent({ firstName, settings, summary, onboard const visibleAlerts = showAllAlerts ? alertItems : alertItems.slice(0, MAX_VISIBLE_ALERTS) const hasMoreAlerts = alertItems.length > MAX_VISIBLE_ALERTS - // Build extension quick actions from enabled extensions - const extensionQuickActions: (QuickActionDefinition & { key: string })[] = liveExtensions - .map(toggle => { - const def = getExtensionDefinition(toggle.sector_slug, toggle.extension_slug) - if (!def?.quickAction) return null - return { ...def.quickAction, key: `${toggle.sector_slug}/${toggle.extension_slug}` } - }) - .filter((a): a is QuickActionDefinition & { key: string } => a !== null) + // Build extension quick actions from all compiled extensions + const extensionQuickActions: (QuickActionDefinition & { key: string })[] = getAllExtensions() + .filter(def => def.quickAction) + .map(def => ({ ...def.quickAction!, key: `${def.sector}/${def.slug}` })) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) // Quick action items diff --git a/components/extensions/ExtensionCard.tsx b/components/extensions/ExtensionCard.tsx index df33a109..1aef5a2c 100644 --- a/components/extensions/ExtensionCard.tsx +++ b/components/extensions/ExtensionCard.tsx @@ -1,43 +1,34 @@ -'use client' - import { Card, CardContent } from '@/components/ui/card' import { resolveIcon } from '@/lib/extensions/icon-resolver' import type { ExtensionDefinition } from '@/lib/extensions/types' import CategoryBadge from './CategoryBadge' -import ExtensionToggleButton from './ExtensionToggleButton' import Link from 'next/link' export default function ExtensionCard({ extension }: { extension: ExtensionDefinition }) { - + const Icon = resolveIcon(extension.icon) return ( -
-
-
- -
-
- - {extension.name} - -

- {extension.description} -

-
- -
+
+
+ +
+
+ + {extension.name} + +

+ {extension.description} +

+
+
-
diff --git a/components/extensions/ExtensionToggleButton.tsx b/components/extensions/ExtensionToggleButton.tsx deleted file mode 100644 index 38d7860b..00000000 --- a/components/extensions/ExtensionToggleButton.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Switch } from '@/components/ui/switch' -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { useExtensionToggle } from '@/lib/extensions/hooks' - -export default function ExtensionToggleButton({ - sectorSlug, - extensionSlug, - subscriptionNotice, -}: { - sectorSlug: string - extensionSlug: string - subscriptionNotice?: string -}) { - const { enabled, isLoading, toggle } = useExtensionToggle(sectorSlug, extensionSlug) - const [showConfirm, setShowConfirm] = useState(false) - - function handleToggle() { - // Show confirmation dialog when enabling an extension with a subscription notice - if (!enabled && subscriptionNotice) { - setShowConfirm(true) - return - } - toggle() - } - - function handleConfirm() { - setShowConfirm(false) - toggle() - } - - return ( - <> - - - {subscriptionNotice && ( - - - - Extern prenumeration krävs - {subscriptionNotice} - - - - - - - - )} - - ) -} diff --git a/extensions.md b/extensions.md index 784a0a27..10b05a18 100644 --- a/extensions.md +++ b/extensions.md @@ -21,13 +21,13 @@ That's the core. It doesn't include receipt scanning, AI categorization, AI chat ## Extensions -Extensions are **everything beyond the core accounting system**. They are self-contained tools that a user adds to their dashboard. No extensions are active by default — the user chooses which ones they want. +Extensions are **everything beyond the core accounting system**. They are self-contained tools that a user adds to their dashboard. All compiled extensions are active for all users — the operator decides which extensions to include via `extensions.config.json` at build time. There are two kinds of extensions: ### General Extensions -General extensions are not tied to any specific business sector. They're useful for any business but they go beyond what a standard accounting system offers. They are optional — the user toggles them on from the marketplace. +General extensions are not tied to any specific business sector. They're useful for any business but they go beyond what a standard accounting system offers. Examples: - **Receipt OCR** — Scan receipts and extract data automatically @@ -36,7 +36,7 @@ Examples: - **Push Notifications** — Event notifications for accounting activities - **Enable Banking** — PSD2 automatic bank transaction sync -These are configured via `extensions.config.json` and only loaded when explicitly enabled. Users toggle them on/off from the marketplace. +These are configured via `extensions.config.json` and only loaded when explicitly enabled by the operator. ### Sector Extensions @@ -142,13 +142,12 @@ Some extensions need data that doesn't exist in any accounting system. No system **Pattern C: Both** Some extensions combine core accounting data with user-submitted data. "Earnings Per Alcohol Liter" reads alcohol revenue from the bookkeeping (Pattern A) and takes user-entered liter counts (Pattern B) to calculate revenue per liter. -### 3. Full marketplace for post-onboarding management +### 3. Extension marketplace for browsing -After onboarding, users have a dedicated "Extensions" marketplace page where they can: -- Browse all available extensions (general + all sectors) +Users have a dedicated "Extensions" marketplace page where they can: +- Browse all compiled extensions (general + all sectors) - Read descriptions and details -- Toggle extensions on/off at any time -- Discover extensions from sectors other than their primary one +- Open extension workspaces ### 4. Primary sector with cross-sector browsing @@ -168,13 +167,10 @@ We build all extensions ourselves initially. But the architecture should be clea ## The User Experience 1. User signs up, goes through onboarding -2. During onboarding, they select their business sector ("Restaurant & Cafe") -3. The app suggests extensions: general extensions + extensions for that sector -4. User toggles on the ones they want -5. On the dashboard, the sidebar has a **"Your Extensions"** section listing all enabled extensions -6. Clicking an extension opens its workspace — a dedicated page with the extension's own UI -7. The user interacts with the extension: views data, enters inputs, sees calculations/reports -8. User can browse the marketplace anytime to add/remove extensions +2. On the dashboard, the sidebar shows links to compiled extensions that have a workspace + quickAction +3. Clicking an extension opens its workspace — a dedicated page with the extension's own UI +4. The user interacts with the extension: views data, enters inputs, sees calculations/reports +5. User can browse the marketplace to see all compiled extensions --- @@ -381,15 +377,14 @@ lib/ types.ts ← Extension, ExtensionDefinition, Sector types sectors.ts ← Sector shells + generated extension definitions workspace-registry.tsx ← Maps sector/slug → lazy-loaded React component - hooks.ts ← useExtensionToggle, useEnabledExtensions loader.ts ← Imports from _generated, registers extensions registry.ts ← Runtime extension registry (get, register) - toggle-check.ts ← isExtensionEnabled() for auth gates context-factory.ts ← Builds ExtensionContext for handlers _generated/ ← AUTO-GENERATED by npm run setup:extensions extension-list.ts ← FIRST_PARTY_EXTENSIONS array (static imports) workspace-map.tsx ← Lazy-loaded workspace components sector-definitions.ts ← ExtensionDefinition[] per sector + enabled-extensions.ts ← ENABLED_EXTENSION_IDS set for build-time checks email/ service.ts ← EmailService interface + no-op default + getEmailService() reports/ @@ -435,7 +430,7 @@ app/(dashboard)/ [sector]/ page.tsx ← Extensions for a specific sector [extension]/ - page.tsx ← Extension detail + toggle + page.tsx ← Extension detail + workspace link e/ ← Extension workspaces [sector]/ [slug]/ @@ -461,27 +456,23 @@ key: 'entries' → [{ "date": "2025-01-15", "liters": 42.5, "type": "spirit key: 'config' → { "revenueAccounts": ["3001"], "trackByType": true } ``` -### The Toggle System +### Extension Enablement -New database table: +Extensions are enabled at **build time** via `extensions.config.json`. All compiled extensions are active for all users — there is no per-user toggle system. The operator (hosted or self-hosted) decides which extensions to include. -```sql -create table extension_toggles ( - id uuid primary key default uuid_generate_v4(), - user_id uuid not null references auth.users on delete cascade, - sector_slug text not null, -- 'general' | 'restaurant' | 'construction' | etc. - extension_slug text not null, - enabled boolean not null default true, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now(), - constraint extension_toggles_unique unique (user_id, sector_slug, extension_slug) -); +To check if an extension is compiled in at runtime (e.g. for conditional UI), use the build-time constant: + +```typescript +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' + +if (ENABLED_EXTENSION_IDS.has('receipt-ocr')) { + // Show OCR UI +} ``` -Also add to company_settings: -```sql -alter table company_settings add column sector_slug text; -``` +AI extensions (`receipt-ocr`, `ai-categorization`, `ai-chat`) additionally require per-user AI consent before making API calls. This is a separate system using the `extension_data` table, managed by `lib/extensions/ai-consent.ts`. + +> **Note:** The `extension_toggles` database table still exists but is no longer queried by any code. It can be dropped in a future migration. ### API Routes for Extensions @@ -525,8 +516,8 @@ URL scheme: `/api/extensions/ext/{extensionId}/{...routePath}` The dispatcher handles: 1. **Auth check** — 401 if not logged in 2. **Extension lookup** — 404 if extension not registered or has no apiRoutes -3. **Toggle check** — 403 if extension is disabled for the user -4. **Path matching** — Matches method + path pattern (supports `:param` wildcards) +3. **Path matching** — Matches method + path pattern (supports `:param` wildcards) +4. **AI consent check** — 403 if AI extension and user hasn't consented 5. **Param extraction** — Path params like `:id` are added as `_id` search params 6. **Context building** — Creates `ExtensionContext` with supabase, userId, settings, storage, logger 7. **Dispatch** — Calls the matched handler with the request and context @@ -541,7 +532,7 @@ app/api/extensions/[sector]/[slug]/ ### Sidebar Integration -The sidebar (`DashboardNav.tsx`) gets a new section: **"Your Extensions"**. It reads enabled extensions from `extension_toggles` and renders links: +The sidebar (`DashboardNav.tsx`) has a section for extensions. It shows all compiled extensions that have a workspace and a `quickAction` with an `href`: ``` ── Your Extensions ────────── @@ -553,21 +544,6 @@ The sidebar (`DashboardNav.tsx`) gets a new section: **"Your Extensions"**. It r Each link goes to `/e/{sector}/{slug}` which renders the extension's workspace component. -### Onboarding Integration - -Add two new steps to the onboarding flow (after entity type selection): - -**Step 2: Sector Selection** -"What type of business do you run?" -Grid of sectors with icons and descriptions. User picks one. -Stores `sector_slug` on `company_settings`. - -**Step 3: Extension Suggestions** -"Here are tools for your business. Pick the ones you want." -Shows general extensions + extensions for the selected sector, grouped by category. -User toggles desired extensions. Inserts into `extension_toggles`. -Can be skipped — user can always add extensions later from the marketplace. - --- ## The Extension Workspace Pattern @@ -620,7 +596,7 @@ The previous architecture had extensions "always loaded" via hardcoded static im 2. **Services pattern** -- Extensions can expose named services via `services?: Record Promise>` on the Extension interface. Core code uses `extensionRegistry.get('ext-id')?.services?.methodName` for runtime lookup instead of direct imports. This is how `ai-categorization` provides template embedding functions to core booking logic. -3. **Catch-all API dispatcher** -- Extension API routes are registered via `apiRoutes: ApiRouteDefinition[]` on the Extension object. The catch-all at `/api/extensions/ext/[...path]/route.ts` handles auth, toggle checks, path param extraction, and dispatches to the handler. URL pattern: `/api/extensions/ext/{extensionId}/{path}`. +3. **Catch-all API dispatcher** -- Extension API routes are registered via `apiRoutes: ApiRouteDefinition[]` on the Extension object. The catch-all at `/api/extensions/ext/[...path]/route.ts` handles auth, AI consent checks, path param extraction, and dispatches to the handler. URL pattern: `/api/extensions/ext/{extensionId}/{path}`. 4. **SRU/NE-bilaga are core** -- These tax compliance features were moved from `extensions/` into `lib/reports/sru-export/` and `lib/reports/ne-bilaga/`. They are always available regardless of extension configuration. @@ -643,14 +619,14 @@ The previous architecture had extensions "always loaded" via hardcoded static im |------|--------|-------| | Extension types (ExtensionDefinition, Sector, etc.) | Done | `lib/extensions/types.ts` | | Sector data registry | Done | `lib/extensions/sectors.ts` + generated definitions | -| Database migration (extension_toggles + sector_slug) | Done | Migration 037 | -| Toggle hooks (useExtensionToggle, useEnabledExtensions) | Done | `lib/extensions/hooks.ts` | +| Database migration (extension_toggles) | Done | Migration 037 (table exists but no longer queried) | +| Build-time extension check | Done | `ENABLED_EXTENSION_IDS` in `_generated/enabled-extensions.ts` | | Workspace component registry | Done | `lib/extensions/workspace-registry.tsx` + generated map | | Workspace routing (`/e/[sector]/[slug]`) | Done | `app/(dashboard)/e/[sector]/[slug]/page.tsx` | | Workspace shell | Done | `components/extensions/ExtensionWorkspaceShell.tsx` | | Marketplace pages | Done | `app/(dashboard)/extensions/` | | Sidebar "Your Extensions" | Done | Wired into DashboardNav | -| Onboarding steps (sector selection + extension suggestions) | Done | Onboarding flow | +| Onboarding with build-time extension checks | Done | Uses `ENABLED_EXTENSION_IDS` | | Shared UI components | Done | KPICard, DataEntryForm, DateRangeFilter, etc. | | Extension API routes (generic CRUD) | Done | `app/api/extensions/[sector]/[slug]/` | | Catch-all API dispatcher | Done | `app/api/extensions/ext/[...path]/route.ts` | @@ -660,7 +636,7 @@ The previous architecture had extensions "always loaded" via hardcoded static im | Manifest files for all extensions | Done | 25 manifest.json files | | Code generator | Done | `scripts/generate-extension-registry.ts` | | extensions.config.json opt-in | Done | Core runs with empty config | -| Migrate general extensions to toggle system | Done | All general extensions have manifests | +| General extensions with manifests | Done | All general extensions have manifests | | Export sector extensions | Done | EU Sales List, Intrastat, VAT Monitor, Currency Receivables | | Restaurant sector extensions | Done | Food Cost, Earnings Per Liter, POS Import, Tip Tracking | | Construction sector extensions | Done | ROT Calculator, Project Cost | @@ -843,7 +819,7 @@ export const myExtension: Extension = { } ``` -Events are one-way: core services emit, extensions subscribe. Extensions should never emit events back to core. The toggle check is enforced by the event handler registration -- if an extension is not loaded (not in `extensions.config.json`), its handlers are never registered. +Events are one-way: core services emit, extensions subscribe. Extensions should never emit events back to core. If an extension is not compiled in (not in `extensions.config.json`), its handlers are never registered. ### Workspace Components @@ -889,6 +865,6 @@ This pattern can be reused for any capability that should degrade gracefully whe - **Sector extensions** (food cost %, earnings per liter, etc.) -- tied to a specific market sector - **Export extensions** (EU Sales List, Intrastat, etc.) -- for businesses with international trade -All extensions live in the same system, use the same toggle mechanism, appear in the same marketplace, and show up under "Your Extensions" in the sidebar. No extensions are active by default -- operators choose which to enable in `extensions.config.json`, and users toggle them on/off from the marketplace. +All extensions live in the same system, appear in the same marketplace, and show up in the sidebar when they have a workspace + quickAction. All compiled extensions are active for all users -- the operator chooses which to include in `extensions.config.json` at build time. AI extensions additionally require per-user consent before making API calls. Extensions are read-only with respect to the core accounting system. They can be fed accounting data, they can accept manual user input, but they never write back to the bookkeeping. They can expose services to core via the registry lookup pattern, and they can register API routes that are dispatched by the catch-all handler. diff --git a/lib/extensions/__tests__/toggle-check.test.ts b/lib/extensions/__tests__/toggle-check.test.ts deleted file mode 100644 index 3a83d24f..00000000 --- a/lib/extensions/__tests__/toggle-check.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { createMockSupabase } from '@/tests/helpers' - -const { supabase: mockSupabase, mockResult } = createMockSupabase() -vi.mock('@/lib/supabase/server', () => ({ - createServiceClient: () => Promise.resolve(mockSupabase), -})) - -import { isExtensionEnabled } from '../toggle-check' - -describe('isExtensionEnabled', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('returns true when toggle exists and is enabled', async () => { - mockResult({ data: { enabled: true }, error: null }) - - const result = await isExtensionEnabled('user-1', 'general', 'receipt-ocr') - - expect(result).toBe(true) - expect(mockSupabase.from).toHaveBeenCalledWith('extension_toggles') - }) - - it('returns false when toggle exists and is disabled', async () => { - mockResult({ data: { enabled: false }, error: null }) - - const result = await isExtensionEnabled('user-1', 'general', 'receipt-ocr') - - expect(result).toBe(false) - }) - - it('returns true for legacy general extensions when no toggle row exists', async () => { - mockResult({ data: null, error: null }) - - const result = await isExtensionEnabled('user-1', 'general', 'receipt-ocr') - - expect(result).toBe(true) - }) - - it('returns false for non-legacy extensions when no toggle row exists', async () => { - mockResult({ data: null, error: null }) - - const result = await isExtensionEnabled('user-1', 'restaurant', 'tip-tracking') - - expect(result).toBe(false) - }) -}) diff --git a/lib/extensions/hooks.ts b/lib/extensions/hooks.ts deleted file mode 100644 index ba41a3b6..00000000 --- a/lib/extensions/hooks.ts +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback } from 'react' -import type { ExtensionToggle } from './types' - -export function useEnabledExtensions() { - const [extensions, setExtensions] = useState([]) - const [isLoading, setIsLoading] = useState(true) - - const refresh = useCallback(async () => { - setIsLoading(true) - try { - const res = await fetch('/api/extensions/toggles') - if (res.ok) { - const { data } = await res.json() - setExtensions(data ?? []) - } - } finally { - setIsLoading(false) - } - }, []) - - useEffect(() => { - refresh() - }, [refresh]) - - return { extensions, isLoading, refresh } -} - -export function useExtensionToggle(sectorSlug: string, extensionSlug: string) { - const [enabled, setEnabled] = useState(false) - const [isLoading, setIsLoading] = useState(true) - - useEffect(() => { - const check = async () => { - setIsLoading(true) - try { - const res = await fetch(`/api/extensions/toggles/${sectorSlug}/${extensionSlug}`) - if (res.ok) { - const { data } = await res.json() - setEnabled(data?.enabled ?? false) - } - } finally { - setIsLoading(false) - } - } - check() - }, [sectorSlug, extensionSlug]) - - const toggle = useCallback(async () => { - const newValue = !enabled - setEnabled(newValue) // Optimistic update - try { - const res = await fetch('/api/extensions/toggles', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - sector_slug: sectorSlug, - extension_slug: extensionSlug, - enabled: newValue, - }), - }) - if (!res.ok) { - setEnabled(!newValue) // Revert on error - } else { - // Notify other components about the toggle change - window.dispatchEvent(new CustomEvent('extension-toggle-changed', { - detail: { sectorSlug, extensionSlug, enabled: newValue }, - })) - } - } catch { - setEnabled(!newValue) // Revert on error - } - }, [enabled, sectorSlug, extensionSlug]) - - return { enabled, isLoading, toggle } -} diff --git a/lib/extensions/index.ts b/lib/extensions/index.ts index 80f55d60..ebfd200a 100644 --- a/lib/extensions/index.ts +++ b/lib/extensions/index.ts @@ -2,14 +2,9 @@ export { extensionRegistry } from './registry' export { loadExtensions } from './loader' export type { Extension, - RouteDefinition, ApiRouteDefinition, - SidebarItem, - ReportDefinition, - SettingsPanelDefinition, - TaxCodeDefinition, - DimensionDefinition, MappingRuleTypeDefinition, ExtensionEventHandler, ExtensionContext, + SettingsPanelDefinition, } from './types' diff --git a/lib/extensions/sectors.ts b/lib/extensions/sectors.ts index b8eb378a..777452ba 100644 --- a/lib/extensions/sectors.ts +++ b/lib/extensions/sectors.ts @@ -48,21 +48,12 @@ export function getExtensionsBySector(slug: SectorSlug): ExtensionDefinition[] { return getSector(slug)?.extensions ?? [] } -/** Extensions with a workspace and a quickAction href — for sidebar nav. - * Filters against the user's enabled extensions when provided. */ -export function getExtensionNavItems( - enabledExtensions?: { sector_slug: string; extension_slug: string }[] -): { href: string; label: string; icon: string }[] { +/** Extensions with a workspace and a quickAction href — for sidebar nav. */ +export function getExtensionNavItems(): { href: string; label: string; icon: string }[] { return getAllExtensions() .filter(e => { const key = `${e.sector}/${e.slug}` - if (!(key in WORKSPACES) || !e.quickAction?.href) return false - if (enabledExtensions) { - return enabledExtensions.some( - t => t.sector_slug === e.sector && t.extension_slug === e.slug - ) - } - return true + return key in WORKSPACES && !!e.quickAction?.href }) .sort((a, b) => (a.quickAction!.order ?? 0) - (b.quickAction!.order ?? 0)) .map(e => ({ diff --git a/lib/extensions/toggle-check.ts b/lib/extensions/toggle-check.ts deleted file mode 100644 index 87874e31..00000000 --- a/lib/extensions/toggle-check.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createServiceClient } from '@/lib/supabase/server' - -/** - * Check if an extension is enabled for a specific user. - * Used by event handlers to gate execution. - * - * For backward compatibility during the transition period, - * general extensions that were previously always-on default to - * enabled when no toggle row exists. - */ -export const LEGACY_GENERAL_EXTENSIONS = [ - 'receipt-ocr', - 'ai-categorization', - 'ai-chat', - 'push-notifications', - 'enable-banking', - 'email', - 'arcim-migration', - 'tic', -] - -export async function isExtensionEnabled( - userId: string, - sectorSlug: string, - extensionSlug: string -): Promise { - const supabase = await createServiceClient() - - const { data } = await supabase - .from('extension_toggles') - .select('enabled') - .eq('user_id', userId) - .eq('sector_slug', sectorSlug) - .eq('extension_slug', extensionSlug) - .single() - - // If no toggle row exists, check if this is a legacy extension - if (!data) { - return sectorSlug === 'general' && LEGACY_GENERAL_EXTENSIONS.includes(extensionSlug) - } - - return data.enabled -} diff --git a/lib/extensions/types.ts b/lib/extensions/types.ts index 7c57a7a1..4335a286 100644 --- a/lib/extensions/types.ts +++ b/lib/extensions/types.ts @@ -52,17 +52,6 @@ export interface Sector { extensions: ExtensionDefinition[] } -/** Database row for extension toggle state */ -export interface ExtensionToggle { - id: string - user_id: string - sector_slug: string - extension_slug: string - enabled: boolean - created_at: string - updated_at: string -} - // ============================================================ // Extension Interface & Supporting Types // ============================================================ diff --git a/scripts/clear-user-data.sql b/scripts/clear-user-data.sql index 67841b51..fb5f60c7 100644 --- a/scripts/clear-user-data.sql +++ b/scripts/clear-user-data.sql @@ -101,7 +101,6 @@ BEGIN DELETE FROM public.chat_sessions WHERE user_id = target_user_id; DELETE FROM public.extension_data WHERE user_id = target_user_id; DELETE FROM public.audit_log WHERE user_id = target_user_id; - DELETE FROM public.extension_toggles WHERE user_id = target_user_id; DELETE FROM public.company_settings WHERE user_id = target_user_id; DELETE FROM public.profiles WHERE id = target_user_id; diff --git a/tests/helpers.ts b/tests/helpers.ts index cd053b4a..87713164 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -17,7 +17,6 @@ import type { CompanySettings, InvoiceInboxItem, } from '@/types' -import type { ExtensionToggle } from '@/lib/extensions/types' // ============================================================ // Chainable Supabase mock @@ -487,21 +486,6 @@ export function makeInvoiceInboxItem( } } -export function makeExtensionToggle( - overrides: Partial = {} -): ExtensionToggle { - return { - id: nextId(), - user_id: 'user-1', - sector_slug: 'general', - extension_slug: 'receipt-ocr', - enabled: true, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - ...overrides, - } -} - // ============================================================ // API Route Test Helpers // ============================================================